Configuration

Runtime tuning

The runtime reads these variables where workflows execute. Set them on the deployment or dev server.

Client polling

WORKFLOW_RETURN_VALUE_POLL_INTERVAL_MS

  • Default: 1000
  • Minimum: 1
  • Delay between status requests made by Run.returnValue while a workflow run is not yet complete.
  • Increase it to reduce polling traffic at the cost of noticing completion later.
  • This variable is read by the process awaiting Run.returnValue, such as an E2E test runner, rather than by the workflow deployment.
  • Applies whenever the World cannot hold a read open. When it can, the wait below replaces most of this polling and the interval only paces retries.

WORKFLOW_RETURN_VALUE_WAIT_MS

  • Default: 50000
  • Minimum: 1
  • How long a single wait for a terminal run status may block, when the World supports waiting (world.runs.waitForTerminalStatus()). Run.returnValue re-issues the wait until the run finishes, so this bounds one request rather than the total wait.
  • 50000 is the largest value the Vercel World can actually request: it clamps the ask to its 60s per-request HTTP timeout minus 10s of headroom, so the budget always expires as a response carrying the run's current status rather than as a client-side timeout. Raising this alone has no effect; raise WORKFLOW_REQUEST_TIMEOUT_MS as well.
  • Read by the process awaiting Run.returnValue, like the interval above.

WORKFLOW_RETURN_VALUE_MAX_LONG_POLLS

  • Default: 10
  • Minimum: 1
  • How many consecutive waits Run.returnValue issues before falling back to interval polling for the rest of the await.
  • It never ends the await. A run that outlives the cap continues waiting on WORKFLOW_RETURN_VALUE_POLL_INTERVAL_MS instead.
  • Runs longer than roughly WORKFLOW_RETURN_VALUE_MAX_LONG_POLLS x WORKFLOW_RETURN_VALUE_WAIT_MS therefore spend most of their life interval polling, which costs more requests than continuing to wait would. Raise it if you routinely await long runs.
  • Read by the process awaiting Run.returnValue, like the interval above.

WORKFLOW_RETURN_VALUE_LONG_POLL

  • Default: enabled
  • Set to 0 or false to stop Run.returnValue from waiting and restore fixed-interval polling on WORKFLOW_RETURN_VALUE_POLL_INTERVAL_MS exactly.
  • The kill switch for the wait. Worlds that do not implement waiting are already on the interval path, so this changes nothing for them.
  • Read by the process awaiting Run.returnValue, like the interval above.

Replay and queue delivery

WORKFLOW_REPLAY_TIMEOUT_MS

  • Default: 240000
  • Clamp: 30000 to 780000
  • Charged time budget for replay and orchestration work in one handler invocation.
  • Covers loading events, re-running the workflow function, resolving suspensions, and scheduling follow-up work.
  • Does not include time spent inside inline "use step" bodies. Steps are bounded by the platform function duration instead.

For example, a workflow can run a 10-minute inline step even with WORKFLOW_REPLAY_TIMEOUT_MS=300000, because the replay budget is paused while the step body runs.

WORKFLOW_REPLAY_TIMEOUT_MAX_RETRIES

  • Default: 3
  • Queue deliveries that may hit the replay timeout before the run is failed with REPLAY_TIMEOUT.

WORKFLOW_MAX_QUEUE_DELIVERIES

  • Default: 48
  • Delivery attempts before a run or step is failed gracefully.
  • Can only be lowered. Workflow calibrates the default to record failure before the queue expires the message.

WORKFLOW_MAX_EVENTS

  • Default: 25000
  • Positive-integer event limit reported by the Local World and enforced by the runtime as MAX_EVENTS_EXCEEDED.
  • The Local and Postgres Worlds also use it as the maximum number of events returned when events.list() is called without a limit. If more events exist, the response includes hasMore: true and a continuation cursor.
  • The Vercel World receives its event limit from the service; this environment variable does not override that service-owned value. See Vercel World limits.
  • Runs started on the QuickJS engine with snapshotting enabled (see WORKFLOW_SNAPSHOT_THRESHOLD) are exempt from this limit on the Local and Vercel Worlds. See max-events-exceeded.
  • Invalid or non-positive values fall back to the default.

