Deemwar Monads & Abstractions

Deterministic simulation testing in celld

Source: Deterministic simulation testing in celld: exploring execution orders and reproducing bugs, by Yusuke Tanaka

celld runs Cloudflare Workers and Durable Objects apps on your own machines. Its team runs the production code inside a simulator that picks every event order from a seed. One seed found an alarm race no hand-written test described, and the same seed replays it on demand.

1 seed replays a whole run
10:00 → 10:05 the alarm move that broke
0 hand-written tests prescribed the order

The problem

A bug in a distributed system “can depend on a particular sequence of delayed messages, failed writes, and node restarts.” Those events can arrive in a different order on the next run, so the test passes and the bug hides. Without the same order again, nobody can trace the cause or prove a fix works.

Peter Deutsch’s Eight Fallacies of Distributed Computing names the assumptions that cause this, including “The network is reliable” and “Latency is zero.”

How DST works in celld

celld separates the code that selects the next event from the code that handles it. The simulator owns selection; production code does the handling, unchanged.

Task orderThe simulator decides when each asynchronous task runs.
StorageIt can make an object-storage write succeed, fail or wait.
TimeIt advances a simulated clock, so retries and alarms need no real waiting.
SeedEvery random choice comes from one seed. Same code, settings and seed: same sequence, states and result.

Each test runs this loop up to a configured number of actions. An action is delivering a request, letting a task run, advancing the clock, or completing a storage operation.

  1. 1

    List candidates

    simulation.availableActions()

    If none remain, check completion conditions, such as every planned request having finished, and stop.

  2. 2

    Choose one

    simulation.choose · seeded

    The seeded generator picks the next action, and decides whether storage succeeds or which fault fires.

  3. 3

    Run it

    production code

    celld handles the event exactly as it would in production.

  4. 4

    Check invariants

    checker

    After every action, the checker tests conditions the system must uphold throughout the run, using observed responses and stored data.

  5. pass → back to 1 · fail → keep the seed and replay the exact run

The alarm race

An alarm runs a cell’s code at a set time. celld can unload an idle cell, so it finds sleeping cells by scanning wake entries in object storage rather than opening every cell’s SQLite database. To save writes, celld reused wake entries.

  1. 1

    Delete the 10:00 alarm

    client

    celld clears the time in SQLite, saves it, and returns success. A separate cleanup task will remove the 10:00 wake entry later.

  2. 2

    Set a 10:05 alarm

    client

    celld reuses the 10:00 wake entry to wake the cell early enough, and returns success. The old cleanup is still waiting.

  3. 3

    The old cleanup runs

    simulator advances it

    It deletes the 10:00 wake entry that the new alarm still needed.

  4. 4

    Invariant violated

    checker

    The 10:05 alarm is set in SQLite, but no wake entry can reactivate the cell. If the node restarted before 10:05, the alarm could fail to run despite the success response.

Two successes, one late cleanup, one missing wake entry.

“We had not written a test that prescribed this sequence.” The simulator found it by exploring requests and pending work.

The fix

Foundby exploring seeds
Replayedsame seed
Redesignedown wake entry
Regression testchecks during the run
BugLessonRule now
ReuseA delayed cleanup for an old alarm can delete what a new alarm depends on.Each alarm setting gets its own wake entry; old ones go only once celld confirms they are no longer needed.
ChecksAn entry that vanishes and returns looks fine at the end.The regression test repeats the failing order and checks the invariant during the run.

Try it yourself

General guidance, not celld code. The portable idea fits any system with queues, timers or retries: put every source of nondeterminism behind one seeded scheduler, and check your invariants after every step.

rng = Random(seed)
for step in range(max_actions):
    actions = sim.available_actions()
    if not actions:
        check_completion(sim); break
    sim.run(rng.choice(actions))
    check_invariants(sim)   # every step, not only the end

Guardrails

Read the source

Deterministic simulation testing in celld, Yusuke Tanaka, October 2, 2026.

Discussion on Hacker News.

Sources: every fact on this page comes from the celld article above. celld is by Deno Land Inc., Apache-2.0.