TechFeedTechFeed
Backend

ETIMEDOUT, fetch failed, undici | 배포 함수에서만 타임아웃이면?

배포 함수에서만 fetch가 끊기면 로컬 회선이 빠른 게 아니라 함수 대기 한도와 아웃바운드 경로가 다르다. ETIMEDOUT과 undici fetch failed의 cause.code를 남기고, maxDuration·AbortSignal·리전을 맞춘다. 개발자, Node.js, 버셀, Next.js, 백엔드, API 기준. 2026년 8월 Node.js·MDN·버셀 문서.

by

배포 함수에서만 fetch가 끊기면 로컬 네트워크가 빠른 게 아니라, 함수의 대기 한도와 아웃바운드 경로가 노트북과 다릅니다. ETIMEDOUT과 undici fetch failed는 연결이 그 시간 안에 끝나지 않았다는 뜻입니다.


한국에서 버셀 서울 리전으로 올리고, 외부 API는 도쿄나 미국인 조합이면 로컬 광랜과 체감이 갈립니다. 제가 돌리는 페이지뷰 핑도 함수 안에서 외부 호스트를 치면 가끔 이 줄이 납니다.


재시도만 늘리기 전에 AbortSignal과 함수 최대 시간, IPv6 이슈를 보세요. 근거는 Node.js 에러, AbortSignal.timeout, 버셀 함수 duration에 있습니다.


타임아웃은 502와 다른 칸이다

상대 서버가 거절한 게 아니라, 내 쪽이 기다리다 포기한 겁니다. 502는 게이트웨이가 업스트림을 이상하다고 본 응답이고, ETIMEDOUT은 소켓이 시간 안에 연결·읽기를 못 한 시스템 에러입니다.


undici는 Node.js 기본 fetch 구현입니다. TypeError: fetch failedcause에 ETIMEDOUT, ECONNRESET, CERT 오류가 붙습니다. 겉 메시지만 보면 네트워크 전체가 죽은 것처럼 보입니다. error.cause.code를 로그하세요.


로컬이 되는 이유는 노트북이 같은 주소를 집 회선으로 바로 치기 때문입니다. 함수는 플랫폼 바깥 경로와 리전, 짧은 기본 시간을 씁니다. 체감이 빨라 보여도 배포 쪽 시계가 더 짧으면 그날따라만 실패하는 것처럼 보입니다.


먼저 기억할 것 | fetch failed만 저장하지 말고 cause.code를 남기세요. ETIMEDOUT이면 시간·리전, CERT이면 인증서, ECONNREFUSED이면 대상 포트입니다.


서버리스 함수에서 외부 API로 나가는 요청이 시간 한도에 걸리는 개념 이미지
함수 최대 시간과 fetch 타임아웃 중 먼저 오는 쪽이 로그에 남는다

시계가 세 개다, 함수·fetch·상대 서버

세 시계 중 가장 짧은 값이 이깁니다.


시계무엇을 자르나어디에 있나
함수 maxDuration핸들러 전체버셀 등 플랫폼 설정
AbortSignal.timeout그 fetch 한 번코드
상대 서버원본이 느림외부 API SLA
프록시/NAT유휴 연결플랫폼 아웃바운드

함수가 10초인데 fetch를 30초로 두면, 10초에 플랫폼이 자릅니다. 로그는 타임아웃이 아니라 함수 킬로 남을 수 있습니다. 반대로 fetch를 3초로 두면 cause가 AbortError입니다. ETIMEDOUT은 보통 OS 소켓 한도입니다.


응답 후 작업은 after로 빼면 사용자 요청 시계와 분리됩니다. 사용자에게 줄 값은 짧은 fetch만 남기세요.


fetch에 명시 타임아웃과 cause 로그
try { const res = await fetch(url, { signal: AbortSignal.timeout(8000) }) if (!res.ok) throw new Error('HTTP ' + res.status) } catch (err) { console.error('code', err.cause && err.cause.code) console.error('name', err.name) throw err }

IPv6와 리전이 로컬과 다를 때

일부 호스트는 IPv6 주소는 광고하는데 실제 포트가 안 열려 있습니다. 노트북은 IPv4로 붙고, 함수 런타임은 AAAA를 먼저 써서 타임아웃이 납니다. 상대가 IPv4만 되면, 플랫폼 문서의 IPv4 강제나 고정 아웃바운드를 확인하세요.


리전은 왕복 시간에 바로 붙습니다. 서울 함수가 미국 원본을 치면 콜드 스타트와 합쳐 기본 시간을 넘기기 쉽습니다. 원본과 가까운 리전을 쓰거나, 캐시 가능한 GET은 엣지 캐시를 앞에 두세요. 캐시 헤더는 캐시 컨트롤 글입니다.


DNS가 함수에서만 느린 경우도 있습니다. 매 요청 새 호스트를 치면 리졸버 한도에 걸립니다. 같은 호스트는 연결을 재사용합니다. undici는 기본 풀을 씁니다. 매 호출 new Agent를 만들지 마세요.


  • [ ] error.cause.code를 로그에 남겼다
  • [ ] 함수 maxDuration과 fetch timeout을 비교했다
  • [ ] 로컬과 배포 리전·원본 리전을 적어 봤다
  • [ ] 사용자 응답에 필요 없는 호출은 after로 뺐다
  • [ ] 재시도에 지터를 넣고 한도를 정했다