WORKFLOW_REPLAY_DIVERGENCE_MAX_RETRIES

  • Default: 3
  • Recovery replays before replay divergence is recorded as corruption.

WORKFLOW_DEPLOYMENT_MISMATCH_MAX_RETRIES

  • Default: 3
  • Times a delivery that reached a deployment other than the one its run is pinned to is re-routed to that deployment before the run is failed with DEPLOYMENT_MISMATCH.
  • Re-routed deliveries back off exponentially (1s, 2s, 4s). Set to 0 to fail the run on the first misrouted delivery.
  • Only applies to Worlds with atomic, immutable deployments (the Vercel World). A run whose pinned deployment cannot be reached at all fails immediately regardless of this value.
  • Transient or unknown queue publishing failures use normal queue redelivery and do not consume this budget.

WORKFLOW_OPEN_WAIT_CLOCK_SKEW_MS

  • Default: 30000 (30 seconds)
  • A pending wait (a sleep() whose timer has not fired, including one that lost a Promise.race() against a hook) keeps the per-step event-log delta optimization off only if it can fire while the current invocation is still running steps inline: its target time must fall before the end of the invocation's inline window (WORKFLOW_V2_TIMEOUT_MS, which is otherwise derived from the platform's function deadline and tops out at 10 minutes) plus this allowance for clock skew between the runtime and the wait timer. A wait due later than that does not cost the extra events.list() per step. A wait_created whose target time cannot be read is treated as due now.
  • Before an invocation parks on a wait over a log it last extended from a step's inline delta, it re-reads the log once, so a wait completed early through run.wakeUp() (the dashboard's "cancel sleeps" action) is acted on then rather than when the wait's own timer would have fired.
  • Turbo's forced optimistic start is unaffected: any open wait keeps it off, whatever the target time.
  • Must be a finite integer. 31536000000 (one year) restores the older behavior of gating on any pending wait; Infinity is rejected and falls back to the default. Open hooks are unaffected: they gate regardless of this setting.

