Tide

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:

OperationJournal OpDescription
readFileop_read_fileRead a file from the virtual filesystem
writeFileop_write_fileWrite a file to the virtual filesystem
removeFileop_remove_fileRemove a file from the virtual filesystem
listFilesop_list_filesList files in the virtual filesystem
sleepop_set_timeoutSleep for a duration
Step beginop_step_beginEnter a transactional step boundary
Step completeop_step_completeComplete a transactional step
console.log / console.errorop_consoleLog 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:

  1. Your JavaScript code re-executes from the top of the workflow function.
  2. Each operation checks the journal instead of executing live. The stored result is returned directly.
  3. readFile returns the stored result from the journal. writeFile replays the stored result but still applies the write to the in-memory virtual filesystem (so subsequent reads see the correct state). sleep returns instantly.
  4. Console output is replayed from the journal. Live console.log and console.error calls during replay are suppressed to avoid duplicate output.
  5. 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, journaled

Operations 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 readFile now happens before a writeFile when 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_console entries. 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 frozen

performance.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 reached

Because the step never completed, none of its journal entries were committed. On restart:

  1. Replay completes all operations that were committed before the step.
  2. The step body re-executes entirely -- all 3 internal operations run live.
  3. 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:

  1. Do not modify operation sequences in already-deployed workflows that have active invocations with existing journals.
  2. Use steps to encapsulate logic that might change, so that changes inside a step do not affect the outer operation sequence.
  3. Start a new invocation if you need to run modified code from scratch rather than replaying an existing journal.

On this page