Skip to content

WebSocket Support

Navigator provides comprehensive WebSocket support for real-time applications, including Action Cable integration, standalone WebSocket servers, and connection management.

Overview

Navigator supports WebSocket connections through multiple approaches:

  • Rails Action Cable: Standard Rails WebSocket integration
  • Standalone WebSocket Servers: External WebSocket services
  • Pattern-Based Routing: Flexible WebSocket endpoint matching
  • Connection Management: Proper connection lifecycle handling

Action Cable Integration

Basic Action Cable Setup

applications:
  tenants:
    - name: main-app
      path: /
      working_dir: /var/www/app

    # Dedicated Action Cable tenant
    - name: cable
      path: /cable
      working_dir: /var/www/app
      force_max_concurrent_requests: 0  # Unlimited connections for WebSockets

Rails Configuration

config/cable.yml
production:
  adapter: redis
  url: <%= ENV['REDIS_URL'] %>
  channel_prefix: myapp_production

development:
  adapter: redis
  url: redis://localhost:6379/1
config/routes.rb
Rails.application.routes.draw do
  # Action Cable endpoint
  mount ActionCable.server => '/cable'

  # Your other routes
  resources :messages
end
server:
  listen: 3000
  static:
    public_dir: /var/www/app/public

pools:
  max_size: 10
  idle_timeout: 600        # Longer timeout for persistent connections

applications:
  global_env:
    RAILS_ENV: production
    REDIS_URL: "${REDIS_URL}"
    ACTION_CABLE_ALLOWED_REQUEST_ORIGINS: "https://myapp.com"

  tenants:
    # Main HTTP application
    - name: web
      path: /
      working_dir: /var/www/app

    # Action Cable WebSocket connections
    - name: cable
      path: /cable
      working_dir: /var/www/app
      special: true    # Skip environment variable substitution
      env:
        RAILS_ENV: production
        ACTION_CABLE_URL: "ws://localhost:3000/cable"
      force_max_concurrent_requests: 0  # Allow unlimited WebSocket connections

managed_processes:
  - name: redis
    command: redis-server
    args: [/etc/redis/redis.conf]
    auto_restart: true

Standalone WebSocket Servers

External WebSocket Service

applications:
  tenants:
    - name: main-app
      path: /
      working_dir: /var/www/app

    # Proxy to external WebSocket server
    - name: websocket-service
      path: /ws/
      standalone_server: "localhost:8080"  # External WebSocket server

Multiple WebSocket Endpoints

applications:
  tenants:
    - name: web-app
      path: /

    # Different WebSocket services
    - name: chat-service
      path: /chat/
      standalone_server: "chat-server:9000"

    - name: notifications
      path: /notifications/
      standalone_server: "notification-server:9001"

    - name: live-updates
      path: /live/
      standalone_server: "update-server:9002"

Pattern-Based WebSocket Routing

Flexible Path Matching

applications:
  tenants:
    # Match any path ending with /socket
    - name: websocket-wildcard
      path: /api/
      match_pattern: "*/socket"
      working_dir: /var/www/app
      force_max_concurrent_requests: 0

    # Match tenant-specific WebSocket paths
    - name: tenant-websockets
      path: /tenants/
      match_pattern: "/tenants/*/ws"
      working_dir: /var/www/app

Multi-Tenant WebSocket Setup

applications:
  env:
    TENANT_ID: "${tenant_id}"
    WEBSOCKET_PATH: "/tenants/${tenant_id}/ws"

  tenants:
    - name: tenant-a-ws
      path: /tenants/a/ws
      working_dir: /var/www/app
      var:
        tenant_id: "tenant_a"
      force_max_concurrent_requests: 0

    - name: tenant-b-ws  
      path: /tenants/b/ws
      working_dir: /var/www/app
      var:
        tenant_id: "tenant_b"
      force_max_concurrent_requests: 0

WebSocket Connection Tracking

Navigator can track active WebSocket connections to prevent apps from shutting down during idle timeouts while connections are active. This feature is enabled by default but can be configured globally or per-tenant.

When to Disable Tracking

Disable WebSocket tracking for tenants that: - Proxy WebSockets to standalone servers (e.g., separate Action Cable service) - Don't handle WebSocket connections directly - Have minimal memory/performance requirements

Global Configuration

applications:
  track_websockets: true  # Default: true (enabled)

  tenants:
    - name: web-app
      # Inherits global setting (true)
      path: /

    - name: api-app
      # Inherits global setting (true)
      path: /api

Per-Tenant Override

