Golang context deadline exceeded 트러블슈팅 가이드
마지막 업데이트

Golang context deadline exceeded 트러블슈팅 가이드


Go 서비스에서 context deadline exceeded가 뜬다면, 보통 문제의 중심은 context 자체가 아닙니다. 이 에러는 호출자가 정한 시간 예산 안에 작업이 끝나지 않았다는 사실만 알려줍니다.

그래서 이 에러는 원인이 다양합니다. 어떤 때는 downstream API가 느린 것이 원인이고, 어떤 때는 DB connection 대기나 lock 대기가 시간을 다 써버립니다. 또 어떤 경우에는 실제 작업보다 queue 대기나 local saturation 때문에 예산이 먼저 소모됩니다.

이 글은 실전 순서에 집중합니다.

  • timeout 예산이 실제로 어디서 사라졌는지 확인하는 방법
  • 느린 dependency와 로컬 병목을 구분하는 방법
  • retry, pool, nested deadline 문제를 어떻게 좁혀갈지

짧게 말하면 deadline이 만료될 때 정확히 어떤 작업이 실행 중이었는지 찾고, 실제 지연과 설정된 timeout을 비교한 뒤, dependency latency, queueing, retry, 비현실적으로 짧은 timeout 중 어디서 예산이 소모되는지 추적하면 됩니다.

Go 안에서 더 큰 분기부터 다시 보고 싶다면 Golang 트러블슈팅 가이드로 먼저 돌아가세요.


이 에러는 보통 무엇을 뜻하나

context deadline exceeded는 context의 deadline이 작업 완료보다 먼저 도달했다는 뜻입니다.

운영에서는 보통 아래 상황 중 하나를 가리킵니다.

  • outbound HTTP 또는 RPC 호출이 느린 경우
  • DB 질의, connection pool 대기, lock 대기가 예산을 다 쓰는 경우
  • worker나 handler가 실제 작업 전에 capacity를 기다리느라 늦어지는 경우
  • nested timeout이나 retry가 예산을 예상보다 빨리 태워버리는 경우
  • 현재 workload에 비해 timeout 값이 지나치게 짧은 경우

핵심은 이것이 경계 에러라는 점입니다. context는 시간이 부족했다고만 알려줍니다. 실제로 시간이 어디서 쓰였는지는 따로 찾아야 합니다.


먼저 느린 경계부터 보기

이 에러는 우선 경계 문제로 보는 편이 빠릅니다.

값부터 바꾸기 전에 아래를 물어보세요.

  • deadline이 만료될 때 정확히 어떤 작업이 실행 중이었는가
  • 이 timeout이 서버, 클라이언트, worker, downstream 중 어디 소속인가
  • 지연의 대부분이 실제 작업, queue 대기, connection wait, retry 중 어디에서 발생했는가

이 프레이밍이 중요한 이유는, 많은 팀이 너무 빨리 timeout을 늘리기 때문입니다. 실제 문제를 pool starvation, retry amplification, dependency 불안정이 만들고 있다면 더 긴 timeout은 문제를 늦게 드러나게 할 뿐입니다.


timeout 예산이 어디서 소모되는지 추적하기

운영에서 가장 빠른 질문은 “timeout 값을 얼마로 썼나?”가 아니라 “밀리초가 어디서 사라졌나?”입니다.

하나의 요청 경로에서 아래를 비교해 보세요.

  • 요청 시작 시각
  • downstream 호출 시작과 종료 시각
  • DB 대기 시간과 실제 query 시간
  • retry와 backoff 시간
  • worker가 작업을 집기 전 queue 대기 시간

경계에 가벼운 로그만 있어도 도움이 됩니다.

ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()

start := time.Now()
err := client.Do(ctx, req)
log.Printf("operation=client.Do elapsed=%s err=%v", time.Since(start), err)

이 호출이 반복해서 1.9s가 걸리고 deadline이 2s라면 결과는 충분히 예상 가능합니다. 반대로 호출 자체는 빠른데 요청 전체가 timeout이라면 예산은 다른 구간에서 사라졌을 가능성이 큽니다.


자주 나오는 원인

1. 느린 downstream 서비스

HTTP나 RPC dependency가 호출자가 허용한 시간보다 느릴 수 있습니다.

자주 보이는 신호는 이렇습니다.

  • 하나의 dependency가 전체 latency를 대부분 차지함
  • 특정 endpoint나 region에서 timeout이 몰림
  • retry가 같은 느린 경로를 더 비싸게 만듦

먼저 그 dependency의 실제 latency를 확인하세요. dependency가 불안정한데 retry, fallback, budget 설계는 그대로 둔 채 timeout만 늘리면 전체 상황이 더 나빠질 수 있습니다.

2. 데이터베이스 지연과 pool 대기

쿼리 실행 시간, connection 획득 대기, lock 경합만으로도 deadline 예산이 쉽게 사라집니다.

