Workflow Timeout and Execution Limits#
Dify enforces a three-layer timeout and execution-limit stack to prevent zombie runs, cap resource usage, and surface failures cleanly across all workflow execution paths.
| Layer | Scope | Key settings | Default |
|---|---|---|---|
| App-level | SSE consumer / queue | APP_MAX_EXECUTION_TIME | 3 600 s |
| Workflow-level | Graph engine | WORKFLOW_MAX_EXECUTION_STEPS / WORKFLOW_MAX_EXECUTION_TIME | 500 steps / 3 600 s |
| Agent-backend-level | Pydantic AI run | DIFY_AGENT_RUN_TIMEOUT_SECONDS | 3 600 s |
All settings are Pydantic BaseSettings fields, overridable via environment variables. Config sources: AppExecutionConfig / WorkflowConfig, AgentBackendConfig, and ServerSettings (dify-agent).
Layer 1 β App-Level Timeout (AppExecutionCoordinator)#
Introduced in PR #39813, AppExecutionCoordinator is a per-execution-attempt cancellation policy owner. The coordinator manages two abort conditions:
- Timeout: Monitored by an independent watchdog timer thread that fires after
APP_MAX_EXECUTION_TIME(default 3 600 s) - Manual stop:
_is_stopped()β polled inlisten(), reads agenerate_task_stopped:{task_id}key from Redis, TTL-cached for 1 s to avoid hot polling
The timeout was increased from 1 200 s to 3 600 s to allow Agent Backend runs to complete their full internal timeout (DIFY_AGENT_RUN_TIMEOUT_SECONDS, also 3 600 s by default) without being prematurely terminated by this outer limit. When configuring long-running agent workflows, coordinate APP_MAX_EXECUTION_TIME, WORKFLOW_MAX_EXECUTION_TIME, and DIFY_AGENT_RUN_TIMEOUT_SECONDS to ensure the outer limits accommodate the agent run deadline.
Each execution attempt creates its own coordinator instance via AppQueueManager.__init__. The watchdog starts when listen() begins and runs independently of the SSE response-streaming loop β so the timeout remains active even if the listener detaches.
Cancellation path: When either condition fires, AppExecutionCoordinator.request_abort(reason) is called (guarded by _abort_sent to run at most once). It:
- Writes the Redis stop key via
set_app_task_stop_flag(task_id)(TTL 600 s) - Calls
GraphEngineManager(redis_client).send_stop_command(task_id, reason=...)to signal the graph engine directly - For timeout-triggered aborts, publishes a
QueueStopEventvia the coordinator'son_timeoutcallback so the SSE client receives a terminal signal
Listener lifecycle: Listener detachment (SSE client disconnect) is treated as an observation, not a cancellation trigger. The coordinator logs the detachment event but does not issue a stop command β allowing streaming workflow execution to continue in the Celery worker and publish durable events for later subscribers (e.g. via the workflow events endpoint).
State transitions: The coordinator tracks AppExecutionState (RUNNING, PAUSED, ABORTING, TERMINAL). PAUSED and TERMINAL both cancel the watchdog timer β preventing a timeout-triggered abort from leaking into a resumed workflow run. A resumed workflow creates a new coordinator instance even though it reuses the same stable task_id.
A user-initiated stop goes through set_stop_flag(task_id, invoke_from, user_id), which performs ownership verification before writing the same Redis key.
Human-input pause timeout: HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS (default 7 days) caps how long a workflow can stay paused waiting for human input. A background Celery beat task (ENABLE_HUMAN_INPUT_TIMEOUT_TASK, interval 1 min) enforces this .
Layer 2 β Workflow-Level ExecutionLimitsLayer#
Introduced in PR #24116, ExecutionLimitsLayer is a graphon GraphEngine layer that enforces step count and wall-clock time limits inside the graph engine itself β independent of the SSE consumer loop.
Registration β WorkflowEntry.__init__ attaches the layer to every top-level engine:
limits_layer = ExecutionLimitsLayer(
max_steps=dify_config.WORKFLOW_MAX_EXECUTION_STEPS,
max_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME
)
self.graph_engine.layer(limits_layer)
Tracked limits (from WorkflowConfig):
| Config var | Default | Description |
|---|---|---|
WORKFLOW_MAX_EXECUTION_STEPS | 500 | Max nodes executed per run |
WORKFLOW_MAX_EXECUTION_TIME | 3 600 s | Max wall-clock time per run |
WORKFLOW_CALL_MAX_DEPTH | 5 | Max nested sub-workflow call depth |
The workflow timeout was increased from 1 200 s to 3 600 s to allow Agent Backend runs to complete their full internal timeout (DIFY_AGENT_RUN_TIMEOUT_SECONDS, also 3 600 s by default) without being prematurely terminated by this outer limit. When configuring long-running agent workflows, coordinate APP_MAX_EXECUTION_TIME, WORKFLOW_MAX_EXECUTION_TIME, and DIFY_AGENT_RUN_TIMEOUT_SECONDS to ensure the outer limits accommodate the agent run deadline.
The layer tracks step count from NodeRunStartedEvent / NodeRunSucceededEvent / NodeRunFailedEvent. When either limit is exceeded it sends an AbortCommand via the engine's command_channel (guarded by _abort_sent to prevent duplicate aborts). The graph engine emits a GraphRunAbortedEvent which the workflow app runner converts to a QueueWorkflowFailedEvent with the abort reason .
Scope note: only the parent GraphEngine receives ExecutionLimitsLayer. Child engines created for iteration/loop nodes do not receive separate execution-limit layers. Step counts from child nodes still contribute to the parent layer's running total because events propagate up through the graph event stream.
Layer 3 β Agent-Backend Run Timeout#
Added in PR #39186 to prevent zombie Agent runs and bound SSE streams; timeout ownership moved to the agent backend in PR #40641.
API-side streaming controls live in AgentBackendConfig:
| Env var | Default | Purpose |
|---|---|---|
AGENT_BACKEND_STREAM_READ_TIMEOUT_SECONDS | 30 s | Per-connection SSE read timeout; stale connection triggers reconnect |
AGENT_BACKEND_STREAM_MAX_RECONNECTS | 3 | Max reconnects before the run is marked failed |
Agent backend run timeout is owned and enforced by the dify-agent backend through ServerSettings and enforced in AgentRunRunner:
| Env var | Default | Purpose |
|---|---|---|
DIFY_AGENT_RUN_TIMEOUT_SECONDS | 3 600 s | Wall-clock deadline for the Pydantic AI agent.run() call |
The timeout is enforced via asyncio.timeout around the Pydantic AI agent.run() call inside AgentRunRunner.run(). When the deadline expires, a TimeoutError is raised and converted to a UsageLimitExceeded error, which is then surfaced through the existing agent_run_limit_exceeded failure contract. Compositor entry, RuntimeLease acquisition, tool preparation, snapshots, and resource cleanup remain outside the timed scope.
The API no longer applies a total deadline to the SSE subscription. A should_stop callable is still accepted by DifyAgentBackendRunClient, allowing the queue manager's stop flag to short-circuit the stream mid-reconnect.
Key Source References#
| File / PR | What to look for |
|---|---|
api/configs/feature/__init__.py β AppExecutionConfig, WorkflowConfig | All timeout/limit env vars and defaults |
api/configs/extra/agent_backend_config.py | API-side agent-backend streaming settings (read timeout, reconnects) |
dify-agent/src/dify_agent/server/settings.py | Dify-agent server settings including run timeout |
dify-agent/src/dify_agent/runtime/runner.py | AgentRunRunner timeout enforcement via asyncio.timeout |
api/core/app/apps/execution_coordinator.py | Per-attempt coordinator, watchdog timer, request_abort(), AppExecutionState transitions |
api/core/app/apps/base_app_queue_manager.py | listen() response loop, manual-stop polling, coordinator initialization, stop-flag Redis helpers |
api/core/workflow/workflow_entry.py β WorkflowEntry.__init__ | ExecutionLimitsLayer registration on the parent engine |
| PR #24116 β feat: add execution limit layer | Original ExecutionLimitsLayer design and abort propagation |
| PR #39186 β fix(agent): bound streams and cancel zombie workflow runs | Agent-backend streaming bounds and cancellation durability |
| PR #40641 β feat(dify-agent): expand and enforce agent run limits | Timeout ownership moved from API to agent backend; 500-request limit |
| PR #39813 β fix(api): decouple response listeners from execution lifecycle | AppExecutionCoordinator introduction, listener-detachment lifecycle, PAUSED / TERMINAL state isolation |