Reliable delivery

Webhooks, document triggers, and cron runs are stored before the first delivery attempt.

Docs menu

Failed deliveries are retried with increasing delays and resumed after a restart. When attempts run out, the delivery becomes a dead letter that you can inspect and replay.

Stored deliveries

Each delivery has an id consisting of dlv_ and 24 hex digits. The digits encode a millisecond timestamp and sequence, so ids sort by time. Each delivery is one record in the app's __deliveries tree in logs.db. It contains a fixed 64-byte header with kind, state, attempt, target, times, and last HTTP status, plus the event, collection, record id, last error, and payload. States are pending (waiting for its next attempt), delivered and dead. After a restart, pending deliveries are tried again in creation order, so a record's trigger runs retain their order.

A delivered record is kept for delivery.retain_days (default 7; 0 keeps none) so it can be replayed; a sweep removes older ones each hour. A dead record is kept until it is replayed.

Retry

After a failed attempt, the next attempt is delayed by base * 2^(attempt - 1) milliseconds, up to the cap. Retries continue up to the attempt count. These are app settings set with settings.set; values are whole numbers, and attempt counts are at least 1:

Setting Default Meaning
webhooks.max_attempts 5 attempts in all, the first included
webhooks.backoff_ms 1000 wait after the first failure
triggers.max_attempts 5 the same, for trigger runs
triggers.backoff_ms 1000
cron.max_attempts 3 the same, for scheduled runs (see schedules)
cron.backoff_ms 1000
delivery.backoff_cap_ms 3600000 the longest wait, for all three
delivery.retain_days 7 days a delivered record stays replayable

A run fails when the function throws, traps, hits a limit, or returns a status of 400 or more, in JavaScript and in Rust. A direct call (a route, /fn/<name>, function.invoke) returns its status and is not retried.

A failed trigger run delays later runs for the same record until it succeeds or becomes dead. A run refused because the executor is stopping does not count as an attempt. It is retained and tried again after the first delay.

Webhooks

webhook.list (GET /v1/_webhooks, MCP substrio_webhook_list) pages the app's webhooks oldest first with limit and cursor, and returns next_cursor.

curl -s "$SUBSTRIO_URL/v1/_webhooks?limit=20" -H "Authorization: Bearer $SUBSTRIO_KEY"

Every request carries <Brand>-Delivery-Id: dlv_..., the same id on each attempt and on a replay, so a receiver can drop a duplicate. <Brand> is the webhooks.brand setting, as for the <Brand>-Signature header.

webhook.replay (POST /v1/_webhooks/deliveries/{id}/replay, MCP substrio_webhook_replay) sends a delivered or dead delivery again under its same id and returns immediately. The replay is a new attempt chain with the full attempt count. A pending delivery is refused (it is already going out); an unknown id is 404; a delivery whose webhook was removed is refused.

Dead letters

A delivery out of attempts is one document in the app's _dead_letters collection: delivery_id, kind (webhook, trigger or cron), target (the webhook, trigger or schedule id), destination (the URL or the function name), attempts, last_error, last_status, collection, record_id, payload and failed_at. If the document cannot be written the delivery stays dead in the store, unmirrored, and is written at the next start.