applications:
  track_websockets: true  # Global default

  tenants:
    # This app proxies WebSockets elsewhere - disable tracking
    - name: web-app
      path: /
      track_websockets: false  # Override: disable tracking

    # This app handles WebSockets directly - keep tracking enabled
    - name: chat-app
      path: /chat
      track_websockets: true   # Override: explicitly enable

    # This app doesn't specify - inherits global (true)
    - name: api-app
      path: /api

Example: Reverse Proxy to Action Cable

applications:
  track_websockets: true  # Global default

  tenants:
    # Main Rails app - proxies /cable to standalone server
    - name: web
      path: /
      track_websockets: false  # Don't track (proxies WebSockets elsewhere)

# Reverse proxy to standalone Action Cable server
routes:
  reverse_proxies:
    - path: "^/cable"
      target: "http://localhost:28080"
      websocket: true  # Enable WebSocket support for proxy

How Tracking Works

When track_websockets: true (default): - Navigator counts active WebSocket connections per tenant - Apps with active connections won't stop during idle timeout - Prevents unexpected WebSocket disconnections - Small memory overhead for connection tracking

When track_websockets: false: - No WebSocket connection counting - App stops normally after idle timeout - Slightly lower memory usage - Use when app doesn't handle WebSockets directly

Connection Management

Connection Limits

pools:
  max_size: 20             # More processes for WebSocket connections
  idle_timeout: 1800       # Longer timeout (30 minutes)

applications:
  tenants:
    # Regular HTTP requests - limited concurrency
    - name: web
      path: /
      max_concurrent_requests: 5

    # WebSocket connections - unlimited
    - name: websockets
      path: /ws/
      force_max_concurrent_requests: 0  # No limit for persistent connections

Connection Monitoring

# Enable debug logging for connection tracking
applications:
  global_env:
    LOG_LEVEL: debug
    ACTION_CABLE_LOG_LEVEL: debug

Production Configuration

High-Performance WebSocket Setup

server:
  listen: 3000
  static:
    public_dir: /var/www/app/public

pools:
  max_size: 30             # More processes for high connection count
  idle_timeout: 3600       # 1 hour - keep connections alive
  start_port: 4000

# Disable suspension for WebSocket servers
suspend:
  enabled: false           # WebSockets prevent proper suspension

applications:
  global_env:
    RAILS_ENV: production
    REDIS_URL: "${REDIS_URL}"
    ACTION_CABLE_ALLOWED_REQUEST_ORIGINS: "${ALLOWED_ORIGINS}"

    # WebSocket-specific settings
    ACTION_CABLE_WORKER_POOL_SIZE: "10"
    ACTION_CABLE_MAX_REQUEST_SIZE: "1048576"  # 1MB

  tenants:
    - name: web
      path: /
      working_dir: /var/www/app
      max_concurrent_requests: 10

    - name: websockets
      path: /cable
      working_dir: /var/www/app
      force_max_concurrent_requests: 0
      env:
        ACTION_CABLE_MOUNT_PATH: "/cable"

managed_processes:
  - name: redis
    command: redis-server
    args: [--maxmemory, 512mb, --maxmemory-policy, allkeys-lru]
    auto_restart: true

Security Configuration

Origin Validation

applications:
  global_env:
    # Restrict WebSocket origins in production
    ACTION_CABLE_ALLOWED_REQUEST_ORIGINS: "https://myapp.com,https://www.myapp.com"

    # Development - allow all origins
    # ACTION_CABLE_ALLOWED_REQUEST_ORIGINS: "*"
config/environments/production.rb
# Action Cable security
config.action_cable.allowed_request_origins = [
  'https://myapp.com',
  'https://www.myapp.com'
]

# Use secure WebSocket URLs
config.action_cable.url = 'wss://myapp.com/cable'

Authentication Integration

app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private

    def find_verified_user
      # Authentication via session
      if verified_user = User.find_by(id: session[:user_id])
        verified_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

Load Balancing and Scaling

Multi-Instance WebSocket Setup

server:
  listen: 3000
  static:
    public_dir: /var/www/app/public

pools:
  max_size: 30             # More processes for high connection count
  idle_timeout: 3600       # 1 hour - keep connections alive
  start_port: 4000

# Disable suspension for WebSocket servers
suspend:
  enabled: false           # WebSockets prevent proper suspension

### Load Balancer Configuration

