Subrequest timeout failure in Cloudflare Workers outbound fetch
0 reputation · 14 Dec 2020, 08:29 UTC
0 reputation · 14 Dec 2020, 08:29 UTC
When a Cloudflare Worker executes a fetch() call to an upstream API, a network timeout or connection reset may occur after the request has reached the destination but before the response is received by the Worker runtime.
Because the runtime does not provide a delivery receipt for outbound subrequests, it is impossible to determine if a failure happened before the upstream service processed the write or after the write was committed but the response was lost. This creates a risk of duplicate data entries when implementing retry logic within the Worker invocation.
What is the recommended pattern for ensuring write idempotency when the Worker cannot distinguish between a failed request and a lost response? How should idempotency keys be managed to prevent duplicate writes during automatic retries?
34775 reputation · 14 Dec 2020, 15:09 UTC
The standard solution is to generate a deterministic idempotency key in the Worker and pass it to the upstream API via a custom header (typically Idempotency-Key). The upstream service must implement a persistence layer that stores the result of the first successful request keyed by this value, with a TTL covering the expected retry window.
Generate the key from immutable request properties so retries within the same Worker invocation produce identical keys:
sha256(payload + operationType + entityId).async function fetchWithIdempotency(url, options, env) {
const payload = options.body ? JSON.stringify(options.body) : '';
const key = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload));
const idempotencyKey = Array.from(new Uint8Array(key)).map(b => b.toString(16).padStart(2, '0')).join('');
const headers = new Headers(options.headers);
headers.set('Idempotency-Key', idempotencyKey);
// Optional: check KV for prior result to avoid redundant upstream calls
const cached = await env.IDEMPOTENCY_KV.get(idempotencyKey);
if (cached) return new Response(cached, { headers: { 'X-Idempotency-Replay': 'true' } });
const response = await fetch(url, { ...options, headers });
if (response.ok) {
const clone = response.clone();
await env.IDEMPOTENCY_KV.put(idempotencyKey, await clone.text(), { expirationTtl: 3600 });
}
return response;
}
The upstream API must:
Idempotency-Key headerConfigure exponential backoff with jitter in the Worker. Cloudflare Workers' default fetch retry behavior does not apply to subrequest timeouts—you must implement retry logic explicitly. Limit retries to 2–3 attempts to stay within CPU time limits.
crypto.subtle available (all current versions).One diagnostic needed: Does your upstream API already support an idempotency header, or must you implement the persistence layer yourself? This changes whether you only modify the Worker or also need upstream changes.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 14 Dec 2020, 09:51 UTC
A fetch() timeout or AbortController.abort() only tells the Worker that the client-side connection ended. It does not confirm whether the upstream server already committed the write. Generate the idempotency key before the first attempt and send the same key on every retry; regenerating it inside a catch block makes the upstream treat the retry as a new write. To verify, point a test Worker at a mock upstream that logs X-Idempotency-Key and simulate a network failure: the same key should appear on retry, while a new key indicates the retry logic is creating duplicate logical writes.