이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • connect timeout과 read timeout은 정확히 어디서 다르게 발생하는가?
  • client, proxy, load balancer, server, DB, external API timeout은 어떤 순서로 정렬해야 하는가?
  • timeout이 너무 길거나 짧을 때 어떤 장애가 생기는가?

개요

Timeout은 “느린 요청을 포기하는 시간”이 아니라, 시스템 자원을 언제 회수할지 정하는 운영 정책이다. 백엔드에서 timeout을 명시하지 않으면 요청 thread, connection, DB transaction, HTTP client pool, proxy upstream connection이 필요 이상 오래 묶일 수 있다.

반대로 timeout을 너무 짧게 잡으면 정상 처리 중인 요청을 중간에서 끊고, retry가 붙으면서 중복 요청과 장애 증폭이 생긴다. 그래서 timeout은 단일 값이 아니라 요청 경로 전체의 정렬 문제로 봐야 한다.

왜 백엔드 개발자가 알아야 하는가

장애 상황에서 timeout은 거의 항상 등장한다.

  • 사용자는 30초 뒤 504를 봤지만 애플리케이션은 60초 동안 계속 DB 작업을 수행한다.
  • client read timeout이 먼저 터져 retry했는데, 첫 번째 요청도 서버에서는 성공해 중복 주문이 생긴다.
  • 외부 API read timeout이 너무 길어 request thread와 HTTP client pool이 고갈된다.
  • DB query timeout이 HTTP timeout보다 길어 사용자는 실패했는데 DB 작업은 계속 돈다.
  • LB idle timeout이 짧아 SSE, WebSocket, long polling 연결이 주기적으로 끊긴다.

이 문제들은 코드 한 줄의 버그가 아니라 여러 계층의 timeout이 서로 다른 의미와 값을 가진 데서 생긴다.

요청 흐름에서의 위치

Timeout은 요청 경로의 모든 계층에 있다.

Client timeout
→ CDN / WAF timeout
→ LB idle timeout
→ Reverse proxy connect/read/send timeout
→ WAS request/thread timeout
→ Application HTTP client timeout
→ DB query / transaction timeout
→ External API timeout

장애 분석에서는 “어디에서 timeout이 났는가”를 먼저 찾아야 한다. 같은 timeout이라도 client가 포기한 것, proxy가 upstream을 기다리다 포기한 것, 앱이 외부 API를 기다리다 포기한 것은 조치가 다르다.

원리

timeout은 크게 연결 수립, 요청 전송, 응답 대기, 자원 대기, 유휴 연결 제한으로 나눌 수 있다.

timeout위치의미대표 증상
connect timeoutTCP 연결 수립대상 host/port와 연결을 맺는 시간 제한connect timed out, 방화벽/라우팅/리스너 의심
TLS handshake timeoutTLS 협상인증서, SNI, cipher 협상 제한handshake failure, 특정 클라이언트만 실패
write timeout요청 전송request body를 보내는 시간 제한큰 업로드, 느린 client, upstream 수신 지연
read/response timeout응답 대기요청 후 첫 응답 또는 전체 응답 대기 제한downstream 처리 지연, DB/API 지연
pool acquire timeoutconnection pool 대기사용 가능한 연결을 빌리는 시간 제한pool pending 증가, 외부 API 호출 시작 전 실패
idle timeout유휴 연결데이터가 없는 연결을 유지하는 시간 제한장기 연결 주기적 끊김, stale connection
request timeout서버 요청 처리서버가 한 요청을 처리할 최대 시간503/504, 서버 thread 장기 점유
query timeoutDB 쿼리DB가 한 쿼리를 수행할 최대 시간slow query, lock wait, transaction 지연
transaction timeout트랜잭션 전체업무 단위 전체의 최대 시간rollback, lock 장기 점유

핵심은 connect timeoutread timeout을 구분하는 것이다. connect timeout은 아직 요청을 제대로 보내기 전의 문제다. read timeout은 연결과 요청 전송 후 상대가 응답을 늦게 주는 문제다.

Timeout 정렬 기준

정답 값은 서비스마다 다르지만, 원칙은 있다.

계층판단 기준
사용자-facing API사용자가 기다릴 수 있는 시간보다 짧아야 한다.
reverse proxy애플리케이션의 정상 처리 상한보다 약간 길거나, 의도적으로 먼저 끊을지 결정해야 한다.
application serverproxy가 끊은 뒤에도 일을 계속하지 않도록 cancellation과 timeout을 맞춘다.
HTTP clientdownstream SLO보다 짧고, caller request timeout보다 짧아야 한다.
DB queryHTTP request timeout보다 길게 방치하지 않는다.
retrytimeout보다 짧은 backoff와 제한된 attempt를 가진다.
long polling / SSE / WebSocketidle timeout보다 짧은 heartbeat를 둔다.

