본문 바로가기
Q&A해결2026년 8월 4일1분 읽기

Promise.all로 수천 개 URL을 한꺼번에 fetch 했더니 EMFILE / socket hang up이 터집니다

c
concur_haeun
조회 518댓글 1

DB에서 상품 URL 8천 개쯤 뽑아서 각 페이지의 재고를 확인하는 스크립트를 짰어요. 빨리 끝내려고 Promise.all로 한 방에 다 돌렸는데, 처음 몇백 개는 되다가 그 뒤로 에러가 쏟아집니다.

Error: connect EMFILE ...
FetchError: request to https://... failed, reason: socket hang up
Error: getaddrinfo ENOTFOUND (간헐적)

const results = await Promise.all(urls.map(u => fetch(u))) 이런 식으로 짰습니다. 8천 개를 동시에 여는 게 문제인 건 알겠는데, 그렇다고 for문으로 하나씩 순차 처리하면 너무 느려서요. 적당히 병렬로 돌리면서 안 터지게 하려면 어떻게 하나요?

댓글 1

김김영삼2개월 전
동시에 8천 개 커넥션을 여니까 OS의 파일 디스크립터 한도(EMFILE = too many open files)랑 DNS 해석기까지 포화돼서 나는 증상이에요. socket hang up이나 간헐적 ENOTFOUND도 다 같은 뿌리입니다. 요청 자체가 잘못된 게 아니라 동시성을 안 막은 거예요.

해결은 동시 실행 개수를 제한하는 겁니다. 순차(1개)와 전체 병렬(8천 개) 사이 어딘가, 보통 10~50 정도로 잡아요.

제일 쉬운 건 p-limit 같은 라이브러리예요.

const pLimit = require('p-limit')
const limit = pLimit(20)
const results = await Promise.all(urls.map(u => limit(() => fetch(u))))

코드 구조는 그대로 두고 동시 실행만 20개로 묶입니다. limit()이 슬롯 빌 때까지 대기시켜줘요.

라이브러리 없이 하려면 URL을 20개씩 청크로 잘라 청크마다 Promise.all 하고 다음으로 넘어가는 방식도 됩니다. 다만 이건 청크 안에서 제일 느린 하나가 끝나야 다음으로 가서, 느린 요청이 섞이면 스루풋이 떨어져요. 그래서 워커 패턴(고정 개수의 async 워커가 공유 큐에서 하나씩 꺼내 처리)이 더 균일합니다.

실전 팁도 같이 챙기세요. 첫째, fetch에 타임아웃을 꼭 거세요. AbortController로 몇 초 넘으면 끊게요. 안 그러면 죽은 커넥션 하나가 슬롯을 영영 물고 있어서 전체가 멈춥니다. 둘째, Promise.all은 하나만 reject돼도 전체가 실패하니, 개별 실패를 허용하려면 Promise.allSettled를 쓰거나 각 요청 catch에서 에러를 값으로 담으세요. 셋째, 같은 호스트로 많이 쏘는 거면 keep-alive Agent를 하나 만들어 재사용하면 커넥션을 재활용해서 오히려 더 빠르고 FD도 아낍니다. undici면 Pool/Agent의 connections 옵션으로 동시성을 커넥션 풀 레벨에서 제한할 수도 있어요.

정리하면 동시성 20~30 제한 + 요청 타임아웃 + allSettled. 이 세 개면 8천 개든 8만 개든 안 터지고 돌아갑니다.
Ctrl+Enter로 등록