fetch 타임아웃 설정: AbortSignal.timeout과 재시도
fetch()에는 timeout 옵션이 없습니다. signal: AbortSignal.timeout(ms)를 넘기고 TimeoutError를 잡은 뒤, 네트워크 오류와 429, 5xx는 백오프로 재시도하세요.

fetch에 타임아웃을 설정하려면 옵션에 abort signal을 넘기세요. fetch(url, { signal: AbortSignal.timeout(5000) })는 5초 뒤에 요청을 포기하고 TimeoutError DOMException으로 reject됩니다. fetch()에는 timeout 옵션도, 재시도 기능도 없으므로 재시도는 직접 작성하는 루프입니다. 네트워크 오류, 타임아웃, 408, 429, 5xx는 백오프를 두고 다시 보내고, Retry-After만큼 기다리며, POST는 Idempotency-Key가 있을 때만 다시 보내세요.
fetch 관련 내용은 MDN의 AbortSignal.timeout(), fetch(), RequestInit 페이지, Node.js의 전역 객체 페이지, 출처에 나열한 undici 문서에서, Sume 관련 내용은 Job과 결과 (영문), Format 호출하기 (영문), 오류와 비용 (영문), 인증 문서와 Sume TypeScript SDK의 현재 코드에서 가져왔습니다. 모두 2026-09-28에 확인했습니다.
fetch에 기본 타임아웃이 있나요?
옵션으로는 없습니다. MDN의 fetch() 옵션 목록인 RequestInit에는 요청을 취소하는 signal이 있을 뿐, 타임아웃 필드는 없습니다. Node.js의 내장 fetch는 번들된 undici로 동작하며, 기본 디스패처는 undici 클라이언트의 기본값을 물려받습니다. headersTimeout과 bodyTimeout이 300e3밀리초이므로, 헤더를 300초까지 기다리고 본문 청크 사이에서도 300초까지 기다립니다. 여러분의 Node.js가 어떤 undici를 번들하는지는 process.versions.undici로 확인할 수 있습니다.
AbortSignal.timeout()은 2024년 4월부터 현재 브라우저 전반에서 동작하며, Node.js에는 v17.3.0과 v16.14.0부터 있습니다. 이 타이머는 활성 시간만 세므로, 페이지가 뒤로-앞으로 캐시(back-forward cache)에 머물거나 워커가 일시 중지된 동안에는 시계가 멈춥니다.
실패한 fetch는 어떻게 재시도하나요?
감싸는 함수를 직접 작성하세요. fetch()는 404나 504 같은 HTTP 오류 상태에서도 resolve되고 요청 자체가 실패할 때만 reject되므로, 루프는 예외를 잡는 것과 함께 res.status도 확인해야 합니다. 아래 버전은 시도마다 별도의 타임아웃을 주고, ±20% 지터를 섞어 지수 백오프하며, 서버가 Retry-After를 보내면 그 초만큼 기다리고, Idempotency-Key가 없는 POST는 절대 다시 보내지 않습니다. 버리는 응답의 본문도 취소합니다. undici README는 Node.js에서 응답 본문을 항상 소비하거나 취소하라고 말하며, 그러지 않으면 연결이 고갈될 수 있습니다.
| 상황 | fetch의 동작 | 재시도 여부 |
|---|---|---|
| 네트워크 실패 | TypeError로 reject됨 | 예, 다시 보내도 안전한 요청이라면 |
| 설정한 타임아웃이 발생 | TimeoutError로 reject됨 | 예, 다시 보내도 안전한 요청이라면 |
408, 429 또는 5xx | resolve됨. res.status 확인 | 예, 백오프 적용. Retry-After가 오면 그만큼 대기 |
그 밖의 4xx | resolve됨. res.status 확인 | 아니요, Sume 생성 요청이라면 아무것도 실행되지 않았고 청구되지도 않음 |
직접 abort()를 호출 | AbortError로 reject됨 | 아니요 |
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));
}
}타임아웃 뒤에 POST를 재시도해도 안전한가요?
멱등성 키가 있을 때만 안전합니다. 타임아웃은 여러분의 대기를 끝낼 뿐 작업을 끝내지 않습니다. Sume 문서에 따르면 클라이언트 쪽 타임아웃은 생성 Job을 취소하지 않으며 Job은 계속 실행되고 계속 과금됩니다. 폴링 루프를 포기해도 Format 실행이나 그 비용은 멈추지 않습니다. 같은 본문을 같은 키로 다시 보내면 Sume는 두 번째 작업을 과금하는 대신 원래 것을 돌려줍니다. Format 실행이라면 idempotency_hit: true가 담긴 200입니다. 모든 시도가 같은 바이트를 보내도록 본문은 문자열로 한 번만 만드세요.
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.타임아웃은 얼마나 길게 잡아야 하나요?
요청에 충분한 만큼이면 되며, 그 뒤의 작업까지 기다릴 필요는 없습니다. Format 실행 생성 요청은 즉시 영수증으로 응답하고 실행 자체는 몇 분이 걸리며, 기다리는 방식의 Job 제출은 최대 30초 동안 블로킹됩니다. 요청당 타임아웃은 이 기준으로 잡고, 결과는 status_url 폴링이나 웹훅으로 받으세요. 자체 마감 시간이 지나더라도 새 유료 Job을 제출하지 말고 계속 폴링하세요. 제출 자체를 재시도할 때는 같은 키로만 하세요. Sume의 대기 한도는 영상 생성 API 타임아웃에 정리되어 있습니다.
이 fetch 코드는 어디서 실행해야 하나요?
여러분의 서버에서 실행하세요. Sume 문서는 브라우저와 모바일 클라이언트가 백엔드를 호출하고 백엔드가 API 키를 붙여야 하며, 키를 프론트엔드 JavaScript에 두면 안 된다고 말합니다. TypeScript에서는 현재 코드 기준으로 @sume-com/sdk가 이미 이런 방식으로 fetch를 감쌉니다. 기본적으로 요청마다 10분짜리 AbortSignal.timeout을 걸고, 408, 429, 5xx와 전송 실패를 ±20% 지터 백오프로 두 번 재시도하며, retry-after를 최대 60초까지 따르고, POST는 Idempotency-Key가 있을 때만 재시도합니다. Sume TypeScript SDK 빠른 시작을 참고하세요.
출처
관련 글
연동 카테고리의 다른 글
- Jenkins Build periodically: cron 문법과 H 기호
Jenkins의 Build periodically는 cron 필드 5개에 H를 더해 받습니다. H는 작업 이름의 해시로 시작 시각을 분산하며, H 20 * * *는 오후 8시대에 한 번 실행됩니다.
- Kilo Code MCP 서버: kilo.jsonc에 Sume 추가하기
kilo.jsonc의 mcp 키 아래에 Sume 호스팅 MCP 서버를 추가하세요. type은 remote로 두고, Sume URL에 API 키 헤더를 더하거나 OAuth로 로그인합니다.
- Langflow MCP 클라이언트: Sume 호스팅 MCP 서버 추가
Langflow를 Sume 호스팅 서버의 MCP 클라이언트로 쓰세요. Bearer 전역 변수로 서버를 등록한 뒤 MCP Tools를 Agent에 연결합니다.
- LibreChat MCP 서버: librechat.yaml에 Sume 추가하기
librechat.yaml의 mcpServers 아래에 Sume 호스팅 MCP 서버를 추가하세요. streamable-http와 Bearer 키 헤더를 쓰고, requiresOAuth는 false로 둡니다.
작성자 Sume