서울 리전 함수와 해외 API 원본 사이의 왕복 지연 개념 이미지
리전이 멀면 로컬 광랜에서 안 보이던 타임아웃이 난다

재시도는 횟수보다 간격을 먼저

같은 호스트를 즉시 세 번 치면 상대 429와 내 타임아웃이 겹칩니다. 지수 백오프에 무작위 간격을 넣고, 멱등인 GET만 재시도하세요. POST 결제는 재시도하면 이중 요청입니다.


429는 다른 글의 주제입니다. ETIMEDOUT 재시도와 한도 초과 재시도를 한 함수에 섞지 마세요. Retry-After가 있으면 그 초를 지키세요.


연결 거절은 ECONNREFUSED입니다. 도커 로컬 5432는 그 칸입니다. 타임아웃은 포트는 열려 있는데 응답이 안 오는 쪽에 가깝습니다.


무한 대기 금지 | signal 없는 fetch는 함수 한도까지 잡아먹습니다. 플랫폼이 자르면 사용자에게는 504로 보일 수 있습니다. 코드에 초를 명시하세요.


함수 킬과 게이트웨이 504를 구분하기

함수 로그에 핸들러가 끝까지 갔는데 브라우저만 504면 앞단 프록시 시간입니다. 핸들러가 중간에 끊기면 maxDuration입니다. fetch cause가 ETIMEDOUT이면 아웃바운드입니다. 세 로그를 같은 요청 ID로 묶으세요.


스트리밍 응답은 첫 바이트 이후에도 유휴 시간이 있습니다. 오래 여는 연결은 함수 상품이 아니라 큐·웹훅이 맞습니다. 문의 저장 후 메일은 after로 빼는 편이 안전합니다.


postgres 문장 제한은 문장 타임아웃입니다. DB가 느려 fetch가 기다려도, cause는 소켓이 아니라 앱이 만든 Abort일 수 있습니다. 레이어를 섞지 마세요.


보이는 것실제 시계손볼 곳
fetch failed + ETIMEDOUT아웃바운드 소켓리전, DNS, 상대 SLA
AbortError코드 timeout초 값, 불필요 호출 제거
504 브라우저프록시 또는 함수 킬maxDuration, 스트리밍

함수 로그와 브라우저 504를 같은 요청으로 맞추는 개념 이미지
브라우저 504와 함수 로그의 cause 코드는 다른 시계일 수 있다

참고 자료


내부 연계: 응답 후 작업, 캐시 헤더, DB 문장 제한, 터널과 웹훅, CI 스크립트 실패


인용한 동작은 2026년 8월 공개 문서 기준입니다.


자주 묻는 질문

로컬 fetch는 1초인데 배포만 실패합니다.

리전, NAT, 함수 한도가 다릅니다. cause.code를 남기고 maxDuration과 AbortSignal을 비교하세요. 로컬 광랜 속도는 증거가 아닙니다.


fetch failed만 보이고 코드가 없습니다.

err.cause를 찍으세요. undici는 본래 에러를 cause에 넣습니다. 겉 TypeError만으로는 DNS, TLS, 타임아웃을 구분할 수 없습니다.


AbortSignal.timeout을 넣으면 ETIMEDOUT이 안 나나요?

그 경우 이름은 보통 AbortError입니다. OS 소켓이 먼저 끊기면 여전히 ETIMEDOUT입니다. 둘 다 로그에 남기는 편이 좋습니다.


재시도를 세 번 하면 해결되나요?

상대가 느린데 즉시 세 번이면 더 바쁩니다. GET만, 간격에 지터를 두고, 함수 총 시간을 넘지 않게 하세요. POST는 멱등을 확인하기 전엔 한 번만 보냅니다.


브라우저 504와 함수 로그가 안 맞습니다.

앞단 프록시 시간과 함수 시간이 다를 수 있습니다. 요청 ID로 세 레이어를 묶으세요. 함수가 성공인데 504면 프록시 유휴입니다.


DB 쿼리 때문에 fetch가 늦는 것 아닌가요?

쿼리는 문장 제한 글의 칸입니다. fetch cause가 ETIMEDOUT이면 그 소켓입니다. DB를 기다리는 동안 fetch를 열어 두면 시계가 겹칩니다. 순서를 나누세요.


배포 함수의 fetch 실패는 노트북 회선이 아니라 함수 시계와 아웃바운드 경로입니다. cause.code를 남기고, 세 시계를 맞춘 뒤 재시도를 넣으세요. 관련 글: after, 캐시, 빌드 메모리.


ETIMEDOUTfetch failedundiciAbortSignalmaxDuration버셀Node.jsNext.js백엔드API개발자타임아웃

함께 보면 좋은 문제 해결

EXPLORE / Backend

이어서 읽어보기

전체 토픽 둘러보기