Troubleshooting
Start with the symptom. Each entry names where to look, the likely cause, and what to do.
| Symptom | Go to |
|---|---|
| A job failed and I need the reason | My script failed |
Job sits in PENDING, nothing runs |
Nothing is running |
| Job never runs, shows as parked | Parked jobs |
Job ended DEAD |
Dead-letter queue |
| A worker will not register, or vanished | Worker problems |
Worker unreachable: accepted no reply |
The job never ran |
| A service deployed but is not reachable | Service problems |
A service shows LIVE but is dead |
Service problems |
| The Timeline is empty for a pipeline | No spans to show |
The DAG list shows everything as PENDING |
Status looks wrong |
| Store disconnected, or dropped writes | Store problems |
| Ephemeral workers are not going away | Workers will not descale |
running is higher than occupied slots |
Orphaned work |
My script failed
1. Find out that it failed
Any of these will tell you:
- Timeline tab — the failures block lists every failed span, and always renders even when empty.
- Cluster tab — the
Dead lettertile turns red when a job exhausted its retries. - SDK —
client.get_job_status("DAG-my-job")returnsFAILEDorDEAD. - DAG Visualizer — the node turns red. Note the caveat on that page: live status resolves through the client-side manifest and can lag, so confirm against the Timeline.
2. Get the reason
The Timeline's failures block gives a short reason per attempt: the exit code plus the last
meaningful line of the traceback. The last line is deliberate, because that is the line naming the
error; the first only says Traceback (most recent call last).
DAG-etl-transform #2 · FAILED on 127.0.0.1:8080
exit 1: ValueError: could not convert string to float: 'n/a'
Three reason shapes mean different things:
| Reason | Meaning | Where the fault is |
|---|---|---|
exit 5: ValueError: … |
The script ran and failed | Your code |
Deployment Failed: … port 9972 never became reachable after 20s |
Process started, never bound its port | Your service's startup |
Worker unreachable: … accepted no reply |
Transport failure | The cluster, not your script |
For the full stdout and stderr rather than the one-line summary:
- Dashboard — click the node in the DAG Visualizer for its live log stream, or open
/logs/<job_id>. - SDK —
client.fetch_logs("DAG-my-job"). - Workspace files — the file panel lists everything the job wrote, including any log or output file it produced itself.
3. Understand what already happened automatically
Before you re-run anything, know that Titan already tried:
- Bounded retry. A failed job is retried while
retryCount <= 3, so up to 3 retries, 4 attempts total. Each attempt is its own span, which is why one job can draw four bars on the Timeline. - Fail-fast. Deterministic failures are not retried — a service that never bound its port will not bind on a retry, so those are failed immediately rather than burning the retry budget.
- Dead-letter. Once retries are exhausted the job is marked
DEAD, quarantined in the DLQ with its reason preserved, and its dependent children are cancelled so the rest of the graph is not left waiting on something that will never arrive.
So FAILED means it may still retry. DEAD means it is finished and nothing downstream will run.
4. Re-run it
| What you want | How |
|---|---|
| Re-run one job, immediately | Dashboard: Replay on the node. Re-submits that job with its dependencies stripped, so it runs now rather than waiting on parents. |
| Re-run the whole pipeline | Dashboard: Redeploy on the DAG. Replays the stored submission payload from the manifest. |
| Re-run after fixing the script | Re-deploy the file first (client.deploy_script("path/to/job.py")), then Replay or Redeploy. Replay re-runs the stored job definition, so it picks up the new script but not a changed job definition. |
| Re-run with a changed definition | Submit it again from the SDK or YAML. Job IDs are unique per run, so nothing is overwritten. |
| Stop the rest of the graph | Dashboard cancel, or client.cancel_job("DAG-my-job") if available in your SDK build. Cancelling cascades to children. |
Replay and Redeploy read the manifest
Both resolve the job or DAG payload from .titan_dag_manifest.json on the machine running the
dashboard. If that file was lost or reset, they return No manifest found — re-submit from the
SDK or YAML instead.
Nothing is running
Cluster tab → Why work isn't moving → Pending reasons. The scheduler states why it could not place the job on its last attempt, and the wording tells you which fix applies:
| Reason | Fix |
|---|---|
no worker registered with capability X |
Start a worker offering X. See Parked jobs. |
N worker(s) match X but all are at capacity |
Wait, or add capacity. Not an error. |
If the Ready queue tile is red, there is queued work and nothing running — a genuine stall
rather than a busy cluster. Check the Workers tile is non-zero and the store is UP.
Parked jobs
A job whose required capability no worker offers is moved out of the queue entirely and parked, rather than being re-queued forever. Cluster tab → Before dispatch → Parked lane.
The capability panel prints the exact command. Start a matching worker and parked jobs are released automatically, event-driven — no resubmit needed:
Parked jobs do not expire by default (titan.scheduler.unschedulable.deadline.ms is 0), so they
accumulate until a worker appears or you cancel them.
Dead-letter queue
Cluster tab → Why work isn't moving → Dead-letter queue. Each entry keeps the job ID, its attempt count, and the reason it died. Non-zero means something is failing repeatably, not just slowly. See My script failed for how to read the reason and re-run.
Worker problems
It never registered. Check the worker's own startup output:
If that line is missing the process did not start. If it started but never appears in Topology, the Master is unreachable from it — check port 9090 and any firewall between them.
It was there and vanished. Cluster tab → Scaling history. A WORKER_LOST entry with
heartbeat failed means the Master stopped getting answers on the 10-second heartbeat. Its
in-flight jobs are recovered and re-queued automatically.
It registered as ephemeral when you wanted permanent. Permanence is a startup argument, not a property of the port. Pass it explicitly:
Without the trailing true the worker defaults to ephemeral and becomes eligible for reclaim.
The job never ran
Worker unreachable: 127.0.0.1:8099 accepted no reply
(process gone, port closed, or network down). The job never ran —
this is not a script failure.
Transport failure, so do not debug your script. The worker died, its port closed, or the network dropped between dispatch and reply. The job is re-queued through the normal retry path. If it repeats, see Worker problems.
Service problems
Deployed but not reachable. The reason will read
port never became reachable after 20s. The process started; the readiness probe failed. Either it
binds a different port than declared, it binds 127.0.0.1 instead of 0.0.0.0, or it takes longer
than the readiness window to come up.
Port already in use. Titan pre-checks the port and refuses rather than deploying on top of a squatter. Pick another port or stop what is holding it.
Shows LIVE but is dead. A known limitation: readiness is probed once, at deploy, and never
re-probed. LIVE means the Master still has it registered, not that it answered recently. Treat
the Services panel as a roster and confirm by dialling the address it lists.
The deploy job says COMPLETED while the service runs. That is correct, not a bug. A SERVICE
job finishes the moment readiness passes; the process it launched keeps serving.
No spans to show
The Timeline's pipeline picker labels what is actually available:
| Label | Meaning |
|---|---|
· 33 spans |
In memory, draws immediately |
· 30 spans (disk only) |
Ran before this Master started — switch window to a from disk option |
· no spans on record |
Never dispatched (a parked job has no span), or it predates span persistence |
Spans live in a 5000-entry in-memory ring that is cleared when the Master restarts, plus
day-partitioned JSONL on disk retained for titan.spans.retain.days (7). If a whole window looks
empty, check the window selector first — a narrow window hides older spans.
Status looks wrong
If the DAG list or graph shows stale or uniformly PENDING states, the live view is resolving
status through the client-side manifest, which is being reworked. The authoritative sources are:
- Timeline — reads execution spans
- Cluster — reads the store
- SDK —
client.get_job_status(job_id)resolves from live state, then the store
Trust those when they disagree with the graph.
Store problems
Cluster tab → the TitanStore strip.
Disconnected— the store is not reachable at the endpoint shown. Titan fails open, so jobs keep running, but nothing is being persisted and a Master restart will lose in-flight state.- Dropped writes > 0 is the real outage signal. Because the store fails open, the error count stays at zero while writes are being discarded.
- State drift — the store lists workers the Master no longer has. Self-correcting; if it persists, the store has entries no live node is refreshing.
Workers will not descale
Reclaim requires all of: the worker is ephemeral, not the root node, at zero load, hosting no service, and idle for more than 45 seconds. Highest port first, one per cycle.
A node hosting a long-running service is never reclaimed, which is intended.
Orphaned work
If the Running count exceeds occupied slots, the Master is counting jobs no live worker is
executing. The Saturation legend says so directly:
N job(s) counted running with no live slot.
This is recovered automatically — within one heartbeat cycle when the worker's death is detected, or within 30 seconds by the reconciliation sweep for any other exit. If it persists beyond that, that is a bug worth reporting.
Useful settings
| Variable | Effect |
|---|---|
TITAN_MASTER_HOST, TITAN_MASTER_PORT |
Point the dashboard at a Master on another machine |
TITAN_DASHBOARD_DEMO=0 |
Disable the demo runner, the only endpoint that mutates the cluster |
titan.spans.retain.days |
How long span history is kept on disk (default 7) |
titan.metrics.spans.max |
In-memory span ring size (default 5000) |
titan.scheduler.unschedulable.deadline.ms |
Expire parked jobs after this long (default 0, never) |
titan.worker.heartbeat.interval |
Heartbeat period in seconds (default 10) |
Which page answers which question
| Question | Page |
|---|---|
| What ran, and did it succeed? | DAG Visualizer |
| Why did the run take as long as it did? | Execution Timeline |
| Why isn't work moving right now? | Cluster & Control Plane |
| Is the cluster getting slower over time? | Cluster → Analytics |