WebSocket Service Architecture#
Dify's WebSocket support is built around a dedicated api_websocket Docker Compose service for real-time workflow collaboration. It uses Socket.IO with a Redis-backed cross-process pub/sub manager for multi-worker correctness, and JWT authentication that requires a shared SECRET_KEY across all API processes.
Service Activation (Profile-Based)#
The api_websocket service runs the same langgenius/dify-api image as the main API, but is only started when the collaboration Compose profile is active . It was separated from the main api service to isolate WebSocket workload .
The service mounts the shared storage volume (./volumes/app/storage:/app/api/storage) to persist user files and the auto-generated .dify_secret_key, and depends on the init_permissions service to ensure correct ownership is set before startup .
In docker/.env.example, COMPOSE_PROFILES includes collaboration by default, so the service starts automatically in standard deployments :
COMPOSE_PROFILES=${VECTOR_STORE:-weaviate},${DB_TYPE:-postgresql},collaboration
The service's worker configuration can be tuned independently from the main API :
| Env Var | Default | Notes |
|---|---|---|
API_WEBSOCKET_WORKER_AMOUNT | 1 | Number of Gunicorn worker processes |
API_WEBSOCKET_WORKER_CLASS | geventwebsocket.gunicorn.workers.GeventWebSocketWorker | Must be this class for WebSocket support |
API_WEBSOCKET_WORKER_CONNECTIONS | 1000 | Max simultaneous connections per worker |
API_WEBSOCKET_GUNICORN_TIMEOUT | 360 | Timeout in seconds |
WEBSOCKET_MAX_HTTP_BUFFER_SIZE | 10485760 (10 MiB) | Maximum Socket.IO / Engine.IO HTTP buffer size in bytes. Large workflow collaboration payloads can exceed the default 1 MiB limit. |
Nginx routes /socket.io/ traffic to the upstream defined by NGINX_SOCKET_IO_UPSTREAM (default: api_websocket:5001) .
Redis-Backed Multi-Worker Pub/Sub#
The core of multi-worker support is socketio.RedisManager, initialized in ext_socketio.py. Without this, Socket.IO room broadcasts are process-local, so a client connected to worker A would not receive events emitted by worker B .
Key implementation details in create_socketio_client_manager():
- Redis URL: reads
dify_config.normalized_pubsub_redis_url, which resolves toPUBSUB_REDIS_URLif set, or falls back to building a URL from the standardREDIS_*variables . - Channel namespacing: the pub/sub channel is
serialize_redis_name("socketio"), which applies the deployment'sREDIS_KEY_PREFIXβ preventing cross-deployment interference on shared Redis instances . - TLS support: if the Redis URL scheme is
rediss://, SSL options (REDIS_SSL_CERT_REQS,REDIS_SSL_CA_CERTS, etc.) are forwarded to the manager . - Global
sioobject:socketio.Serveris instantiated once at module load withasync_mode="gevent",client_manager=create_socketio_client_manager(), andmax_http_buffer_size=dify_config.WEBSOCKET_MAX_HTTP_BUFFER_SIZE. The buffer size setting prevents large workflow graph payloads insync_requestevents from exceeding Engine.IO's limit and disconnecting clients.
PR #38242 also added Redis hash wrapper methods (hkeys, hexists) to ext_redis.py to ensure prefixed key operations work correctly for collaboration session tracking .
βββββββββββββββββββ βββββββββββββββββββ
β api_websocket β β api_websocket β
β (Process A) β β (Process B) β
β sio (Server) β β sio (Server) β
ββββββββββ¬βββββββββ ββββββββββ¬βββββββββ
β β
βββββββββββββ¬ββββββββββββ
Redis pub/sub
(channel: <prefix>:socketio)
JWT Authentication#
Every Socket.IO connection is authenticated on the connect event in api/controllers/console/socketio/workflow.py:
- The access token is extracted from the request environ.
PassportService().verify(token)decodes the JWT using HS256 withdify_config.SECRET_KEY.- The
user_idis pulled from the decoded payload and the account is loaded and permission-checked. - On success, socket identity is saved via the collaboration service; on failure,
Falseis returned, rejecting the connection.
Critical: PassportService uses dify_config.SECRET_KEY for both signing (issue) and verification (verify) . In any deployment where api and api_websocket are separate processes β including multi-worker or Kubernetes β all instances must share the same SECRET_KEY. A mismatch causes InvalidSignatureError and silently drops all WebSocket connections.
When SECRET_KEY is left empty (the documented default in .env.example), the application auto-generates a persistent key in the storage directory. For this to work correctly, all services (api, worker, api_websocket) must mount the same storage volume so they read the same .dify_secret_key file. The storage volume mount on api_websocket ensures this consistency .
Key Files#
| File | Purpose |
|---|---|
api/extensions/ext_socketio.py | Socket.IO server init + RedisManager setup |
api/controllers/console/socketio/workflow.py | connect event handler, JWT auth, room management |
api/libs/passport.py | JWT issue/verify via SECRET_KEY |
api/configs/middleware/cache/redis_pubsub_config.py | normalized_pubsub_redis_url resolution logic |
api/extensions/ext_redis.py | Redis client wrapper with key prefixing |
docker/docker-compose.yaml | api_websocket service definition + collaboration profile |
docker/.env.example | COMPOSE_PROFILES, NGINX_SOCKET_IO_UPSTREAM, worker tuning vars |
Related PRs#
- #35981 β chore: separate websocket service β introduced dedicated
api_websocketservice andcollaborationCompose profile - #38242 β fix: support multi-worker workflow collaboration β added
socketio.RedisManagerfor cross-process pub/sub, configurable worker count - #39424 β fix(docker): mount shared storage volume in api_websocket β added storage volume mount and
init_permissionsdependency toapi_websocketfor SECRET_KEY consistency