```nginx title="nginx.conf - WebSocket Load Balancing"
upstream websocket_backend {
    # Enable session persistence for WebSockets
    ip_hash;

    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
    listen 80;
    server_name myapp.com;

    # WebSocket proxy
    location /cable {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket specific timeouts
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }

    # Regular HTTP traffic
    location / {
        proxy_pass http://websocket_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Monitoring WebSocket Connections

Connection Metrics

#!/bin/bash
# WebSocket connection monitoring

# Count active WebSocket connections
websocket_connections=$(netstat -an | grep :3000 | grep ESTABLISHED | wc -l)
echo "websocket_connections $websocket_connections"

# Monitor Rails processes handling WebSockets
rails_processes=$(pgrep -f "rails server" | wc -l)
echo "websocket_rails_processes $rails_processes"

# Check Redis connections (for Action Cable)
redis_connections=$(redis-cli info clients | grep connected_clients | cut -d: -f2)
echo "websocket_redis_connections $redis_connections"

# Memory usage of WebSocket processes
websocket_memory=$(ps aux | grep -E "(rails|navigator)" | awk '{sum+=$6} END {print sum}')
echo "websocket_memory_kb $websocket_memory"

Action Cable Monitoring

config/initializers/action_cable_monitoring.rb
# Monitor Action Cable performance
ActionCable.server.config.logger = Rails.logger

# Custom metrics for Action Cable
Rails.application.config.after_initialize do
  ActionCable.server.config.connection_class.prepend(Module.new do
    def connect
      Rails.logger.info "ActionCable connection established: #{request.remote_ip}"
      super
    end

    def disconnect
      Rails.logger.info "ActionCable connection closed: #{request.remote_ip}"
      super
    end
  end)
end

Troubleshooting

WebSocket Connection Issues

Issue: WebSocket connections fail to establish

Debug Steps:

# Check Navigator WebSocket support
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
     -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: test" \
     http://localhost:3000/cable

# Check Rails Action Cable
rails console
> ActionCable.server.config.logger = Logger.new(STDOUT)

# Test WebSocket connection
wscat -c ws://localhost:3000/cable

High Memory Usage

Issue: WebSocket connections cause high memory usage

Solutions:

# Limit connection lifetime
applications:
  global_env:
    ACTION_CABLE_CONNECTION_TIMEOUT: "300"  # 5 minutes

# Use Redis for connection state
applications:
  global_env:
    REDIS_URL: "${REDIS_URL}"
    ACTION_CABLE_ADAPTER: "redis"

Connection Drops

Issue: WebSocket connections drop frequently

Solutions:

# Increase timeouts
pools:
  idle_timeout: 3600       # 1 hour

# Disable suspension for WebSocket servers
suspend:
  enabled: false

# Configure keepalive
applications:
  global_env:
    ACTION_CABLE_KEEPALIVE: "30"  # 30 seconds

Integration Examples

Real-Time Chat Application

app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end

  def receive(data)
    ActionCable.server.broadcast("chat_#{params[:room]}", {
      message: data['message'],
      user: current_user.name,
      timestamp: Time.current
    })
  end
end
navigator.yml
applications:
  tenants:
    - name: web
      path: /

    - name: chat
      path: /cable
      force_max_concurrent_requests: 0
      env:
        ACTION_CABLE_ALLOWED_REQUEST_ORIGINS: "https://myapp.com"

Live Dashboard Updates

app/channels/dashboard_channel.rb
class DashboardChannel < ApplicationCable::Channel
  def subscribed
    stream_from "dashboard_#{current_user.id}"
  end
end

# Broadcast updates from controllers
class MetricsController < ApplicationController
  def update
    # Update metrics logic

    ActionCable.server.broadcast("dashboard_#{current_user.id}", {
      metrics: updated_metrics,
      timestamp: Time.current
    })
  end
end

Notification System

app/channels/notifications_channel.rb
class NotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notifications_#{current_user.id}"
  end
end

# Background job to send notifications
class NotificationJob < ApplicationJob
  def perform(user_id, notification)
    ActionCable.server.broadcast("notifications_#{user_id}", notification)
  end
end

Best Practices

1. Connection Limits

# Set appropriate limits
applications:
  tenants:
    - name: websockets
      force_max_concurrent_requests: 0  # Unlimited for WebSockets
    - name: web
      max_concurrent_requests: 10       # Limited for HTTP

2. Resource Management

# Allocate sufficient resources
pools:
  max_size: 25             # More processes for connections
  idle_timeout: 3600       # Longer timeouts for persistent connections

3. Security

# Validate origins in production
applications:
  global_env:
    ACTION_CABLE_ALLOWED_REQUEST_ORIGINS: "https://yourdomain.com"

4. Monitoring

# Monitor connection health
netstat -an | grep :3000 | grep ESTABLISHED | wc -l
redis-cli info clients

5. Graceful Degradation

// Client-side reconnection logic
const cable = ActionCable.createConsumer('/cable');

cable.subscriptions.create('ChatChannel', {
  connected() {
    console.log('WebSocket connected');
  },

  disconnected() {
    console.log('WebSocket disconnected - attempting reconnect');
    // Implement exponential backoff reconnection
  },

  received(data) {
    // Handle received data
  }
});

See Also