Break it. Watch it recover.
ReLab is a reliability testing tool for workflows and background jobs. It runs a workflow across real worker processes, breaks it on purpose by killing a worker, delivering a task twice or holding an upstream down, and then records whether the system actually recovered.
Explore a real failureView on GitHub
It is a reliability testing and replay tool. It is not a Temporal replacement. This dashboard is a read-only view of what the journal recorded; everything it shows is also available from relab on the command line.
One real failure, start to finish
A worker ran the workflow's charge step — a recorded effect standing in for a payment — and was killed before it could acknowledge the task. Nobody told the system it had gone. Every step below is an event from that run’s journal, at its real sequence number and its real time.
- Run started#6 23:31:09.439
RUN_STARTEDA worker picked up the first task in the run.
- Failure injected on purpose charge#11 23:31:09.648
FAULT_INJECTEDReLab degraded the real system here. This is the break, and everything after it is the recovery.
- Worker declared gone#12 23:31:11.020
WORKER_LOSTThe holder of this run's task is gone and its leases are released. Usually that is five missed heartbeats; a worker that shuts down deliberately while holding work writes the same event, because what the run experienced is the same either way.
- Worker stopped responding charge#13 23:31:11.220
TASK_LEASE_EXPIREDNobody renewed the hold on this task, so another process concluded the holder is gone. This is the mechanism that also works when a machine loses power.
- Task returned to the queue charge#14 23:31:11.220
TASK_REQUEUEDThe work a vanished worker was holding became claimable again.
- Task running charge#16 23:31:11.255
TASK_STARTEDThe handler began executing. Each attempt writes one of these.
- Duplicate effect prevented charge#17 23:31:11.257
SIDE_EFFECT_SKIPPEDThe retry asked to perform an effect already recorded under the same key, so it was not performed a second time.
- Task finished charge#18 23:31:11.259
TASK_SUCCEEDEDThe handler returned without an error and the result was recorded.
- Workflow completed#23 23:31:11.299
RUN_SUCCEEDEDEvery task finished. This event is the run's last: a finished run's story cannot change.
RECOVEREDfrom 23 recorded events
- Workflow completedyes
- Tasks abandoned0
- Duplicate effects prevented1
- Attempts run4
- Time to recover1.65s
Open the whole run, all 23 events worker-crash-after-effect
Why this exists
Workflow systems get tested on the path where nothing goes wrong. The paths that cost money are the ones that are hard to exercise on purpose:
- A worker dies holding a task. Does the task come back?
- A hold on a task expires while its worker is still alive. Do two workers now run it?
- A retry repeats a step that already charged a customer. Does it charge twice?
- The coordinator restarts with work in flight. Does the work resume?
“We handle worker failure” is either backed by a test that kills a real process, or it is a hope. ReLab is that test: a crash is a real SIGKILL to a real process, and a latency fault really delays the task towards its lease. The upstream-error and database-disconnect faults report the failure the dependency would have produced rather than taking the dependency down, so that a scenario tests what the scheduler does with one failed task and not what a shared pool does to its neighbours.
Who this is for
Use ReLab if you
- Run background jobs or multi-step workflows and have said “it retries” without a test that proves it.
- Need to show a reviewer that killing a worker mid-task is survivable, on a real process rather than a mocked one.
- Want a reproducible failure — same seed, same break — to put in CI next to the happy-path tests.
- Care whether a retry charges a customer twice, and want the answer recorded rather than argued.
ReLab is not
- A production workflow engine. No durable timers, no signals, no queries, no versioned workflow code. v1 is a self-hosted tool for development, staging and CI.
- A Temporal replacement. Different job: this one breaks a workflow on purpose and answers whether it recovered.
- Exactly-once. The guarantee is at-least-once, and an effect already recorded under a key is not performed again. There is a window between performing an effect and recording it — the glossary says where.
- Authenticated. v1 has no API authentication, so nothing here should be exposed to a network you do not control.
How a killed worker’s task comes back
The mechanism, once, in order. Every stage names the event that records it, so the picture can be checked against a real journal rather than believed.
A worker holds the task
The worker holds a lease and renews it while it works. It performs the workflow's charge step — a recorded effect standing in for a payment — and writes it to the ledger under a key.
TASK_LEASEDThe worker is killed
SIGKILL, mid-task. No shutdown, no handover, nothing gets the chance to report it. The renewals simply stop.
FAULT_INJECTEDThe hold expires
Nobody renewed the lease, so the reaper concludes the holder is gone. This is the same path a machine losing power takes.
TASK_LEASE_EXPIREDThe task comes back
The work is claimable again, and another worker takes it under a new attempt number. Nobody had to notice the death for this to happen.
TASK_REQUEUEDThe charge is not repeated
The retry asks for an effect already recorded under that key, so it is not performed a second time.
SIDE_EFFECT_SKIPPEDThe run finishes
Every task done, the whole thing recorded in one gapless sequence that replay can rebuild without the database.
RUN_SUCCEEDED
Nothing in this sequence depends on the dying worker reporting anything, which is the point: a process that has been SIGKILLed, or a machine that has lost power, gets no chance to. What each of these words means.
How a ReLab run works
- 1
Run a workflow
A multi-step workflow runs across worker processes, each task held by one worker at a time.
- 2
Break something
A scenario injects a real degradation from a fixed seed: a worker crash, a duplicate delivery, latency, an HTTP error, a database disconnect.
- 3
Record what happened
Every run and task state change, and the event describing it, are written in one transaction, in a gapless sequence.
- 4
Replay the history
A pure reducer rebuilds the run’s state from the journal alone, with no access to the database it came from.
- 5
Verify the result
Assertions check the recovery: the task came back, the side effect was not repeated, the run reached the state the journal says it reached.
Underneath: PostgreSQL as the only datastore and the coordination point, task leases renewed by the worker holding them, a reaper that releases the leases of workers that stopped answering, an idempotency ledger keyed per effect, and an append-only event log. The guarantees, stated precisely.
Now
A live worker is one that is HEALTHY or SUSPECT: still heartbeating, or doubted but not yet reclaimed. A dead letter is a task that exhausted its attempts. Nothing here is running: these are the counters as they stood when the recording was made.
Latest runs
| Status | Workflow | Run | What was broken | Started | Duration |
|---|---|---|---|---|---|
| SUCCEEDED | effectful v1 | 96ea5bfd | worker-crash-after-effect | 11:31:09 PM | 1.89s |
| SUCCEEDED | data-pipeline v1 | bb06d758 | worker-crash-during-analyze | 11:31:07 PM | 2.28s |
| SUCCEEDED | effectful v1 | c68a432f | duplicate-delivery-on-charge | 11:31:07 PM | 39ms |
| FAILED | data-pipeline v1 | 29780383 | upstream-down-throughout | 11:31:04 PM | 2.97s |
| SUCCEEDED | data-pipeline v1 | e1a8ee6c | nothing | 11:31:03 PM | 47ms |
Go deeper
- README what it is, and a demo you can run
- Glossary every event in a journal, in plain language
- Architecture engine, workers, replay, fault injection
- Reliability guarantees stated precisely, with the limitations kept
- Benchmarks measured, on stated hardware
- Decision records what was decided, and what was rejected