아래를 먼저 보세요.

  • 느린 쿼리
  • connection pool 고갈
  • 오래 잡고 있는 transaction lock
  • query 시작 전부터 길게 대기하는 요청

그래서 timeout 장애와 DB 장애는 자주 같이 나타납니다. 애플리케이션 로그에는 timeout으로 보이지만, 실제 병목은 DB 계층에 있을 수 있습니다.

3. retry와 nested deadline

하나의 요청은 여러 계층을 통과할 수 있습니다.

  • incoming request timeout
  • service-level timeout
  • client timeout
  • dependency retry timeout

이 계층들이 잘 설계되지 않으면 서로 충돌합니다. 짧은 내부 timeout에 여러 retry를 얹으면 바깥 budget을 다 써버릴 수 있고, parent context에 시간이 거의 없는데도 retry가 계속 돌 수 있습니다.

간단한 예시는 이렇습니다.

ctx, cancel := context.WithTimeout(parentCtx, 500*time.Millisecond)
defer cancel()

for i := 0; i < 3; i++ {
	err := client.Do(ctx, req)
	if err == nil {
		break
	}
}

한 번의 시도만으로도 예산 대부분이 사라진다면 뒤의 retry는 성공 가능성이 거의 없습니다.

4. 로컬 worker 포화

dependency는 정상인데 요청이 worker, queue slot, semaphore, DB connection을 기다리느라 늦어지는 경우도 많습니다.

자주 보이는 단서는 아래와 같습니다.

  • 부하가 올라갈 때 CPU나 goroutine 수가 함께 상승함
  • dependency latency는 정상인데 end-to-end latency만 늘어남
  • timeout이 concurrency spike 구간에서 집중됨

이 경우 예산은 원격이 아니라 로컬에서 소모되고 있는 것입니다.


실전 점검 순서

장애가 진행 중일 때는 아래 순서가 빠릅니다.

  1. context deadline exceeded를 반환한 정확한 작업 식별
  2. 실제 latency와 설정된 timeout 비교
  3. dependency latency, pool wait, queue delay 확인
  4. 같은 경로의 retry와 nested timeout 검토
  5. timeout 값이 비현실적인지, 아니면 실제 병목이 있는지 판단

이 순서가 좋은 이유는 두 가지 흔한 실수를 막아주기 때문입니다.

  • 병목을 찾기 전에 timeout부터 늘리는 실수
  • 로컬 queueing과 pool starvation을 보지 않고 dependency 탓부터 하는 실수

같은 서비스에서 goroutine 수도 계속 오르거나 작업이 걸린 채 멈춘다면 다음으로 Goroutine leak 찾는 법을 이어서 보세요.


timeout을 늘리는 것이 틀린 해결책일 때

더 긴 timeout이 맞는 경우도 있습니다. 하지만 아래 상황에서는 첫 대응으로 적절하지 않습니다.

  • dependency가 이미 불안정하고 retry가 부하를 키우는 경우
  • 대부분의 시간이 로컬 capacity 대기에 쓰이는 경우
  • 요청 경로에 lock 또는 queue 병목이 있는 경우
  • 불필요한 작업 때문에 예산이 낭비되는 경우

이럴 때는 timeout을 높여도 에러가 잠시 줄어들 뿐, tail latency와 backlog, 리소스 압박이 커질 수 있습니다.

정상 작업에 비해 timeout이 작은 것인지, 비정상 대기가 예산을 먹는 것인지 먼저 구분한 뒤 timeout을 조정하세요.


로컬 병목과 원격 병목을 빨리 나누는 법

이렇게 나누면 편합니다.

  • 원격 병목: 로컬 queue가 건강해도 dependency latency가 높다
  • 로컬 병목: dependency latency는 정상인데 dependency에 도달하기 전부터 오래 기다린다

이 구분에 따라 다음 액션이 완전히 달라집니다.

원격 병목이라면 dependency 상태, timeout budget, retry 규칙, fallback 동작을 보세요.

로컬 병목이라면 아래를 보세요.

  • worker concurrency
  • DB pool 크기
  • semaphore 또는 queue 경합
  • 메인 호출 전에 handler가 동기적으로 처리하는 작업

FAQ

Q. 이 에러가 뜨면 항상 upstream이 느린 건가요?

아닙니다. 로컬 queueing, pool wait, lock contention, retry amplification도 원인이 될 수 있습니다.

Q. timeout을 그냥 늘리면 안 되나요?

시간이 어디서 쓰였는지 확인하기 전에는 권하지 않습니다. 먼저 병목을 찾는 쪽이 안전합니다.

Q. 운영에서 무엇부터 보는 게 가장 빠른가요?

timeout이 난 정확한 작업을 찾고, 실제 latency와 configured deadline을 비교한 뒤, 빠진 시간이 dependency latency인지 local waiting인지 먼저 나누세요.


Sources:

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.