TechFeedTechFeed
Backend

ETIMEDOUT, 연결 시간 초과, fetch | 거절이 아니라 기다리다 죽으면?

ETIMEDOUT은 거절이 아니라 소켓이 응답을 기다리다 한도를 넘긴 줄입니다. ECONNREFUSED, ENOTFOUND, 서버리스 함수 한도와 구분하고 AbortSignal.timeout을 명시합니다. Node.js, Next.js, 한국 1인 개발자 기준. 2026년 9월 Node 에러·MDN fetch 문서.

by

ETIMEDOUT은 거절당한 게 아니라, 그 주소에 연결을 시도한 뒤 응답이 안 와서 소켓이 기다리다 죽은 줄입니다. 서버 창이 열려 있어도 방화벽이나 느린 DNS, 한도 없는 fetch면 같은 코드가 납니다.


로컬에선 되고 프리뷰만 30초 만에 끊기거나, 카카오 콜백이 가끔만 빨간 줄로 떨어지는 일, 1인 넥스트 운영에서 자주 만납니다. 로그에는 200이 없는데 브라우저는 빙글빙글만 돕니다.


포트를 죽이기 전에 연결이 거절인지, 이름을 못 찾는지, 기다리다 죽었는지부터 가르세요. 근거는 Node.js 에러 문서AbortSignal.timeout에 있습니다.


거절이 아니라 기다리다 죽은 줄인가

소켓이 응답을 못 받아 한도를 넘긴 줄입니다. 노드는 원격 호스트에 SYN을 보냈는데 ACK가 안 오거나, 연결은 됐는데 본문이 안 끝나면 ETIMEDOUT을 던집니다. 상대가 "그 포트에 아무도 없다"고 바로 거절한 줄과는 다릅니다.


ECONNREFUSED는 그 주소에 수신 프로세스가 없을 때 바로 돌아옵니다. ENOTFOUND는 DNS가 이름 자체를 IP로 못 바꾼 줄입니다. ETIMEDOUT은 이름은 풀렸고 패킷도 나갔는데, 반대편이 침묵해서 시계가 먼저 끝난 경우입니다.


그래서 서버 터미널이 열려 있어도 이 줄이 납니다. 프로세스는 살아 있는데 다른 네트워크 경로로 나가거나, 보안 그룹이 인바운드만 열고 아웃바운드를 막거나, 상대 API가 응답을 안 주는 상태면 로컬 listen과 무관합니다.


먼저 읽을 것 | 메시지에 호스트와 포트가 같이 찍힙니다. 그 숫자가 내가 연 개발 서버인지, 외부 API인지부터 가르면 다음 명령이 달라집니다.


fetch가 응답을 기다리다 ETIMEDOUT으로 끊기는 네트워크 개념 이미지
거절이 아니라 침묵이면 포트 종료가 아니라 경로와 한도를 본다

세 줄 에러를 먼저 가르기

같은 "연결 실패"라도 다음 행동이 갈립니다. 표로 먼저 나눕니다.


코드무엇이 실패했나먼저 볼 곳
ETIMEDOUT연결 또는 읽기 한도 초과방화벽, 상대 응답, fetch 한도
ECONNREFUSED그 포트에 수신 없음listen 여부, 포트 번호
ENOTFOUNDDNS가 이름을 못 품호스트 철자, 도커 서비스명
FUNCTION_INVOCATION_TIMEOUT서버리스 실행 시간 초과maxDuration, 웹훅 쪼개기

버셀에서 함수가 끊긴 줄은 소켓 타임아웃이 아닙니다. 플랫폼이 허용 초를 넘겨 프로세스를 죽인 줄이라, fetch 한도를 늘려도 함수 자체가 먼저 끝납니다. 그 칸은 서버리스 시간 한도 글입니다.


거절 줄은 ECONNREFUSED, 이름 줄은 ENOTFOUND로 나눠 두었습니다. 콘솔에 찍힌 네 글자만 복사해도 글이 갈립니다.


에러 코드와 대상 URL을 같이 찍기
try { const res = await fetch(url, { signal: AbortSignal.timeout(8000) }) console.log('status', res.status) } catch (err) { console.error('code', err.cause && err.cause.code, err.code) console.error('url', url) }

로컬은 되고 배포만 기다리다 죽을 때

노트북은 회사 Wi-Fi나 집 공유기 안에서 상대 서버에 바로 붙습니다. 프리뷰 함수는 데이터센터에서 나가서, 상대가 IP 허용 목록에 노트북만 넣어 둔 순간 침묵합니다. 카카오·토스 웹훅 URL을 로컬 터널로만 열어 둔 경우도 같은 증상입니다.


두 번째는 IPv6입니다. 호스트가 localhost::1로 풀었는데 서버는 IPv4만 listen하면, 거절이 아니라 경로에 따라 오래 기다리다 죽을 수 있습니다. fetch URL을 127.0.0.1로 바꿔 재현되면 여기입니다.


세 번째는 한도 없는 요청입니다. 브라우저 fetch는 기본 한도가 느슨하고, 서버 컴포넌트에서 외부 API를 부르면 상대가 멈춘 동안 함수 시계만 갑니다. AbortSignal.timeout으로 8~10초를 명시하면, 무한 대기 대신 내가 정한 줄로 떨어집니다.


  • [ ] 에러 코드가 ETIMEDOUT인지 네 글자를 확인했다
  • [ ] 대상이 내 서버인지 외부 API인지 URL을 읽었다
  • [ ] 로컬과 프리뷰에서 같은 호스트로 나가는지 비교했다
  • [ ] localhost면 127.0.0.1로도 재현해 봤다
  • [ ] fetch에 타임아웃을 명시했는지 봤다

