Retries & timeouts

A trigger fires as an execution. If the execution fails, a retry policy decides whether another attempt is scheduled. When no further attempt will be made, the execution is marked dead (isDeadLetter: true) and the trigger is done.

Which retry policy applies

Three sources, most specific first. The first one that says something wins; it replaces the others rather than merging with them.

PrecedenceSourceSet with
1Per-trigger policyttr-retry-*header headers on /schedule, or a retry object on /declare and /bulk items
2Tag policies/tag-policies (or the dashboard). When a trigger carries several tags with a retry strategy, the most defensive values are merged — fewest attempts, longest delay, most restrictive retryOn.
3Project defaultPUT /project/retry-policy (or the dashboard's Tag policies page). Applies to every trigger whose tags express no retry opinion: triggers with no tags, tags with no policy, and policies whose strategy is none.

Untagged triggers used to be never retried. Before September 2026 a trigger without a tag policy had no retry policy at all, so a single transient network error — a DNS hiccup, a 10-second connect timeout — was final. Every project now has a default policy (below) that covers those triggers. Set the project default's strategy to none to restore the old behaviour.

The default policy

Every project starts with:

FieldValueMeaning
retryStrategyexponentialdelays double after each failure
retryMaxAttempts3the first attempt plus two retries
retryInitialDelaySeconds30attempt 2 after 30 s, attempt 3 after a further 60 s
retryMaxDelaySecondsnullno cap
retryJitterfalse
retryOnnetwork_onlyonly when no response was received

network_only is deliberate: it retries the failures where nothing reached your endpoint (DNS, connect timeout, TLS, connection refused) or where the outcome is unknown (connection reset, request timeout), and leaves a 4xx/5xx your endpoint actually produced alone. Widen it to 5xx_and_network or non_2xx if your endpoint is idempotent.

Read or change it with an API key or a dashboard session:

GET https://api.timetriggers.io/project/retry-policy
ttr-api-key: ttr_…
PUT https://api.timetriggers.io/project/retry-policy
ttr-api-key: ttr_…
Content-Type: application/json
{ "retryOn": "5xx_and_network", "retryMaxAttempts": 5 }

PUT is a patch: fields you omit keep their value.

Per-trigger retry

Opt a single trigger in on /schedule with ttr-retry-*header headers. Sending any of them sets a per-trigger policy; the others take these defaults:

HeaderDefaultValues
ttr-retry-strategyheaderexponentialnone, fixed, exponential
ttr-retry-max-attemptsheader3integer ≥ 1, counts the first attempt
ttr-retry-initial-delayheader30seconds; first delay (every delay for fixed)
ttr-retry-max-delayheadernoneseconds; cap for exponential
ttr-retry-jitterheaderfalsetrue / false; randomises each delay within ×0.5–1.5
ttr-retry-onheader5xx_and_networknetwork_only, 5xx_and_network, non_2xx
POST https://api.timetriggers.io/schedule
ttr-api-key: ttr_…
ttr-url: https://example.com/hook
ttr-retry-max-attempts: 5
ttr-retry-on: network_only

ttr-retry-strategy: none pins the trigger to a single attempt whatever the tags or the project say. A per-trigger policy on a cron(...) trigger is stored on the generator and copied onto every instance. Re-posting a trigger without retry headers clears its per-trigger policy — /schedule always replaces the whole definition.

On /declare and /bulk, the same options live in a retry object on each item (an empty object {} opts in with the defaults):

{
"upserts": [
{
"customKey": "invoice-42",
"url": "https://example.com/hook",
"retry": { "maxAttempts": 5, "retryOn": "network_only" }
}
]
}

A changed retry object counts as an update (changedFields: ["retry"]); omitting it on a later call clears the policy.

Timeouts

Two independent budgets apply to every execution:

TimeoutDefaultSet by
Connect timeout — DNS + TCP + TLS handshake10 sfixed
Request timeout — the whole exchange, from connect to the last byte of the response300 s (5 min)requestTimeoutSeconds on a tag policy; the shortest across a trigger's tags wins

A target that never completes the handshake fails after about 10 seconds with connect_timeout, not after five minutes. Both values are recorded on the execution (connectTimeoutSeconds, timeoutSeconds).

What an execution records when it fails

Alongside errorMessage, an execution that got no HTTP response carries:

  • errorKind — the classification: dns, connect_timeout, connection_refused, unreachable, tls, connection_reset, timeout, protocol, body_read, invalid_request, unknown.
  • errorCode — the underlying code, e.g. ENOTFOUND, UND_ERR_CONNECT_TIMEOUT, ECONNRESET, CERT_HAS_EXPIRED.

The dashboard shows the kind as the result badge (CONNECT TIMEOUT, DNS, TLS, …) and the code and timeouts in the execution's details. errorMessage itself reads like Connect timeout after 10s: Connect Timeout Error (attempted address: 203.0.113.4:443, timeout: 10000ms) [UND_ERR_CONNECT_TIMEOUT] (POST https://example.com/hook).

For dns, connect_timeout, connection_refused, unreachable and tls nothing reached your endpoint; for connection_reset and timeout the request may have.

Finding and replaying dead executions

Every read endpoint accepts your API key (ttr-api-keyheader or Authorization: Bearer ttr_…), scoped to the key's project — no dashboard session needed. A reconciler can poll for terminal failures and replay them:

GET https://api.timetriggers.io/executions?deadLetterOnly=true&limit=100
ttr-api-key: ttr_…

returns the executions with isDeadLetter: true, newest first, each with its jobId, errorKind, errorCode and the job's URL. To retry one:

POST https://api.timetriggers.io/jobs/{jobId}/executions
ttr-api-key: ttr_…
Content-Type: application/json
{ "triggeredBy": "retry" }

which queues a fresh attempt with the trigger's stored method, headers and body. GET /jobs, GET /jobs/{id}/executions and GET /executions (with jobId, customKey, executionStatus, httpStatus, deadLetterOnly filters) work the same way — see the full spec.

TimeTriggers — Schedule HTTP requests at any time.