Fetch timeout in JavaScript: AbortSignal.timeout and retry
fetch() has no timeout option. Pass signal: AbortSignal.timeout(ms), catch the TimeoutError, and retry network errors, 429 and 5xx with backoff.

To set a fetch timeout, pass an abort signal in the options: fetch(url, { signal: AbortSignal.timeout(5000) }) gives up after 5 seconds and rejects with a TimeoutError DOMException. fetch() has no timeout option and no retry, so retries are a loop you write: resend on network errors, timeouts, 408, 429 and 5xx with backoff, wait for Retry-After, and resend a POST only when it carries an Idempotency-Key.
Fetch facts come from MDN's AbortSignal.timeout(), fetch() and RequestInit pages, Node.js's global objects page and the undici docs listed under Sources. Sume facts come from Jobs and results, Create a run, Errors and spend and Authentication, plus the current code of Sume's TypeScript SDK. All were read on 2026-09-28.
Does fetch have a default timeout?
Not as an option: MDN's list of fetch() options, RequestInit, offers signal to cancel a request and no timeout field. In Node.js, the built-in fetch is powered by a bundled undici, whose default dispatcher inherits undici's client defaults: headersTimeout and bodyTimeout of 300e3 milliseconds, so 300 seconds waiting for headers and 300 seconds between body chunks. process.versions.undici shows which undici your Node.js bundles.
AbortSignal.timeout() has worked across current browsers since April 2024 and exists in Node.js since v17.3.0 and v16.14.0. It counts active time, so the clock pauses while a page sits in the back-forward cache or a worker is suspended.
How do I retry a failed fetch?
Wrap it. fetch() resolves on HTTP error statuses such as 404 or 504 and rejects only when the request fails, so the loop has to check res.status as well as catch. This version gives each attempt its own timeout, backs off exponentially with ±20% jitter, waits Retry-After seconds when the server sends them, and never resends a POST that has no Idempotency-Key. It also cancels the body of a response it throws away: undici's README says to always consume or cancel a response body in Node.js, or connections can run out.
| What happened | What fetch does | Retry it? |
|---|---|---|
| Network failure | Rejects with a TypeError | Yes, if the request is safe to resend |
| Your timeout fired | Rejects with a TimeoutError | Yes, if the request is safe to resend |
408, 429 or 5xx | Resolves; check res.status | Yes, with backoff; wait Retry-After when sent |
Any other 4xx | Resolves; check res.status | No. At a Sume create, nothing ran and nothing was charged |
You called abort() | Rejects with an AbortError | No |
async function fetchWithRetry(url, init = {}, { retries = 2, timeoutMs = 30_000 } = {}) {
const method = (init.method ?? "GET").toUpperCase();
const resendable =
method === "GET" || method === "HEAD" || new Headers(init.headers).has("Idempotency-Key");
for (let attempt = 0; ; attempt++) {
let res;
try {
res = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
const transient = res.status === 408 || res.status === 429 || res.status >= 500;
if (!transient || attempt >= retries || !resendable) return res;
await res.body?.cancel(); // release the connection before retrying
} catch (err) {
// TimeoutError from the signal, or TypeError for a network failure
if (attempt >= retries || !resendable) throw err;
}
const retryAfter = Number(res?.headers.get("retry-after"));
const delayMs =
retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt * (0.8 + Math.random() * 0.4);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}Is it safe to retry a POST after a timeout?
Only with an idempotency key. A timeout ends your wait, not the work: Sume's docs say a client-side timeout does not cancel a generation job, which keeps running and billing, and that abandoning a poll loop does not stop a Format run or its spend. Resend the same body with the same key and Sume returns the original instead of billing a second one; for a Format run that is 200 with idempotency_hit: true. Build the body once as a string so every attempt sends the same bytes.
const res = await fetchWithRetry("https://api.sume.com/v1/formats/acme/weekly-promo/runs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "weekly-promo-2026-W40",
},
body: JSON.stringify({ input: { week: "2026-W40" } }),
});
const { data, error } = await res.json(); // 202: new run. 200: replay.How long should the timeout be?
Long enough for the request, not for the work behind it. A Format run create answers immediately with a receipt while the run takes minutes, and a job submit that waits blocks for at most 30 seconds. Size the per-request timeout for that, then take the result by polling status_url or by webhook. If your own deadline passes, keep polling rather than submitting a new paid job; retry the submit itself only with the same key. Video generation API timeouts lists Sume's wait limits.
Where should this fetch code run?
On your server. Sume's docs say browser and mobile clients should call your backend, which attaches the API key, and that keys don't belong in frontend JavaScript. In TypeScript, @sume-com/sdk already wraps fetch this way in current code: a 10-minute AbortSignal.timeout per request by default, two retries of 408, 429, 5xx and transport failures with ±20% jittered backoff, retry-after honored up to 60 seconds, and a POST retried only when it carries an Idempotency-Key. See the Sume TypeScript SDK quickstart.
Sources
- Jobs and results
- Create a run
- Runs and results
- Errors and spend
- Authentication
- MDN: AbortSignal.timeout() (read 2026-09-28)
- MDN: Window.fetch() (read 2026-09-28)
- MDN: RequestInit (read 2026-09-28)
- Node.js: Global objects (read 2026-09-28)
- undici README (read 2026-09-28)
- undici: Agent (read 2026-09-28)
- undici: Client (read 2026-09-28)
Related posts
More in Integrations
- Jenkins build periodically: cron syntax and the H symbol
Jenkins' Build periodically takes 5 cron fields plus H, a hash of the job name that spreads start times: H 20 * * * runs once in the 8 p.m. hour.
- Kilo Code MCP server: add Sume in kilo.jsonc
Add Sume's hosted MCP server to Kilo Code under the mcp key in kilo.jsonc: type remote, the Sume URL, and an API key header or an OAuth sign-in.
- Kling API in n8n: HTTP Request node, auth, and waiting
Call the Kling API from n8n with an HTTP Request node and a Bearer credential, then poll or resume a Wait node. Direct to Kling, or kling-3 through Sume.
- Langflow MCP client: add Sume's hosted MCP server
Use Langflow as an MCP client for Sume's hosted server: register it with a Bearer global variable, then wire MCP Tools into an Agent.
Written by Sume