실무에서 자주 쓰는 기준은 “후단 의존성 timeout은 caller의 전체 timeout보다 짧아야 한다”이다. 그래야 caller가 사용자에게 응답할 시간을 남긴다.

잘못 정렬된 예시

Client read timeout: 5s
Application external API read timeout: 10s
Payment API actual processing: 8s
Client retry: enabled
Idempotency key: 없음

이 구조에서는 client가 5초에 포기하고 retry할 수 있다. 그런데 첫 번째 요청은 서버에서 계속 결제 API를 기다리고 있고, 8초에 성공할 수 있다. retry 요청도 다시 결제를 시도하면 중복 결제 위험이 생긴다.

더 나은 방향은 아래와 같다.

Client read timeout: 8s
Application external API timeout: 3s
Retry: 최대 1회, backoff + jitter
Idempotency key: 필수
Payment result 저장: 요청 key 기준 unique

결제처럼 중복에 민감한 요청은 timeout보다 idempotency가 먼저다.

실무에서 자주 만나는 문제

  • proxy timeout이 app timeout보다 짧다.
    • 사용자는 504를 보지만 앱은 계속 처리한다.
    • 요청 취소 전파, app timeout, DB timeout을 함께 맞춘다.
  • HTTP client read timeout이 너무 길다.
    • 외부 API 장애 때 request thread와 pool이 오래 묶인다.
    • 중요한 dependency는 짧게 실패하고 fallback이나 안내 응답을 준다.
  • DB query timeout이 없다.
    • 사용자가 떠난 뒤에도 DB lock과 connection이 오래 남는다.
    • query timeout, transaction timeout, statement timeout을 둔다.
  • retry가 timeout을 곱한다.
    • 3초 timeout에 3회 retry면 한 요청이 9초 이상 자원을 점유할 수 있다.
    • retry budget과 전체 deadline을 둔다.
  • idle timeout을 모른 채 SSE/WebSocket을 운영한다.
    • 60초, 120초마다 연결이 끊기는 식의 주기적 장애가 난다.
    • heartbeat와 LB/proxy idle timeout을 맞춘다.

디버깅 방법

timeout 장애는 메시지에 어느 계층의 timeout인지 힌트가 남는다.

connect timed out
→ DNS 이후 TCP 연결 수립 실패 가능성
→ 방화벽, security group, route, listener, 대상 port 확인
 
Read timed out
→ 연결은 되었지만 응답이 늦음
→ downstream 처리, DB, 외부 API, server saturation 확인
 
upstream timed out
→ proxy가 backend 응답을 기다리다 포기
→ NGINX/Envoy upstream log, app latency, DB/API trace 확인
 
504 Gateway Timeout
→ gateway/proxy가 upstream 대기 중 timeout
→ target health, upstream latency, proxy timeout 확인
 
pool acquire timeout
→ 외부 API에 요청하기 전 client pool 대기 실패
→ pool active/pending, retry, downstream latency 확인

curl -w로 외부에서 보이는 지연 위치도 확인한다.

curl -sS -o /dev/null \
  -w "code=%{http_code}\ndns=%{time_namelookup}\nconnect=%{time_connect}\ntls=%{time_appconnect}\nfirst_byte=%{time_starttransfer}\ntotal=%{time_total}\n" \
  https://example.com/api/orders

connect가 길면 TCP 연결 전후를 보고, first_byte가 길면 서버가 요청을 받은 뒤 처리하는 구간을 본다.

코드로 확인하기

Spring WebClient에서 timeout을 분리해 명시한다.

ConnectionProvider provider = ConnectionProvider.builder("order-dependency")
        .maxConnections(50)
        .pendingAcquireTimeout(Duration.ofMillis(500))
        .maxIdleTime(Duration.ofSeconds(20))
        .build();
 
HttpClient httpClient = HttpClient.create(provider)
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2_000)
        .responseTimeout(Duration.ofSeconds(3))
        .doOnConnected(conn -> conn
                .addHandlerLast(new ReadTimeoutHandler(3))
                .addHandlerLast(new WriteTimeoutHandler(3)));
 
WebClient client = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .baseUrl("https://dependency.example.com")
        .build();

NGINX에서도 timeout 이름이 다르다.

location /api/ {
    proxy_pass http://app_upstream;
    proxy_connect_timeout 2s;
    proxy_send_timeout 10s;
    proxy_read_timeout 30s;
    client_body_timeout 10s;
}

