Skip to main content

Durable Timer v1 operations and configuration

Durable Timer lets a Triggers flow keep an event for a configured relative delay, then resume the flow from the Timer node. It is an environment-wide platform capability: business actions stay unaware of Timer state and receive the same action inputs and results they use in flows without a Timer.

Any released flow can contain one or more Timer nodes, branches, and retry loops. A retry loop returns to a Timer using the existing Timer reference, so the original event snapshot is emitted again when the next due time arrives.

Who owns what

OwnerResponsibilityConfiguration location
Flow authorPlaces Timer nodes and connects normal flow edges. Configures each Timer delay and its retry edge in Studio.Released flow version
Business serviceRuns actions and service-in nodes. Its SDK carries private Timer context only on internal NATS hops.Existing SDK configuration and service code
Notify TriggersReleases flows, dispatches Timer commands, consumes Timer resumes, and continues Timer paths.triggers-v2 deployment
Durable Timer servicePersists occurrence state, schedules wakes, and sends a durable resume to Notify.durable-timer-svc deployment
Platform operatorProvisions the required JetStream resources, enables Notify and Timer for the environment, and monitors capacity and readiness.Helm/environment configuration

Before enabling

  1. Run NATS 2.14 with JetStream enabled.
  2. Provision the streams, KV bucket, and consumers listed in NATS resources and subjects.
  3. Confirm every Timer-lineage service-in node uses NATS and its config.target exactly matches the default target of an active manifest service_sources[] entry. A transport-only match is rejected at release as durable_timer_target_mismatch.
  4. Deploy Notify with Timer enabled and deploy Durable Timer against the same NATS bus.
  5. Test start, retry, completion, retry exhaustion, restart recovery, and an overdue wake before enabling flows for users.

There is one shared trusted plain NATS 2.14 bus. v1 does not use ACLs, TLS, NATS credentials, certificates, message signatures, a Timer-specific NATS connection, or a Timer-specific broker configuration.

Flow author configuration

Configure the Timer node in Studio as part of the released flow. v1 uses a relative delay, not a timestamp and not an external cancellation API.

Flow settingAllowed valueMeaning
delay_ms1000 through 2592000000Relative delay before the Timer emits its resume.
max_retries0 through 100Maximum number of retry occurrences after the initial Timer occurrence. 0 disables retry.
Start inputExactly one edgeThe event that creates a Timer reference and its immutable snapshot.
Retry inputAt most one direct edge from a Condition on the same Timer lineageRe-arms the existing Timer reference only when the selected condition branch points back to it.
Time reached outputExactly one edgeThe path Notify continues after the Timer resumes the flow.
disabledTop-level field on the Durable Timer node, outside configWhen true, this Timer path halts and no wait is scheduled. It does not disable the whole flow; other branches may continue.

The final retry edge must connect a Condition directly to this Timer's Retry input. When release validation traces that Condition backward to this Timer's Time reached lineage, it rejects a Mapping, Source, another Timer, Output, or an HTTP/Kafka service boundary. A NATS service/action path is allowed. Actions still need no Timer-specific arguments, results, or code.

Business-service SDK requirements

The Go and Python SDK advertise durable_timer.v1 only when all three requirements are met:

  • the service has its ordinary NATS connection;
  • the service calls EnableDedup() in Go or enable_dedup() in Python;
  • Redis resolves successfully.

Business services use their existing ordinary NATS connection to receive and publish Timer-lineage traffic. They do not configure a Timer flow list, a Timer URL, a second NATS connection, or a Timer-specific recovery flag.

VariableRequired for a Timer-lineage business serviceDefaultMeaning
TRIGGERS_NATS_URLYesFalls back to NATS_URLExisting SDK NATS connection.
TRIGGERS_REDIS_URL or supported Redis inputsYesUnsetRedis used by SDK deduplication and Timer-scoped result recovery.
TRIGGERS_DEDUP_WINDOWNo30sExisting SDK duplicate window.

