---
title: "Node.js 서버가 큰 업로드를 300초 근처에서 408로 끊는 이유, requestTimeout 기본값"
description: "Express 서버로 1GB 파일을 동시에 올리면 5분 조금 넘어 빈 408이 오는 현상을 추적했습니다. 원인은 Node 의 requestTimeout 기본값 300초와 30초 점검 주기였습니다."
date: 2026-10-26
updated: 2026-10-26T07:15:20.502Z
tags: [nodejs, http, upload, timeout, express]
canonical: https://blog.wooncloud.com/posts/nodejs-request-timeout-408-large-upload
---

Express 서버로 큰 파일을 올리다가 이상한 현상을 만났습니다. 서버는 브라우저가 보낸 업로드를 받아 S3로 스트리밍하고, S3 업로드가 끝나야 응답합니다. 1GB 파일 1개는 잘 올라가는데 2개 이상을 동시에 올리면 5분 조금 넘어 전부 실패했습니다.

코드는 바뀐 게 없었습니다.

![단일 업로드는 300초 시한 안에 끝나 201 을 받지만, 동시 업로드로 처리량이 나뉘면 시한을 넘겨 408 로 끊긴다.](/images/posts/nodejs-request-timeout-408-large-upload/6cc43289-b5b8-4948-85e2-80d46e057c27.webp)

## 408인데 요청 ID가 없다

실패 응답은 408이었고 본문은 비어 있었습니다. 요청 ID 헤더도 없었습니다.

요청 ID는 미들웨어가 붙이는 값입니다. 그게 없다는 건 미들웨어에 닿기도 전에 끊겼다는 뜻이죠. Express가 아니라 Node `http` 서버가 직접 끊은 겁니다.

그리고 업로드 도중 수십 초 동안 응답이 없는 건 정상입니다. 본문을 아직 받는 중이니까요. 이걸 장애 증상으로 읽으면 프록시, 디스크, rate limiter 같은 엉뚱한 곳을 파게 됩니다.

## 300초인데 왜 318초에 끊길까

Node 18부터 `server.requestTimeout` 기본값은 300000ms입니다. 클라이언트로부터 요청 전체를, 본문까지 포함해서 받는 데 허용하는 시간입니다.

측정해 보니 단일 업로드는 306초에 201로 성공했고 동시 2개는 318초에 408로 끊겼습니다. 300초에서 딱 끊기지 않아서 헷갈리기 쉽습니다.

이 시한은 즉시 판정되지 않습니다. `connectionsCheckingInterval`(기본 30초) 간격으로 점검하기 때문에 실제 절단은 300~330초 사이 어딘가에서 일어납니다. 306초 성공과 318초 실패가 함께 나온 이유입니다.

![300초 시한은 30초 간격 점검으로 판정되어, 점검 전에 끝난 단일 업로드는 306초에 201이 되고 점검에 걸린 동시 업로드는 318초에 408로 끊긴다.](/images/posts/nodejs-request-timeout-408-large-upload/b13523e9-cac0-406b-88a0-5f5f852012a7.svg)

스트리밍 업로드는 backpressure 때문에 본문 수신 완료가 곧 저장소 전송 완료입니다. 동시 업로드로 처리량이 나뉘면 한 건이 300초를 넘기 쉽습니다.

## 타임아웃은 어떻게 바꿀까

`req.setTimeout()`과 `server.timeout`은 소켓 무활동 시한이라 이 값을 바꾸지 못합니다. requestTimeout은 서버 단위라 라우트별로 줄 수도 없습니다.

그래서 서버를 만들 때 업로드 상한에 맞춰 올립니다. 기준은 최대 파일 크기를 허용할 최저 처리량으로 나눈 값입니다.

```ts
import express from "express";

const app = express();
const server = app.listen(3000);

// 최대 6GiB를 3MB/s 로도 받을 수 있게, 약 35분
server.requestTimeout = 40 * 60 * 1000;
// 대신 30~60초 넘게 바이트가 오가지 않으면 끊는다(값은 서비스 경로에 맞게)
server.timeout = 120 * 1000;
// server.headersTimeout 은 기본값(60초) 유지
```

대신 `server.timeout`을 걸어 데이터가 멈춘 연결만 정리합니다. 꾸준히 흐르는 업로드는 통과하니까요. `headersTimeout`(기본 60초)은 그대로 둡니다. 헤더를 질질 끄는 slowloris 방어는 이 값이 맡습니다.

다만 무활동 시한을 걸기 전에 확인할 게 하나 있습니다. 바이트 없이 오래 머무는 경로가 있는지입니다. SSE, 롱폴링, 첫 바이트 전 준비가 긴 스트리밍, 클라이언트가 읽기를 멈춘 다운로드가 그런 예입니다.

## 진단할 때 볼 것

실패 시각이 300초에서 330초 사이에 몰리면 requestTimeout을 의심해 볼 만합니다.

그리고 판정용 측정은 중간에 끊지 말고 끝까지 기다려야 합니다. 80초에 끊어 본 시도는 아무것도 증명하지 못합니다.

근본적으로는 큰 파일을 presigned URL로 저장소에 직접 올리게 해서 서버를 거치지 않는 쪽이 낫습니다. 타임아웃 값을 키우는 건 그 전까지의 응급처치로 봐야겠죠.

## 참고

- https://nodejs.org/api/http.html#serverrequesttimeout
- https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
- https://nodejs.org/api/http.html#servertimeout
- https://nodejs.org/api/http.html#serverheaderstimeout