로컬 노트북과 프리뷰 데이터센터가 다른 경로로 API에 붙어 한쪽만 타임아웃 나는 이미지
IP 허용 목록에 노트북만 있으면 프리뷰는 기다리다 죽는다

한도를 명시하고 재시도는 짧게

고치는 첫 칸은 한도를 내가 정하는 것입니다. MDN 기준 AbortSignal.timeout(ms)는 그 시간이 지나면 요청을 취소합니다. 취소와 타임아웃을 구분하려면 catch에서 AbortError와 시스템 코드 ETIMEDOUT을 같이 로그하세요.


재시도는 횟수와 대기를 박지 않으면 상대를 더 막습니다. 5xx나 타임아웃만 한두 번 재시도하고, 4xx는 바로 실패로 두는 편이 안전합니다. 웹훅이면 200을 먼저 돌려주고 본문 처리는 뒤로 미루는 쪽이 함수 한도와도 맞습니다.


상대가 정말 죽은 건지 보려면 같은 호스트에 curl -v --max-time 5를 노트북과 프리뷰 로그에서 비교합니다. 노트북만 되면 허용 IP, 보안 그룹, 프라이빗 네트입니다. 둘 다 죽으면 상대 장애이거나 DNS가 엉뚱한 IP를 줍니다.


한도를 명시한 fetch
const TIMEOUT_MS = 8000 export async function getJson(url) { const res = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS), headers: { Accept: 'application/json' }, }) if (!res.ok) throw new Error('HTTP ' + res.status) return res.json() }

실전에서 좁히는 순서

코드부터 고치지 않습니다. 에러 코드, 대상 URL, 로컬/프리뷰 차이를 적고, 거절·이름·서버리스 한도와 겹치는지 표로 걸러 냅니다. 그다음 curl로 경로를 확인하고, fetch 한도를 명시합니다.


DB 풀이 비어 멈추는 줄은 소켓 타임아웃과 다릅니다. 프리즈마가 연결을 못 빌려 대기하다 죽는 칸은 P2024입니다. 호스트까지 못 붙는 줄과 풀 칸이 없는 줄을 한 로그에 섞지 마세요.


CORS로 화면만 막힌 줄도 다릅니다. 브라우저는 응답을 읽지 못해도 서버 로그에는 200이 남습니다. ETIMEDOUT은 서버 쪽 소켓이 먼저 죽어서 로그에 성공이 안 찍힙니다. 헤더 문제는 CORS 글을 보세요.


curl max-time으로 로컬과 프리뷰 경로를 비교해 타임아웃 원인을 좁히는 이미지
같은 호스트를 두 환경에서 5초만 두드리면 경로 차이가 드러난다

참고 자료


내부 연계: ECONNREFUSED, ENOTFOUND, 서버리스 시간 한도, 프리즈마 연결 풀


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


자주 묻는 질문

서버는 켜 뒀는데 왜 타임아웃인가요?

프로세스가 살아 있는 것과 그 요청 경로가 닿는 것은 다른 칸입니다. 포트가 다르거나, IPv6로 나가거나, 상대가 침묵하면 창이 열려 있어도 기다리다 죽습니다. 에러에 찍힌 호스트부터 읽으세요.


ECONNREFUSED와 어떻게 다른가요?

거절은 그 포트에 수신이 없다고 바로 돌아옵니다. 타임아웃은 패킷이 나갔는데 답이 없어 시계가 끝난 줄입니다. 포트를 죽이는 명령은 거절 줄에 먼저 해당합니다.


버셀 함수 타임아웃과 같은가요?

아닙니다. FUNCTION_INVOCATION_TIMEOUT은 플랫폼이 함수 실행 초를 넘겨 프로세스를 끊은 줄입니다. ETIMEDOUT은 그 안의 소켓이 상대를 기다리다 죽은 줄입니다. maxDuration과 fetch 한도를 섞지 마세요.


AbortSignal.timeout을 넣으면 해결되나요?

무한 대기는 막을 수 있습니다. 상대 경로가 막힌 문제는 남습니다. 한도를 명시한 뒤 curl로 로컬과 프리뷰 경로를 비교하세요.


재시도를 많이 하면 나아지나요?

상대가 잠깐 멈춘 경우에만 한두 번이 도움이 됩니다. 방화벽에 막힌 요청을 반복하면 더 오래 죽습니다. 4xx는 재시도하지 마세요.


프리즈마 타임아웃과 섞여 보입니다.

P2024는 디비에 닿은 뒤 풀 칸을 못 받은 줄입니다. ETIMEDOUT은 OS 소켓이 먼저 죽은 줄입니다. 메시지 코드와 meta를 따로 복사하세요.


ETIMEDOUT은 상대가 거절한 게 아니라 침묵해서 시계가 끝난 줄입니다. 거절·DNS·서버리스 한도와 가른 뒤, URL과 경로를 확인하고 fetch 한도를 명시하세요. 관련 글: 연결 거부, DNS, 함수 시간 한도.


ETIMEDOUT연결 시간 초과fetchAbortSignal타임아웃넥스트노드백엔드프리뷰개발자

함께 보면 좋은 문제 해결

EXPLORE / Backend

이어서 읽어보기

전체 토픽 둘러보기