Result recovery is automatic for Timer traffic

When a service advertises durable_timer.v1, the SDK automatically enables result recovery for structurally valid private Timer handoffs. This is required so a restarted service can avoid repeating a completed action while its Timer lineage is resumed.

TRIGGERS_RESULT_RECOVERY_ENABLED is not required for Durable Timer. When set to true, it broadens result recovery to ordinary non-Timer deliveries as well, and still requires SDK deduplication plus Redis. TRIGGERS_RESULT_RECOVERY_TTL defaults to 24h; it is the duplicate-suppression horizon for stored action results, never a Timer due time and never a schedule retention setting. Set it to cover the expected redelivery and outage horizon: a redelivery after the stored result expires can execute the external action again.

Notify Triggers configuration

These variables belong only to the triggers-v2 process. Notify uses its existing NATS connection to publish four commands and consume the Timer resume subject.

VariableRequired when Timer is enabledDefaultMeaning
TRIGGERS_DURABLE_TIMER_ENABLEDYesfalseEnables Notify's Timer boundary. Helm sets it from durableTimer.enabled.
TRIGGERS_DURABLE_TIMER_RESUME_STREAMNoDURABLE_TIMER_RESUMESStream that holds Timer resumes.
TRIGGERS_DURABLE_TIMER_RESUME_CONSUMERNoNOTIFY_TRIGGERS_DURABLE_TIMER_RESUMENotify's durable resume consumer.
TRIGGERS_DURABLE_TIMER_RESUME_OUTBOX_LEASENo30sClaim lease for one pending Notify resume outbox record. Must be positive.
TRIGGERS_DURABLE_TIMER_RESUME_RECEIPT_RETENTIONNo24hRetention for an already processed resume receipt. It is measured from processing time, never expires pending work, and must exceed the outbox lease.

Notify accepts Timer commands and resumes for valid released flow versions. It retains its normal manifest, replay, and DLQ behavior.

Durable Timer service configuration

These variables belong only to durable-timer-svc. The service connects to the same NATS bus as Notify.

VariableRequiredDefaultMeaning
DURABLE_TIMER_NATS_URLYesNonenats:// URL for the shared NATS 2.14 bus.
DURABLE_TIMER_HEALTH_ADDRNo:8080HTTP listener for health and readiness.
DURABLE_TIMER_STATE_BUCKETNoDURABLE_TIMER_STATELogical JetStream KV bucket for active occurrences. Its underlying stream is KV_DURABLE_TIMER_STATE.
DURABLE_TIMER_SNAPSHOT_STREAMNoDURABLE_TIMER_SNAPSHOTSImmutable original event snapshots.
DURABLE_TIMER_TERMINAL_STREAMNoDURABLE_TIMER_TERMINALSImmutable terminal receipts.
DURABLE_TIMER_SCHEDULE_STREAMNoDURABLE_TIMER_SCHEDULESNative schedule and wake work.
DURABLE_TIMER_COMMAND_STREAMNoDURABLE_TIMER_COMMANDSStart, retry, complete, and fail commands.
DURABLE_TIMER_COMMAND_CONSUMERNoDURABLE_TIMER_COMMAND_SERVICETimer command consumer.
DURABLE_TIMER_WAKE_CONSUMERNoDURABLE_TIMER_WAKE_SERVICETimer wake consumer.
DURABLE_TIMER_RESUME_STREAMNoDURABLE_TIMER_RESUMESDurable handoff to Notify.
DURABLE_TIMER_RESUME_CONSUMERNoNOTIFY_TRIGGERS_DURABLE_TIMER_RESUMENotify consumer checked by Timer readiness.
DURABLE_TIMER_RUNTIME_FETCH_TIMEOUTNo5sPositive timeout for resolving the released Notify runtime body.
DURABLE_TIMER_RECONCILE_INTERVALNo1mPositive interval to recreate deterministic wakes at startup and while waiting work exists.
DURABLE_TIMER_SHUTDOWN_GRACENo45sPositive grace after readiness withdrawal; unsettled work is NAKed after it expires.