관찰 포인트는 다음이다.

  • proxy_connect_timeout은 proxy가 upstream에 연결하는 제한이다.
  • proxy_read_timeout은 upstream 응답을 기다리는 제한이다.
  • app의 외부 API timeout이 proxy timeout보다 길면 사용자는 실패했는데 app은 계속 일할 수 있다.
  • DB query timeout이 HTTP client timeout보다 길면 downstream 장애가 DB lock으로 번질 수 있다.

주니어가 자주 하는 오해

  • timeout은 넉넉하게 길게 잡으면 안전하다고 생각한다.
    • 긴 timeout은 실패를 늦게 발견하고 thread, connection, lock을 오래 붙잡는다.
  • connect timeout과 read timeout을 같은 것으로 본다.
    • connect는 연결 수립 전, read는 요청 후 응답 대기다.
  • proxy 504를 보면 애플리케이션이 반드시 죽었다고 생각한다.
    • app이 살아 있어도 proxy가 정한 시간 안에 응답하지 못하면 504가 난다.
  • retry를 넣으면 timeout 장애가 해결된다고 본다.
    • timeout과 retry는 함께 설계하지 않으면 장애를 곱한다.

시니어의 설계 판단 기준

  • API 유형별 deadline을 먼저 정한다. 조회, 결제, 업로드, streaming은 기다릴 수 있는 시간이 다르다.
  • 모든 downstream 호출은 caller의 전체 deadline보다 짧은 timeout을 갖는다.
  • retry 횟수는 timeout과 곱해서 최악의 점유 시간을 계산한다.
  • 결제, 주문, 예약은 timeout 설정보다 idempotency key와 상태 저장 설계가 먼저다.
  • timeout이 발생해도 서버 내부 작업을 계속할지 취소할지 명확히 정한다.
  • timeout 값은 코드, proxy 설정, LB 설정, DB 설정을 한 표로 관리한다.

인프라 협업 포인트

인프라 팀과 맞춰야 할 timeout은 많다.

항목함께 확인할 것
LB idle timeoutSSE, WebSocket, long polling 연결 유지 시간
proxy read timeout앱의 정상 처리 상한과 긴 처리 API 여부
proxy body timeout파일 업로드, 느린 client, mobile network
health check timeout느린 readiness가 target을 불필요하게 unhealthy로 만드는지
WAF timeout큰 body 검사나 특정 rule 평가 지연
Gateway to backend timeoutapp 처리 시간과 upstream dependency 지연

장애 때는 “timeout이 납니다”가 아니라 “어느 계층의 어떤 timeout이 몇 초 뒤 발생합니다”라고 말해야 한다.

실전 팁

  • timeout inventory를 만든다.
Client:
CDN/WAF:
Load Balancer:
Reverse Proxy:
WAS:
HTTP Client:
DB Query:
Transaction:
External API:
Retry 전체 deadline:
  • timeout은 평균 응답시간이 아니라 p95/p99와 사용자 기대 시간을 기준으로 잡는다.
  • retry가 있다면 timeout 값을 줄이는 대신 idempotency와 backoff를 같이 본다.
  • 긴 작업은 동기 HTTP 요청으로 버티지 말고 202 Accepted + polling, callback, queue를 검토한다.
  • timeout이 난 요청이 서버에서 성공했을 수 있다는 사실을 항상 고려한다.

위험 신호!

  • timeout 값이 라이브러리 기본값에 의존한다.
  • proxy timeout과 application timeout이 문서화되어 있지 않다.
  • 504가 난 뒤에도 DB 작업이 계속 돈다.
  • retry 횟수와 각 timeout을 곱한 최악의 시간이 SLO를 넘는다.
  • payment/order API에 idempotency key 없이 client retry가 가능하다.

확인 질문

확인 질문

  • connect timeout과 read timeout의 가장 큰 차이는 무엇인가?
    • connect timeout은 대상과 TCP 연결을 맺는 단계의 제한이고, read timeout은 요청을 보낸 뒤 응답을 기다리는 단계의 제한이다.
  • proxy timeout이 app timeout보다 짧으면 어떤 일이 생길 수 있는가?
    • 사용자는 504를 보지만 애플리케이션은 계속 처리할 수 있고, 이로 인해 중복 처리나 불필요한 자원 점유가 생길 수 있다.
  • retry를 설계할 때 timeout과 함께 계산해야 하는 것은 무엇인가?
    • 전체 deadline, 최대 시도 횟수, backoff, retry budget, idempotency 여부다.

참고 문서