TechFeedTechFeed
Backend

ECONNRESET, socket hang up, 연결 리셋 | 요청이 중간에 끊기면?

ECONNRESET은 포트가 비어서 거절된 줄이 아니라 한 번 붙었던 소켓을 상대가 갑자기 닫아서 요청이 중간에 끊긴 줄입니다. 원인 코드, 연결 재사용, 토스·카카오 웹훅은 성공 응답을 먼저 보냅니다. Node.js, Next.js, 한국 1인 개발자 기준. 2026년 9월 Node.js 에러·MDN Fetch 문서.

by

ECONNRESET은 포트가 비어서 거절된 줄이 아니라, 한 번 붙었던 소켓을 상대가 갑자기 닫아서 요청이 중간에 끊긴 줄입니다. 서버 창이 열려 있어도 fetch가 떨어질 수 있습니다.


로컬에선 되고 카카오 콜백이나 토스 웹훅만 가끔 끊기는 일, 넥스트 API를 혼자 붙이다 보면 한 번은 만납니다. 거절·이름 못 찾음·시간 초과와 리셋은 원인 칸이 다릅니다. 창이 열려 있다고 연결이 살아 있는 건 아닙니다.


소켓이 리셋되면 포트부터 죽이지 말고, 어느 쪽이 끊었는지와 연결 재사용부터 보세요. 근거는 Node.js 에러 문서MDN Fetch에 있습니다.


ECONNRESET은 거절과 무엇이 다른가

붙었던 소켓을 상대가 강제로 닫아서 납니다. 거절은 그 주소와 포트에 수신자가 없을 때이고, 리셋은 핸드셰이크 이후입니다. Node.js 문서는 이 코드를 시스템 에러로 두고, 소켓이 상대의 RST 패킷을 받았을 때 던진다고 적습니다.


그래서 터미널에 서버가 떠 있어도 이 줄이 납니다. 프로세스는 살아 있는데 그 요청을 처리하던 연결만 끊긴 겁니다. 카카오 로그인 콜백이 한 번은 되고 다음 번에 떨어질 때도 같은 그림입니다. 포트가 막혀 서버가 시작도 못 하는 줄은 EADDRINUSE이고, 시작 전에 거절되는 줄은 ECONNREFUSED입니다.


시간 초과와도 갈립니다. 한도까지 기다리다 죽은 줄은 ETIMEDOUT입니다. 리셋은 기다리기 전에 상대가 연결을 잘라 버린 쪽에 가깝습니다. 호스트 이름을 IP로 못 바꾼 줄은 ENOTFOUND라서, 메시지 앞 코드부터 읽으면 칸이 갈립니다.


먼저 읽을 것 | 에러 객체에 codeECONNRESET인지, 그 아래 cause에 다른 코드가 있는지 보세요. 언디치 fetch는 겉에 TypeError만 남기고 진짜 코드는 안쪽에 있습니다.


이미 열린 소켓이 상대 RST로 끊기며 ECONNRESET이 나는 네트워크 개념 이미지
거절은 시작 전, 리셋은 붙은 뒤에 상대가 연결을 잘라 버린 줄이다

어느 쪽이 소켓을 끊었는지 가리기

클라이언트 로그와 서버 로그를 같은 시각으로 맞춥니다. 표로 먼저 나눕니다.


증상먼저 볼 곳
상대 프로세스 종료요청 도중 서버 창이 사라짐서버 로그의 크래시, OOM, 미처리 예외
프록시 유휴 끊기긴 작업만 중간에 떨어짐nginx proxy_read_timeout, 로드밸런서 idle
연결 재사용첫 요청은 되고 다음만 리셋Keep-Alive, 언디치 에이전트, Connection: close
본문 길이 불일치업로드·웹훅만 끊김Content-Length와 실제 바이트, 청크 종료
TLS 핸드셰이크 중단HTTPS만 리셋, HTTP는 통과인증서 체인, SNI, 중간자 검사

한국에서 카카오·토스 웹훅을 받는 1인 넥스트라면 네 번째 칸을 자주 만납니다. 상대가 보낸 본문 길이와 서버가 읽은 길이가 다르면 프록시가 연결을 잘라 RST를 남깁니다. 다섯 번째 칸은 인증서 글과 자리가 겹치니, 코드가 리셋이면 여기 표를 먼저 보고 HTTPS만 실패하면 인증서 칸으로 넘기세요.