All duration settings above must be positive. The Timer process rejects blank resource names and NATS URLs that are not nats:// URLs at startup.

NATS resources and subjects

Platform provisioning must create these resources before the services become ready.

ResourceSubjectsConsumerPurpose
DURABLE_TIMER_COMMANDSdurable_timer.commands.start.v1, .retry.v1, .complete.v1, .fail.v1DURABLE_TIMER_COMMAND_SERVICETimer command work queue.
DURABLE_TIMER_SNAPSHOTSdurable_timer.snapshot.v1.>NoneImmutable event snapshots.
DURABLE_TIMER_TERMINALSdurable_timer.terminal.v1.>NoneImmutable terminal archive.
DURABLE_TIMER_STATE bucket$KV.DURABLE_TIMER_STATE.> in stream KV_DURABLE_TIMER_STATEKV accessActive occurrence state.
DURABLE_TIMER_SCHEDULESdurable_timer.schedule.v1.>, durable_timer.wake.v1.>DURABLE_TIMER_WAKE_SERVICE filtered to durable_timer.wake.v1.>Native scheduling and wake work.
DURABLE_TIMER_RESUMEStriggers.v2.durable_timer.resume.v1NOTIFY_TRIGGERS_DURABLE_TIMER_RESUMEDurable handoff from Timer to Notify.

Command, wake, and resume consumers use explicit acknowledgement and unlimited delivery. Waiting state, referenced snapshots, scheduled wakes, commands, and resumes have no live expiry. A full store rejects new admission rather than evicting waiting or replayable work.

Health, capacity, and monitoring

GET /health reports that the Timer HTTP process is alive. GET /ready fails closed unless it can reach NATS, the state KV bucket, all five streams, the command and wake consumers, Notify's resume consumer, and the released-runtime resolver. Use /health for startup and liveness probes; use /ready for readiness.

Default logical capacity is 1 GiB for active state and 10 GiB each for snapshots and terminal records. Replica storage overhead is additional. Snapshot and terminal records do not expire in v1. The schedule stream also grows with available storage, so alert before storage headroom is exhausted. NATS max_payload must be at least 2 MiB: the maximum Timer envelope is 1,471,832 bytes and command/resume stream messages are limited to 1.5 MiB.

Monitor readiness, JetStream storage and consumer backlog, NAK/redelivery advisories, rejected admission, and terminal failures. Dependency, transport, and not-ready failures are NAKed for retry. Only typed invalid contracts terminate immediately. Retry exhaustion is persisted as a terminal record with failure_code: "max_retries_exceeded".

Advanced runtime contract

The Timer control context is private, unsigned JSON. SDKs accept it only on internal NATS service-in deliveries together with X-Triggers-Execution-Id and X-Triggers-Durable-Timer-Payload-SHA256; the latter is the lowercase SHA-256 of the exact current-hop bytes. It never reaches actions, action inputs or results, public HTTP, Kafka, or output topics.

The trust boundary is access to the internal NATS subjects. The SDK still rejects malformed, oversized, duplicate, stale, lineage-inconsistent, and current-payload-hash-inconsistent context. Deterministic identifiers, occurrence fences, stored state, and hashes provide replay and stale-command protection, but v1 does not establish publisher identity.

On terminal completion, the service first persists a terminal receipt in active state, then archives the exact state in DURABLE_TIMER_TERMINALS, then deletes the active KV entry. If archival fails, active state remains for replay or reconciliation. After deletion, reads fall back to the immutable terminal archive. The state bucket purges prior KV history and expires only the purge marker after 60 seconds.

Go and Python derive the same Timer identifiers from UTF-8 strings joined by NUL bytes. The identifiers are dtr_v1_ references, dto_v1_ occurrences, dtrr_v1_ resume runs, and dtc_v1_ commands. Cross-language fixtures live in notify_triggers/testdata/durable_timer_v1_golden.json.