Durable Execution
How journaling, replay, and determinism work in Tide
Tide provides durable execution for your workflows. Every side effect your code performs is recorded in a journal. If the process crashes, Tide replays the journal to restore state and continues where it left off -- without re-executing completed work.
This page explains the three pillars of durable execution: the journal, replay, and determinism.
The Journal
Every operation your workflow performs is recorded as a journal entry. Each entry captures the operation type, its result, and whether it errored:
{"op": "op_write_file", "result": null, "is_error": false}
{"op": "op_read_file", "result": "file contents here", "is_error": false}
{"op": "op_console", "result": {"level": "log", "message": "hello\n"}, "is_error": false}
{"op": "op_set_timeout", "result": null, "is_error": false}
{"op": "op_step_begin", "result": {"step": "my-step"}, "is_error": false}
{"op": "op_step_complete", "result": {"step": "my-step", "value": {}}, "is_error": false}The following operations are journaled:
| Operation | Journal Op | Description |
|---|---|---|
readFile | op_read_file | Read a file from the virtual filesystem |
writeFile | op_write_file | Write a file to the virtual filesystem |
removeFile | op_remove_file | Remove a file from the virtual filesystem |
listFiles | op_list_files | List files in the virtual filesystem |
sleep | op_set_timeout | Sleep for a duration |
| Step begin | op_step_begin | Enter a transactional step boundary |
| Step complete | op_step_complete | Complete a transactional step |
console.log / console.error | op_console | Log output from the workflow |
The journal is persisted to the chosen storage backend after each operation. For operations inside a step, entries are buffered and committed atomically when the step completes.
Replay
When you run the same invocation ID again, Tide loads the journal from storage and enters replay mode. During replay:
- Your JavaScript code re-executes from the top of the workflow function.
- Each operation checks the journal instead of executing live. The stored result is returned directly.
readFilereturns the stored result from the journal.writeFilereplays the stored result but still applies the write to the in-memory virtual filesystem (so subsequent reads see the correct state).sleepreturns instantly.- Console output is replayed from the journal. Live
console.logandconsole.errorcalls during replay are suppressed to avoid duplicate output. - Once the journal is exhausted, execution switches to live mode and new operations execute normally against real storage.
Replay in practice
Consider a workflow that performs 5 operations. On the first run, it crashes after operation 3:
First run:
op 1: writeFile("/data.json", "...") -- executes live, journaled
op 2: readFile("/config.json") -- executes live, journaled
op 3: writeFile("/output.txt", "...") -- executes live, journaled
--- crash ---
op 4: (never reached)
op 5: (never reached)When the same invocation ID is run again, Tide loads the 3-entry journal and replays:
Second run:
op 1: writeFile("/data.json", "...") -- REPLAY (instant, returns stored result)
op 2: readFile("/config.json") -- REPLAY (instant, returns stored result)
op 3: writeFile("/output.txt", "...") -- REPLAY (instant, returns stored result)
--- journal exhausted, switch to live mode ---
op 4: sleep(5000) -- executes live, journaled
op 5: readFile("/output.txt") -- executes live, journaledOperations 1 through 3 replay instantly from the journal without touching storage. Operations 4 and 5 execute live as if the crash never happened. The workflow completes successfully.
Determinism
Tide enforces determinism. Your workflow must produce the same sequence of operations on every execution. The replayed code must call the same operations, in the same order, as the original run. If the replayed code calls a different operation than what the journal expects at a given position, Tide throws a determinism violation error.
This constraint is what makes replay safe. Because the operation sequence is identical, Tide can substitute stored results for live execution and know the workflow will behave the same way.
What breaks determinism
The following patterns will cause determinism violations and must be avoided in workflow code:
Math.random()-- Tide does not provide a deterministic randomness API. Random values will differ between the original run and replay, potentially changing which operations execute or in what order.- Branching on real wall-clock time -- Time is frozen in Tide (see below). Do not use external time sources or system clocks to make decisions.
- Reordering operations between runs -- If you change your code so that a
readFilenow happens before awriteFilewhen the journal recorded the opposite order, replay will fail. - Changing code that affects the operation sequence -- Adding, removing, or reordering operations in already-journaled code paths will cause a mismatch.
What is safe
The following changes are safe and will not break determinism:
- Changing log messages -- Console output is journaled separately via
op_consoleentries. The content of log messages is transparent to the replay mechanism. You can change what you log without affecting the operation sequence. - Adding operations after the last journaled entry -- Once the journal is exhausted and execution switches to live mode, any new operations are simply appended. This is how you extend a workflow that was partially complete.
- Wrapping conditional logic in steps -- Steps provide transactional boundaries. Logic inside a step is treated as a single atomic operation from the journal's perspective, giving you flexibility to change internal step logic.
Deterministic Time
On first run, Tide captures Date.now() at startup and freezes it. Every subsequent call to Date.now() or new Date() returns that same frozen value for the entire duration of the workflow. On replay, the frozen timestamp is loaded from storage so that time is identical across runs.
const t1 = Date.now();
await sleep(5000);
const t2 = Date.now();
console.log(t1 === t2); // true -- time is frozenperformance.now() always returns 0.
This ensures your workflow produces identical results regardless of when it is replayed. A workflow that ran at 2:00 PM and is replayed at 3:00 PM will see the exact same timestamps, preventing time-dependent branching from breaking determinism.
Crash Recovery
If a process crashes mid-execution:
- All committed journal entries are safe. They have been persisted to the storage backend and will survive the crash.
- Any uncommitted entries are lost. Operations inside a step that did not complete are buffered in memory and never written to storage.
- On restart, replay picks up from the last committed entry. The workflow re-executes from the top, replaying all committed operations instantly, then resumes live execution.
- The incomplete step re-executes from scratch. Because the failed step's journal entries were never committed, the step body runs again in its entirety.
Example: crash inside a step
Consider a step that performs 3 internal operations. The process crashes after the second one:
Step "process-data":
internal op 1: readFile("/input.json") -- buffered (not committed)
internal op 2: writeFile("/temp.json") -- buffered (not committed)
--- crash ---
internal op 3: writeFile("/output.json") -- never reachedBecause the step never completed, none of its journal entries were committed. On restart:
- Replay completes all operations that were committed before the step.
- The step body re-executes entirely -- all 3 internal operations run live.
- When the step completes successfully, all 3 entries are committed atomically.
This is safe because the failed step's filesystem changes and journal entries were never persisted. The virtual filesystem state is restored from committed entries only, so the re-executed step sees the same state as the original attempt.
See Steps: Crash Recovery for more details on how steps provide transactional guarantees.
Determinism Validation
When replaying, Tide validates each operation against the journal. If the code produces an operation that does not match the journal entry at the current position, Tide throws a determinism violation error:
Determinism violation: expected op 'op_write_file' at position 3, got 'op_read_file'This error means your workflow code has changed in a way that produces a different operation sequence than what was originally recorded. To resolve this:
- Do not modify operation sequences in already-deployed workflows that have active invocations with existing journals.
- Use steps to encapsulate logic that might change, so that changes inside a step do not affect the outer operation sequence.
- Start a new invocation if you need to run modified code from scratch rather than replaying an existing journal.
Related Pages
- Steps -- Transactional boundaries for atomic operations
- Filesystem Introduction -- How file state is persisted and replayed
- Storage Backends -- Where the journal is stored
- Architecture -- Internal implementation details