Plugin Daemon Communication#
The Dify API communicates with the plugin daemon β a separate Go-based service (default port 5002) β for all plugin-related work: model dispatch, tool calls, agent strategy execution, and credential validation. The API never runs plugin code directly; every invocation is proxied over HTTP to the daemon.
Dify API ββ(HTTP/SSE)βββΊ Plugin Daemon :5002
X-Api-Key, X-Plugin-ID headers
URL path: plugin/{tenant_id}/...
Communication Model#
All requests go through BasePluginClient in api/core/plugin/impl/base.py. Two transport modes are used:
_request()β synchronous, blockinghttpx.Client.request()call for non-streaming operations_stream_request()β synchronous generator usinghttpx.Client.stream(), consumed line-by-line via SSE for LLM inference, embeddings, and rerank
Both modes use a module-level, process-shared httpx.Client created once at import time . The client is pooled via get_pooled_http_client() and capped at 50 keep-alive / 100 max connections.
Key architectural constraint: Both
_request()and_stream_request()are fully synchronous β they block the calling Gunicorn worker thread for the entire duration of the plugin daemon call, including all LLM inference time.
Configuration#
Defined in PluginConfig (api/configs/feature/__init__.py):
| Variable | Default | Purpose |
|---|---|---|
PLUGIN_DAEMON_URL | http://localhost:5002 | Base URL for the plugin daemon |
PLUGIN_DAEMON_KEY | plugin-api-key | Shared secret β sent as X-Api-Key on every request |
PLUGIN_DAEMON_TIMEOUT | 600.0 s | Per-request timeout; governs how long a worker can be held |
In Docker Compose deployments, set PLUGIN_DAEMON_URL=http://plugin_daemon:5002 (not localhost).
Worker Exhaustion Under Concurrent Load#
The Root Cause#
Gunicorn defaults in Docker to 1 worker and 10 connections :
SERVER_WORKER_AMOUNT=1
SERVER_WORKER_CLASS=gevent
SERVER_WORKER_CONNECTIONS=10
Each Gunicorn worker holds its connection to the plugin daemon open for the full duration of a plugin call β up to PLUGIN_DAEMON_TIMEOUT (default 600 s) for slow LLM inference. When an Agent node executes, it serializes multiple tool calls through the plugin daemon, each blocking the worker. With the default single worker, one in-flight Agent execution can starve all other API requests, causing complete unresponsiveness until the agent finishes or times out.
This is a documented failure mode in v1.15.0 β users report that even SERVER_WORKER_AMOUNT=10 may not fully resolve the issue, because a sufficiently concurrent Agent workload can still saturate the enlarged pool before per-request timeouts fire.
Why Agent Nodes Are Especially Prone#
Agent nodes are the worst-case consumer because:
- They make repeated sequential plugin daemon calls per turn (strategy invocation β tool call(s) β LLM call(s)), all while holding the same Gunicorn worker.
- The
PluginAgentClientroutes agent strategy execution through the daemon, which in turn calls LLM provider plugins β stacking latency. - MCP tool calls via the plugin daemon can individually be long-lived, and each blocks synchronously.
Gunicorn Startup#
The entrypoint.sh launches Gunicorn as :
gunicorn \
--workers ${SERVER_WORKER_AMOUNT:-1} \
--worker-class ${SERVER_WORKER_CLASS:-geventwebsocket.gunicorn.workers.GeventWebSocketWorker} \
--worker-connections ${SERVER_WORKER_CONNECTIONS:-10} \
--timeout ${GUNICORN_TIMEOUT:-200} \
app:socketio_app
The gevent worker class provides cooperative concurrency within each worker process; however, httpx.Client calls are not greenlet-cooperative in the plugin daemon path, so they still block the gevent event loop for that worker.
Mitigations#
| Action | How |
|---|---|
| Increase worker count | Set SERVER_WORKER_AMOUNT=4 (minimum) or match CPU core count in .env |
| Reduce plugin daemon timeout | Lower PLUGIN_DAEMON_TIMEOUT from 600 to 60β120 s to release blocked workers sooner |
| Increase Celery concurrency | Set CELERY_WORKER_CONCURRENCY=10 or higher for workflow-heavy workloads |
| Verify daemon URL | Ensure PLUGIN_DAEMON_URL=http://plugin_daemon:5002 in Docker, not localhost |
Related Files#
| File | Purpose |
|---|---|
api/core/plugin/impl/base.py | BasePluginClient β all synchronous HTTP methods |
api/configs/feature/__init__.py | PluginConfig β daemon URL, key, and timeout |
docker/.env.example | Default SERVER_WORKER_AMOUNT, GUNICORN_TIMEOUT, etc. |
api/docker/entrypoint.sh | Gunicorn startup command |
api/gunicorn.conf.py | Gunicorn hooks (gevent + gRPC patching) |
api/core/helper/http_client_pooling.py | Shared httpx.Client pooling factory |
| Issue #38912 | Canonical bug report: Agent node API hang pattern |