Stale reads, and why nothing has to be rejected

  • Not a variable: this is how a replay working from an out-of-date event log stays correct, and why no World needs a precondition guard to make it so.
  • Three properties work together. A reader's log is always a prefix of the run's log, never a prefix with a hole in it. The World allocates positions at commit, so nothing lands behind a position a reader has already passed. Replay is deterministic on a prefix: the same prefix always yields the same decisions, so a shorter log does not mean a different run, only a run that has not caught up. Every write reports what it missed: a creation names the position it replayed from (eventCount), and the World returns the events occupying the positions it was pushed past. The replay merges those and continues, correcting itself on the write rather than on a read.
  • So a stale replay costs a merge, not a rejection. None of the shipped Worlds refuses a write for being stale.
  • A World may refuse instead, with 412 (PreconditionFailedError). This is appropriate when it allocates positions somewhere other than the commit and cannot report a gap reliably. The runtime restarts the replay in the same invocation from a corrected event log and falls back to a re-invocation with a fresh replay after spending the restart budget. It never retries the rejected write as-is because a replay working from a corrected log derives different events.
  • A World that does refuse should only ever do so on evidence, and accept the write in every other case. A rejection then always means the position was stale, while the absence of one proves nothing about currency.
  • Two runtime behaviors follow from the properties above rather than from any fence. The per-step event-log delta optimization (consuming the delta returned by a step's terminal write instead of issuing an extra events.list per step) stays active while the run has an open hook. A hook_received event missed by the delta window is observed one iteration later, and the next write brings it back. While a hook is open, inline steps take the await-then-run path even when optimistic inline start is enabled. Several invocations race for one step's claim there, and awaiting it means the body runs only for the writer that won.

WORKFLOW_SLOT_GAP_CHECK

  • Default: enabled
  • A replay checks that the event log it loaded is dense before it runs, and fails the run with CORRUPTED_EVENT_LOG when a position below the log's highest holds no event. A log missing only its first position, meaning a run whose run_created is still being written, is left alone.
  • A position can be briefly empty while the write that occupies it is still committing, so the check re-reads the log a few times before it decides, and the replay continues from whichever log it settled on.
  • The check trades one failure for another. Most holes stand for an event that never happened, and replaying past those is correct. A hole standing for an event that did happen looks identical, and replaying past that one produces a run whose result is silently wrong. Failing is the recoverable side of that trade.
  • Set 0 to replay across holes instead.

WORKFLOW_SEALED_LOG

  • Default: enabled
  • New runs are created at the sealed-log spec version, in which the World's backend assigns each event its position before the write commits rather than letting concurrent writers race for one. Concurrent writes then never contend for a position, which is what makes a wide fan-out cheap.
  • The price of assigning positions in advance is that a writer which claims one and then dies leaves a position no writer will ever fill. The backend closes such a position by writing a noop event into it once it can prove the position was abandoned, so a reader still sees the dense log it needs. Replay steps over a noop without delivering it to the workflow or advancing the deterministic clock. Its timestamp belongs to whichever reader sealed it, not to the run.
  • Set 0 to put a deployment back on the previous scheme, where each position is allocated by the write that occupies it. Use this as the kill switch if position assignment turns out to be at fault for event-log problems.
  • Setting 0 also stamps new runs below spec version 8, so another run can't take their hook tokens over with experimental_force. A forced hook in another run gets HookConflictError instead. Runs created with 0 can still take tokens over from others.
  • Existing runs are unaffected either way. A run's spec version is stamped once, at creation, and read from the run for the rest of its life, so flipping this changes only what new runs get, and a run in flight keeps the scheme it started on. Every build reads sealed logs regardless of the setting.
  • A run created at the sealed-log version can only be replayed by a reader that knows to skip noop events. That includes every runtime on this release train, but a runtime that pins its own accepted spec range separately, such as the Python runtime, has to catch up before it can read these runs. Switch this off in an environment where it has not.
  • Only the Vercel World seals. The Local and Postgres Worlds allocate each position at the commit that occupies it, so they cannot leave a hole and never write a noop; the setting still moves the version they stamp, so the fleet stays on one spec.

WORKFLOW_PRECONDITION_MAX_INPROCESS_RESTARTS

  • Default: 3
  • How many times a single invocation restarts its replay in-process after an event creation is rejected as stale before it falls back to a re-invocation. No shipped World rejects one, so this budget is reserved for a World that chooses to.
  • A restart reloads the event log and rebuilds the workflow from scratch, so it costs a replay but no queue round trip. A World may attach the missing events to its rejection, in which case the first restart needs no event-log request at all.

WORKFLOW_PRECONDITION_MAX_REINVOCATIONS

  • Default: 5
  • How many re-invocations one run may spend on rejected event creations before it is failed with RUNTIME_ERROR.
  • Counted on the run's queue messages rather than per invocation, because a re-invocation enqueues a fresh message and so restarts the ordinary delivery count. Without this budget a run whose event log keeps growing faster than it can be read would cycle indefinitely.

WORKFLOW_PRECONDITION_REINVOKE_DELAY_SECONDS

  • Default: 2
  • Delay before a re-invocation caused by a rejected event creation.
  • Unlike an in-process restart, which re-reads immediately, a re-invocation only happens after the in-process budget fails to catch up. The delay gives the other writers time to quiesce.

WORKFLOW_LOG_ORDER_DRAWS

  • Default: enabled
  • Experimental. Pins correlation-ID draw order to event-log order. A branch-deciding delivery (a step result, hook payload, or wait completion) resolves to the workflow only after every earlier-in-log delivery's continuation has fully quiesced, and never ahead of a lower-slot delivery that is committed to happening.
  • Without it, a delivery that resolves while an earlier delivery's continuation is still a few microtask hops from its next step/hook/wait call can overtake it on the run's shared correlation-ID sequence. Draw order, and therefore correlation IDs, then depends on how much of the event log a replay had loaded. Two concurrent replays holding different-length prefixes can bind one ID to two different entities, failing the run with CORRUPTED_EVENT_LOG.
  • Costs one event-loop turn (roughly 15 to 20 microseconds via setImmediate) per branch-deciding delivery during replay, and more when continuations genuinely overlap. Measurements on a 100-step sequential replay show about 2 ms added end to end.
  • Only applies to the default Node.js VM engine. WORKFLOW_VM=quickjs has its own event feed and correlation-ID sequence and is unaffected by this setting.
  • Correlation IDs of runs created before the setting changed are not affected on platforms where a run keeps replaying on the deployment it started on. Elsewhere, only change it while no runs are in flight.
  • Set 0 to opt back into arrival-order delivery resolution. Only the literal value 0 opts out; false or off leave it enabled.

Inline execution

WORKFLOW_V2_TIMEOUT_MS

  • Default: derived from the runtime deadline. The value is 600000 when the invocation has 25 minutes or more left, 300000 when it has 10 minutes or more, and 120000 otherwise.
  • Wall-clock guard for the inline replay loop.
  • Once elapsed, the handler requeues the workflow instead of continuing to run more inline work in the same invocation.
  • The default comes from World.getRuntimeDeadline(), so raising a function's maxDuration widens the inline budget without configuration. Worlds that do not report a deadline get the flat 120000. Setting this variable to a finite positive number overrides the tiering entirely.

WORKFLOW_MAX_INLINE_STEPS

  • Default: 3
  • Clamp: 1 to 16
  • Number of newly-created steps one invocation runs inline in parallel before queueing the rest.

WORKFLOW_TURBO

  • Default: enabled
  • Fast path for a run's first delivery.
  • Stops forcing optimistic inline step start once the run creates a hook or wait.
  • Set 0 or false to disable.

WORKFLOW_OPTIMISTIC_INLINE_START

  • Default: disabled
  • Starts inline step bodies before their step_started event is confirmed.
  • Use only when step side effects are idempotent.
  • Set 0 or false to force it off, including the first-delivery fast path used by WORKFLOW_TURBO.

WORKFLOW_RETAINED_VM

  • Default: enabled
  • Node.js VM engine only. Keeps the suspended workflow VM alive across inline steps within one invocation, so each iteration of the inline loop appends only the newly written events instead of replaying the whole event log in a fresh VM. QuickJS manages its own retained inline loop independently of this setting.
  • A step- or attribute-driven suspension can keep the VM retained even when hooks or waits are open or created at the same boundary. Hook- or wait-only suspensions park the invocation because nothing in the current delivery can advance them, with one exception: when the hook's own create is what the workflow is waiting on (a hook.getConflict() awaiter, or a create whose token is already claimed), the invocation resumes the retained VM over the committed hook_created or hook_conflict instead of re-invoking through the queue. Any replay divergence falls back to a full replay.
  • Step inputs made of plain data (objects, arrays, primitives) and standard built-ins (Map, Set, Date, RegExp, typed arrays, ArrayBuffer, URL, Headers) keep the VM retained. Patching or polyfilling built-in prototypes doesn't change that because serialization never calls them. A boundary falls back to a full replay only when serializing its arguments runs code the workflow controls, such as a getter, a proxy, or a custom class serializer, or computes an Error's stack trace.
  • Set 0 or false to replay the Node.js workflow from scratch in a fresh VM on every iteration.

WORKFLOW_INLINE_OWNERSHIP

  • Default: enabled
  • Records which queue message owns each inline step execution, so a wake (hook resume, elapsed wait) that replays the run mid-step schedules a delayed backstop instead of immediately redispatching and reexecuting the step. See Inline step message ownership.
  • Set 0 or false to revert to the previous unconditional immediate re-dispatch.

WORKFLOW_INLINE_OWNERSHIP_LEASE_SECONDS

  • Default: 860
  • Clamp: 1 to 900
  • How long after an inline step's latest step_started other invocations assume its owner may still be executing the body. Within the lease they defer the step's backstop message; past it they enqueue immediately.
  • Raise this on self-hosted multi-instance deployments whose inline steps run longer than the default (the default is sized for Vercel's function duration ceiling).

Workflow VM engine

WORKFLOW_VM

  • Default: node
  • Values: node or quickjs
  • Selects the sandboxed VM engine that executes workflow functions ("use workflow"). Step functions are unaffected and always run with full Node.js access.
  • node (default) runs workflow code in a node:vm context.
  • quickjs (experimental) runs workflow code in a QuickJS VM compiled to WebAssembly (via quickjs-wasi). Both engines implement the same event-replay execution model (seeded PRNG, deterministic clock, and correlation-ID sequences are identical), but the global surface is not identical. Review the differences below before switching an existing deployment. The QuickJS engine is intended for platforms that do not implement node:vm, and supports VM-memory snapshotting (see WORKFLOW_SNAPSHOT_THRESHOLD).
  • Global-surface differences under quickjs apply to workflow functions only. Step functions always have full Node.js:
    • crypto.getRandomValues() and crypto.randomUUID() are provided and deterministic (seeded like the node engine's). All crypto.subtle.* methods, including digest, throw with guidance to move to a step function. The node engine supports digest.
    • Intl is not available (QuickJS has no ICU). The Intl.* constructors throw, and toLocaleString-family methods (including localeCompare) throw when called with an explicit locale. Calling them without arguments keeps the engine default. Perform locale-sensitive formatting in a step function.
    • WebAssembly and Atomics are not available.
    • process exposes only a frozen copy of env, matching the node engine.
  • The engine choice is stamped into the run's executionContext when the run starts, so a run keeps executing on the engine it started on even if the deployment's WORKFLOW_VM changes. Runs without a stamped engine use the handler's WORKFLOW_VM value.
  • Unknown values throw at startup.

WORKFLOW_QUICKJS_BASELINE_SNAPSHOT

  • Default: enabled
  • Only read when WORKFLOW_VM=quickjs.
  • Evaluates the workflow bundle once per function instance, snapshots the resulting VM, and restores that snapshot at the start of every invocation instead of re-evaluating the bundle. This is the dominant share of QuickJS VM startup: roughly 77 ms to 3 ms to first suspension for a 1.3 MB bundle.
  • A bundle whose module scope consumes randomness, reads the clock, or replaces a serialization intrinsic cannot be snapshotted safely. The runtime detects these cases when preparing the snapshot and falls back to per-invocation evaluation.
  • Set 0 or false to always evaluate the bundle per invocation.

WORKFLOW_SNAPSHOT_THRESHOLD

  • Default: 1000 when the QuickJS engine is selected
  • Values: non-negative integer. 0 disables snapshotting.
  • Experimental. Only used by the QuickJS engine (WORKFLOW_VM=quickjs), and only with a World that provides snapshot storage (world-local, world-postgres, and world-vercel do).
  • When above 0, the runtime persists a VM-memory snapshot at a suspension once at least this many events have been processed since the last snapshot. Subsequent invocations restore the VM from the snapshot and replay only the events recorded since, instead of re-executing the workflow from the top against the full event log.
  • Short-lived runs below the threshold never pay the snapshot cost; long-running or unbounded runs stop scaling their resume cost with total event-log length. 1 snapshots at every qualifying suspension.
  • Choosing a value: a restore costs roughly the same regardless of log length (tens of milliseconds to decompress and restore the heap, plus fetching the snapshot), while a full replay grows with the log (tens of milliseconds at a few hundred events, seconds at several thousand). Restoring starts to pay off at a few hundred events, so values in the hundreds suit most long-running workflows. Very low values mostly add snapshot saves to runs that replay quickly anyway.
  • Snapshots are an optimization, not a source of truth: the event log remains authoritative, and a missing, corrupt, oversized, or incompatible snapshot automatically falls back to a full replay. Snapshots over 32 MB uncompressed are not saved.
  • Set 0 to opt out and always replay the full event log.
  • Like WORKFLOW_VM, the policy is stamped into the run's executionContext at start, so a run keeps the snapshot policy it started with. Runs started with WORKFLOW_VM=quickjs are stamped with this value, or the default when unset. Runs without a stamped policy use the handler's value, or the default. start() rejects an invalid value. An invalid value on the workflow handler disables snapshotting with a warning.
  • Runs stamped with workflowVm: "quickjs" and a positive threshold are exempt from the default per-run event limit on the Local and Vercel Worlds.
  • A snapshot can only be restored by the exact QuickJS engine build that captured it. On Vercel, runs keep executing on the deployment they started on, so deploys don't affect in-flight snapshots. If your deployment serves in-flight runs from new code (for example, a self-hosted rolling update), an upgrade that changes the embedded engine makes every in-flight run fall back to a full replay at its next resume, all at once, before re-snapshotting at its next qualifying suspension. Plan for that one-time replay load when upgrading.

What a snapshot contains, and how it is protected:

  • A snapshot is the workflow VM's memory: the workflow's code and in-memory state, including step results and hook payloads it holds, and the copy of process.env the VM exposes. It is executable state, so write access to snapshot storage is equivalent to running code in the workflow VM.
  • Snapshots are compressed and then encrypted with the run's encryption key. The encryption also authenticates the snapshot: on restore, the runtime rejects any snapshot that isn't encrypted with the run's key, or whose stored metadata doesn't match the metadata sealed inside it.
  • By default, runs without an encryption key are never snapshotted. Only world-vercel provides run encryption keys. On world-local and world-postgres, set WORKFLOW_SNAPSHOT_ALLOW_UNENCRYPTED=1 on the workflow handler to snapshot anyway. Those snapshots are stored in plaintext (in .workflow-data/snapshots/ or the workflow_snapshots table) and are not authenticated, so only enable this where snapshot storage is as trusted as your deployment.
  • A restored run sees process.env as of the current invocation, the same as a full replay.
  • Snapshots are deleted after the run reaches a terminal state. world-local and world-postgres have no separate retention for them, so a snapshot of a run that ends without any invocation observing it (for example, one cancelled externally) stays until you remove it.

WORKFLOW_SNAPSHOT_ALLOW_UNENCRYPTED

  • Default: disabled
  • Set 1 or true on the workflow handler to let the QuickJS engine persist VM snapshots for runs without an encryption key. See WORKFLOW_SNAPSHOT_THRESHOLD for what that stores.

Dynamic workflows

WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS

  • Default: disabled
  • Set 1 or true (case-insensitive) to let this deployment start dynamic workflows, execute their stored code on delivery, and advertise dynamic support in its health check. Any other value leaves it disabled.
  • Dynamic source runs with the full privileges of the deployment's functions. Enable it only on deployments whose dynamic source you trust as much as your own code.
  • When disabled, start() with source throws before writing anything, and a delivered dynamic run fails instead of executing.

Compression and tracing

WORKFLOW_DISABLE_COMPRESSION

  • Default: compression enabled
  • Set 1 to disable compression when writing payloads.
  • Reads still decompress existing payloads.
  • To skip compression only for one run's arguments, pass compressArguments: false to start() instead.

WORKFLOW_COMPRESSION_CODEC

  • Default: automatic
  • Forces the write-side codec to zstd or gzip.
  • Invalid values are ignored. Automatic mode prefers zstd when the current Node.js runtime supports it, otherwise it falls back to gzip.
  • Writes for a run on another deployment (for example resumeHook() reaching a run that started before the project moved to a newer Node.js version) use zstd only when that run's deployment runs Node.js 22.15+ (or 23.8+), and gzip otherwise, since older Node.js versions cannot decode zstd. Runs created before @workflow/core recorded the Node.js version always receive gzip. Setting this to zstd does not override that check.

WORKFLOW_TRACE_MODE

  • Default: linked
  • OpenTelemetry span topology for runs.
  • Accepts linked or continuous.

DEBUG

  • Default: unset
  • Debug log filter with wildcards and negation.
  • Examples: workflow:*, workflow:*,-workflow:telemetry:*.

Transport

WORKFLOW_H2_MULTIPLEX

  • Default: enabled
  • On the Vercel World, lets concurrent event-log requests share one HTTP/2 connection instead of one connection per in-flight request.
  • Set 0 to take the event-log requests off HTTP/2 entirely, back to one request per HTTP/1.1 connection. Use this as the kill switch if HTTP/2 turns out to be at fault for event delivery problems.
  • Has no effect when WORKFLOW_NODE_HTTP is enabled, which takes the whole HTTP/2 path away.

WORKFLOW_NODE_HTTP

  • Default: disabled
  • Makes the Vercel and Local Worlds issue their HTTP requests through Node's built-in node:http and node:https modules, instead of the HTTP client library those Worlds normally use.
  • Set 1 to switch to Node's modules. Read the trade-offs below first: they cost throughput on every deployment, which is why this is opt-in.
  • Use it when that library is not an option: a bundler that mangles it, a runtime that does not ship a working copy of it, or a transport-level fault you want to rule out. It is not reached by way of fetch() either, so a runtime whose fetch() is built on the same library is still covered.

Node's own modules do less than the client they replace, so enabling this drops the per-call-site tuning the Worlds configure:

  • Event-log requests lose HTTP/2, so concurrent reads and writes no longer share one connection, and the enlarged HTTP/2 receive windows no longer apply. This is the largest difference, and it slows down replays that read a big event log. It does not apply to event writes on the WebSocket events transport, which is the default and takes neither transport.
  • Requests lose their transport-level retry. Failures still surface to the layers above, which retry event writes and redeliver queue messages, so nothing is silently dropped, but a failure that a same-connection retry would have hidden now costs a full redelivery.
  • Stream close loses its retry of retriable server errors. A transient failure at close can leave a stream marked closing until the run expires, where it would previously have resolved on the retry.

Connection pooling, keep-alive, and the request, header, and body deadlines are preserved. Node's agents configure pooling and keep-alive, and each request receives the deadlines from the Local World's two queue timeouts or the same defaults that the Vercel World's HTTP client currently applies. Queue sends are the partial exception: that client takes no transport override, so it cannot move to Node's modules. It does honor this variable, by dispatching through the runtime's own copy of the library rather than the copy the World bundles, which is the distinction that matters when the bundled copy is the thing that does not work.

A dispatcher passed to createVercelWorld() still wins over this variable. The variable chooses which transport the World builds when you have not supplied one.

WORKFLOW_VERCEL_QUEUE_TIMEOUT_MS

  • Default: 30000
  • Total deadline for one request the Vercel World's queue client makes, measured from the moment it is handed to the transport, so it also covers time the request spends waiting for a free connection.
  • Clamped to [5000, 120000].
  • This is the only request path the World cannot bound with WORKFLOW_REQUEST_TIMEOUT_MS, because the queue client makes its own calls and accepts no override for them. Without a deadline here, a queue acknowledgement that never completes holds the invocation until the platform kills it, and a killed invocation never acknowledges, so the message is redelivered.
  • Keep it below the queue's visibility-renewal interval (60 seconds) so a failed call has room to surface and be retried before the message lease lapses.

WORKFLOW_VERCEL_QUEUE_CONNECTIONS

  • Default: 64
  • Connections the Vercel World's queue client may open to the queue service.
  • Clamped to [1, 1024].
  • Sized much higher than the World's other connection pools on purpose. The queue client makes roughly two small requests per invocation, so its concurrency tracks how many invocations a compute instance is serving at once rather than any per-request fan-out. Lower it only if you have a reason to cap sockets; too low turns invocation concurrency into the limit on how fast messages can be acknowledged.

Queue namespace

WORKFLOW_QUEUE_NAMESPACE

  • Default: none
  • Queue topic namespace shared by build output and Worlds.
  • Must match ^[a-z][a-z0-9]*$.
  • Set it consistently at build and runtime.

Streams and waits

These variables are primarily for tests, debugging, or unusual deployments.

WORKFLOW_STREAM_FLUSH_INTERVAL_MS

  • Default: 0 (dispatch the first chunk of an idle stream immediately)
  • Group-commit window for the leading chunk of an idle stream. 0 sends it at once; a positive value holds it up to that many milliseconds to collect a group. This opt-in setting trades first-chunk latency for larger batches and can benefit slow-but-steady producers. Chunks arriving while a request is already in flight always coalesce into the next group regardless of this setting.
  • Also available as streamFlushIntervalMs on Worlds that expose it (the env var, when set, takes precedence over the World option).

WORKFLOW_STEP_STREAM_DRAIN_TIMEOUT_MS

  • Default: 30000 (30 seconds)
  • Maximum time a step waits for writes queued by a released workflow stream writer to be acknowledged by the Workflow server before recording step_completed. A timeout fails the step instead of exposing a completion while released-writer data remains client-side. A writer intentionally kept locked does not trigger this durability wait.

WORKFLOW_STREAM_MAX_INFLIGHT_CHUNKS

  • Default: 1000
  • Flow-control bound: how many stream chunks may be read-but-not-yet-durably-written while writes are coalesced. Once this many are outstanding, the producer pauses reading until a batch is written. Bounds in-memory buffering; distinct from the per-request batch caps below.

WORKFLOW_STREAM_MAX_CHUNKS_PER_BATCH

  • Default: 1000
  • Wire limit: maximum number of chunks sent in a single coalesced multi-write. Larger bursts are split across requests so one request can't exceed the server's per-multi-write chunk cap.

WORKFLOW_STREAM_MAX_BYTES_PER_BATCH

  • Default: 1048576 (1 MiB)
  • Wire limit: maximum cumulative bytes in a single coalesced multi-write, so large chunks don't produce a request body that platform limits reject. A single chunk larger than this is still sent on its own.

WORKFLOW_STREAM_MAX_BUFFERED_BYTES

  • Default: 8388608 (8 MiB)
  • Flow-control bound: maximum cumulative bytes accepted into the stream writer's group-commit buffer before write() applies backpressure. The byte-denominated counterpart of WORKFLOW_STREAM_MAX_INFLIGHT_CHUNKS; distinct from WORKFLOW_STREAM_MAX_BYTES_PER_BATCH, which only splits how much goes out in one request.

WORKFLOW_FRAMED_STREAM_MAX_RECONNECTS

  • Default: 50
  • Consecutive reconnect cap for framed stream readers.

WORKFLOW_FRAMED_STREAM_MAX_TOTAL_RECONNECTS

  • Default: 1000
  • Total reconnect cap per stream session.

WORKFLOW_WAIT_CONTINUATION_MAX_DELAY_SECONDS

  • Default: 82800 (23 hours)
  • Longest single queue delay used for sleep() continuations.
  • If a wait is longer than this, Workflow queues one continuation for the maximum delay, then queues another continuation after that message fires, repeating until the wait's target time is reached.

WORKFLOW_NEAR_ELAPSED_WAIT_THRESHOLD_SECONDS

  • Default: 2
  • Clock-skew tolerance for wait continuations that arrive near their target time.

WORKFLOW_DEFERRED_CHECK_DELAY_MS

  • Default: 100
  • Delay before the unconsumed-event check fires.
  • Minimum: 10.

WORKFLOW_LOCK_POLL_INTERVAL_MS

  • Default: 10
  • Poll interval for detecting stream lock release.

Limits

WORKFLOW_MAX_EVENTS_OVERRIDE

  • Default: unset
  • Lowers the per-run event ceiling supplied by the World. A run whose event log reaches the ceiling fails with MAX_EVENTS_EXCEEDED, which stops a runaway loop from growing its log without bound.
  • Clamp-down only: it never raises the World's limit, and it applies even when the World supplies none. With no World limit and no override, nothing is enforced.
  • The Local and Vercel Worlds both supply a limit; the Local World defaults to 25,000 and is configurable with WORKFLOW_MAX_EVENTS. The Vercel World's ceiling is documented under Workflow run limits.
  • QuickJS runs with snapshotting are exempt from the World's default limit, but this override still applies to them.