언디치 fetch에서 원인 코드 꺼내기
try { const res = await fetch(url, { signal: AbortSignal.timeout(15_000) }) if (!res.ok) throw new Error('HTTP ' + res.status) } catch (err) { const code = err.cause?.code || err.code console.error('fetch 실패', code, err.message) // ECONNRESET / ECONNREFUSED / ETIMEDOUT / ENOTFOUND 가 여기서 갈린다 }

연결 재사용과 프록시에서 자주 나는 이유

서버가 유휴 연결을 먼저 닫으면 클라이언트가 그 소켓을 재사용하다 리셋됩니다. 노드 기본 HTTP 에이전트와 언디치는 연결을 풀에 남겨 둡니다. 앞단 엔진엑스나 클라우드 로드밸런서가 유휴 60초에 끊는데 클라이언트는 아직 살아 있다고 믿으면, 다음 요청이 RST를 받습니다.


고치는 법은 유휴 시간을 맞추는 것입니다. 클라이언트 keep-alive를 프록시보다 짧게 두거나, 문제 나는 경로만 Connection: close로 한 번 쓰고 버립니다. 긴 작업은 응답을 먼저 보내고 뒤를 돌리는 편이 안전합니다. 버셀 함수 한도에 걸리는 줄은 FUNCTION_INVOCATION_TIMEOUT이고, 그 한도 전에 소켓만 잘리면 여기 리셋입니다.


로컬 엔진엑스 없이 넥스트만 띄운 창에서는 잘 안 보입니다. 프리뷰 도메인 앞에 CDN이 붙는 순간 유휴 규칙이 달라집니다. 카카오 콜백 URL이 프리뷰와 프로덕션이 다르면, 한쪽만 리셋이 나는 이유도 여기 있습니다.


문제 경로만 연결을 재사용하지 않기
const res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', connection: 'close', }, body: JSON.stringify(payload), signal: AbortSignal.timeout(15_000), })
  • [ ] err.cause.code가 ECONNRESET인지 확인했다
  • [ ] 같은 시각 서버 로그에 크래시가 있는지 봤다
  • [ ] 첫 요청은 되고 다음만 실패하면 keep-alive를 의심했다
  • [ ] 긴 작업만 실패하면 프록시 유휴 시간을 비교했다
  • [ ] HTTPS만이면 인증서 칸으로 넘겼다

프록시가 유휴 연결을 닫은 뒤 클라이언트가 소켓을 재사용하다 리셋되는 흐름 이미지
앞단이 먼저 닫은 소켓을 클라이언트가 재사용하면 RST가 난다

웹훅과 서버리스에서만 끊길 때

상대가 응답을 못 기다리고 먼저 끊으면 우리 쪽 로그에 리셋이 남습니다. 토스 웹훅은 짧게 200을 돌려 주라는 쪽으로 안내합니다. 저장과 메일과 정산을 한 요청 안에서 모두 끝내면, 상대 타임아웃이 우리 소켓을 자릅니다.


버셀 서버리스는 함수가 끝나며 소켓을 닫습니다. 클라이언트가 스트리밍을 읽다 함수가 내려가면 리셋으로 보입니다. 로컬 next dev에는 그 한도가 없어서 재현이 안 됩니다. 프리뷰에서만 나면 maxDuration과 웹훅 200 먼저 패턴을 같이 보세요.


도커 안에서 호스트 API를 부를 때도 리셋이 납니다. 컨테이너가 재시작되며 포트는 열려 있는데 이전 연결만 죽은 경우입니다. 거절은 도커 localhost 5432 글이고, 붙었다가 잘리면 여기입니다.


메시지연결 상태다음
ECONNRESET붙었다가 상대 RST이 글
ECONNREFUSED수신자 없음거절 글
ETIMEDOUT한도까지 무응답시간 초과 글
ENOTFOUND이름부터 못 품DNS 글

웹훅은 200을 먼저 | 토스·카카오처럼 짧은 응답을 기대하는 쪽이면, 본 작업은 큐나 after로 미루세요. 상대가 먼저 끊은 리셋을 코드 버그로 오래 보면 시간이 갑니다.


실전에서 고치는 순서

코드부터 고치지 말고 메시지 코드를 확정합니다. 겉 TypeError만 보고 재시도 루프를 넣으면 거절과 리셋이 섞입니다. 리셋만 한두 번 재시도하는 건 의미가 있지만, 거절을 재시도하면 포트만 두들깁니다.


서버 로그에 미처리 예외가 있으면 그쪽이 원인입니다. 요청 처리 중 프로세스가 죽으면 클라이언트가 보는 줄은 리셋입니다. 힙이 부족해 빌드가 죽는 줄은 다른 글이고, 런타임 API가 죽어도 같은 코드가 날 수 있습니다.


프록시 앞이면 유휴 시간과 본문 한도를 맞춥니다. CORS로 막힌 줄은 브라우저가 읽고, 소켓 리셋은 노드 쪽에서 납니다. 헤더 허용 문제는 CORS 프리플라이트로 가세요. 리셋과 CORS를 한 증상으로 묶지 마세요.


  • [ ] cause.code를 로그에 남겼다
  • [ ] 거절·시간 초과·DNS와 표를 대조했다
  • [ ] 서버 크래시 여부를 같은 시각으로 봤다
  • [ ] keep-alive 유휴를 프록시보다 짧게 맞췄다
  • [ ] 웹훅은 200을 먼저 보내고 뒤를 나눴다

웹훅 수신 서버가 짧은 200을 먼저 보내고 뒷일을 나누는 흐름 이미지
상대가 먼저 끊는 웹훅은 본 작업을 같은 요청에 넣지 않는다

참고 자료


내부 연계: 연결 거절, 연결 시간 초과, DNS 이름, 포트 점유, 서버리스 시간 한도


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


자주 묻는 질문

서버는 떠 있는데 fetch만 리셋됩니다. 포트를 죽여야 하나요?

아닙니다. 포트 점유는 서버가 시작도 못 할 때입니다. 리셋은 이미 붙은 연결이 중간에 잘린 줄이라, 프로세스 종료보다 크래시·유휴·본문 길이를 먼저 봅니다.


로컬은 되고 프리뷰만 끊깁니다.

앞단 유휴 시간과 함수 한도를 의심하세요. 로컬 개발 서버는 연결을 오래 붙들고, 프리뷰 CDN과 서버리스는 짧게 자릅니다. 웹훅이면 200을 먼저 보내는 쪽으로 나눕니다.


콘솔에는 TypeError: fetch failed만 보입니다.

언디치는 겉 이름만 TypeError로 감쌉니다. err.cause.code를 찍어야 ECONNRESET인지 거절인지 갈립니다. 겉 메시지만 보고 재시도하면 칸이 섞입니다.


첫 요청은 되고 두 번째만 실패합니다.

연결 재사용입니다. 서버나 프록시가 유휴 소켓을 닫았는데 클라이언트가 그 소켓을 다시 씁니다. 문제 경로만 Connection: close를 붙이거나, 에이전트 유휴을 앞단보다 짧게 두세요.


HTTPS만 리셋되고 HTTP는 됩니다.

핸드셰이크 단계에서 상대가 연결을 자른 경우가 많습니다. 리프 서명 검증 실패는 다른 코드로 나오기도 하고, 중간 장비가 RST만 남기기도 합니다. HTTPS만이면 인증서 체인을 같이 보세요.


재시도를 넣어도 될까요?

리셋과 시간 초과만 한두 번, 멱등 GET에 한정하는 편이 안전합니다. POST 웹훅을 무조건 재시도하면 주문이 두 번 저장됩니다. 코드가 거절이면 재시도 대신 수신 프로세스부터 살리세요.


ECONNRESET은 붙었던 소켓을 상대가 잘라 버린 줄입니다. 거절·시간 초과·DNS와 표를 가리고, cause.code를 남기고, 크래시와 keep-alive와 웹훅 200을 순서대로 보세요. 관련 글: 연결 거절, 시간 초과, DNS.


ECONNRESETsocket hang up연결 리셋fetchkeep-alive웹훅카카오토스넥스트백엔드개발자

함께 보면 좋은 문제 해결

EXPLORE / Backend

이어서 읽어보기

전체 토픽 둘러보기