Troubleshooting
Common issues and how to diagnose them using built-in tools.
Runs Stuck in Pending
A workflow run stays in pending status when the orchestrator has not yet processed its workflow.started event.
Diagnosis:
dagnats run inspect <run-id>Check the run status and step states. If the run shows pending with no steps queued:
- Engine not running: verify
dagnats serveis up and the orchestrator started. Check logs forActorOrchestrator.Starterrors. - Consumer lag: the orchestrator’s durable consumer on
WORKFLOW_HISTORYmay be behind. Check withnats consumer info WORKFLOW_HISTORY orchestratorand look atNum Pending. - NATS connectivity: the engine may have lost its connection. Check
/health– a 503 response means NATS is disconnected.
Resolution: Restart dagnats serve. The orchestrator keeps no state between events: its durable consumer continues from the last event it acknowledged, and run state is in the workflow_runs snapshots, so in-flight runs carry on.
Tasks Not Being Picked Up
Steps are stuck in queued status – the engine published tasks but no worker is processing them.
Diagnosis:
dagnats workers listIf no workers appear, none are connected. If workers appear but tasks are stuck:
- Task type mismatch: the worker must handle the exact task type defined in the step. Check
worker.Handle("task-type", ...)matches the step’sTaskTypefield. - Consumer not created: the worker creates a pull consumer on
TASK_QUEUESfiltered to its task type. Checknats consumer ls TASK_QUEUESfor the expected consumer. - Rate limit exhausted: if the step has a rate limit configured, tasks queue until tokens refill. Check the
rate_limitsKV bucket. - Concurrency limit reached: per-task-type or per-workflow concurrency limits can hold tasks. Check the
concurrency_tasksandconcurrency_runsKV buckets. A held task is retried throughSLEEP_TIMERSevery second.
Resolution: Start a worker that handles the correct task type. If rate or concurrency limits are the cause, wait for them to clear or adjust the limits.
Task Timeouts
A step with a Timeout fails with a retriable step timeout exceeded error when its current attempt has run that long. The engine’s watchdog timer does this, not NATS AckWait. See Timeouts.
Common causes:
- Handler too slow: the attempt ran past the step’s
Timeout.ctx.Heartbeat()keeps the claim on the task message (it calls NATSInProgress()), but it does not extend the step timeout. - Time spent paused: time in
Pause()counts toward the timeout, so a step that pauses repeatedly needs aTimeoutthat covers its whole run. - Worker crashed mid-task: when the worker stops extending its claim, NATS redelivers the task after
AckWaitto another worker (or the same one after restart). The redelivery does not restart the step timeout. If the handler is an agent loop, the redelivered iteration starts from the last state committed byContinue(). If it callsJournal(), it also reuses the model responses and tool results the crashed worker recorded, and the engine fails the step withrestart_limitif the worker crashes on five deliveries in a row. See Durable Agents. Rundagnats journal check <run-id>to audit what a journal step recorded. - Network partition: the worker lost its NATS connection. NATS redelivers to another consumer.
Diagnosis:
dagnats run inspect <run-id>Check the step’s Attempts count. If it is climbing toward MaxAttempts, the handler is consistently failing or timing out.
Dead Letter Queue Growing
The DEAD_LETTERS stream receives tasks that exhausted all retry attempts.
Diagnosis:
dagnats dlq list
dagnats dlq list --run=<run-id>Each dead letter entry includes the original task body, the error message, and the run and step IDs. Look at the ERROR column for the root cause.
Common causes:
- Permanent failure: the handler called
FailPermanent(err)to skip retries. This is intentional – fix the underlying issue and usedagnats run retry <run-id>to start it again. - Retry exhaustion:
MaxAttemptsreached. Either increase retries in the retry policy or fix the handler. - Invalid input: the task received data the handler cannot process. Check the workflow definition’s input wiring.
Resolution:
# Re-run a single run (fresh start; the run's input is reused unless you pass new input)
dagnats run retry <run-id>
# Retry all failed runs of a workflow in a time window
dagnats run retry-all --workflow=my-workflow --mode=rerun \
--after=2026-04-01T00:00:00Z --before=2026-04-02T00:00:00Z
# Replay from the DLQ (resume at the failed step, needs the entry to be under 30 days old)
dagnats run retry-all --workflow=my-workflow --mode=replay
# Replay one dead letter by its sequence number
dagnats dlq replay <sequence>Worker Disconnects
Workers disappear from dagnats workers list and in-flight tasks eventually timeout.
Diagnosis:
Check worker logs for NATS connection errors. Common causes:
- NATS server unreachable: network issue or NATS server restart. Workers should auto-reconnect if using the default NATS client options.
- Slow consumer: the worker’s subscription fell behind and NATS disconnected it. This happens with high message volume and slow processing. Increase the worker’s pending message limit.
- Resource exhaustion: the worker process ran out of memory or file descriptors.
The workers KV bucket has a 60-second TTL. If a worker misses two consecutive heartbeats (30s interval), its entry expires and it disappears from the list. The worker itself continues functioning – the directory is observability-only.
Resolution: Fix the underlying connectivity or resource issue. In-flight tasks will timeout and be redelivered by NATS to healthy workers.
Run Inspection
The dagnats run inspect command is the primary diagnostic tool:
dagnats run inspect <run-id>It shows:
- Run status (pending, running, completed, failed, cancelled)
- Each step’s status, attempts, output, and error
- Workflow definition name and version
- Timing information
For deeper investigation, query the event history directly:
nats stream get WORKFLOW_HISTORY --subject "history.<run-id>" --lastThis shows the raw events on the WORKFLOW_HISTORY stream for that run, in order. Since DagNats is event-sourced, this stream contains the complete, authoritative history of every state change.
Cluster Problems
For clusters, start with GET /health/cluster on each node. See Clustering.
cluster quorum did not format startup: the node waited 60 seconds and no JetStream meta leader answered. Check that every node is running, thatnats_cluster_routeslists the other nodes on the route port, thatnats_cluster_nameandnats_cluster_auth_tokenmatch everywhere, and that the route port is reachable between nodes./health/clusterreturns 503: a replicated stream or bucket lost quorum. The names are underjetstream.unavailable. Bring the missing node back./health/clusterreturns 200 withjetstream.degraded: only single-replica assets are unreachable. The cluster still has quorum.- A node left an old replica behind after a restart: nodes before 0.0.27 named themselves from the process ID, so each restart joined as a new JetStream peer. Set
nats_server_name, or upgrade; the name then stays the same across restarts.
Telemetry Export Failures
If spans are not appearing in your observability backend:
- Check
OTEL_EXPORTER_OTLP_ENDPOINT: must be a valid OTLP/HTTP base URL (e.g.,http://localhost:4318) - Check connectivity: the telemetry exporter must reach
{endpoint}/v1/traces - Check TELEMETRY stream: spans are always written to the NATS
TELEMETRYstream regardless of export. If the stream has messages but exports fail, the issue is between DagNats and your backend.
Export failures never affect workflow execution. Telemetry is best-effort.