- Published on
Go gRPC context deadline exceeded 9가지 원인
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버와 클라이언트 모두 정상처럼 보이는데 gRPC 호출이 context deadline exceeded로 끝나면, 원인은 “느림” 하나가 아니라 데드라인이 어디에서 걸렸는지(클라이언트/서버/프록시/네트워크)부터 분해해야 합니다. 특히 Go gRPC는 context.Context 기반으로 타임아웃이 전파되기 때문에, 작은 설정 실수나 리소스 병목이 곧바로 데드라인 초과로 관측됩니다.
이 글은 Go gRPC에서 context deadline exceeded가 발생하는 대표 원인 9가지를 증상 → 확인 방법 → 해결 형태로 정리합니다. (환경이 Kubernetes/EKS라면 DNS와 네트워크 계층을 반드시 함께 보세요. 예: EKS에서 Pod DNS만 느릴 때 ndots·search 튜닝, EKS STS 엔드포인트 타임아웃 - VPC·NAT·DNS 해결)
먼저: 이 에러는 어디서 발생했나?
context deadline exceeded는 보통 클라이언트가 정한 deadline 내에 RPC가 완료되지 못했을 때 발생합니다. 하지만 “서버가 느림”만이 전부가 아닙니다. 다음을 먼저 분리하세요.
- 클라이언트 측: Dial 단계에서 timeout? RPC 처리 단계에서 timeout?
- 서버 측: 서버 로그에 요청이 도착했는가? 도착했는데 처리 중 끊겼는가?
- 중간 프록시/LB: idle timeout/connection draining/HTTP2 설정 문제?
최소 재현/관측 코드(클라이언트)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
start := time.Now()
resp, err := client.SomeRPC(ctx, &pb.Request{})
elapsed := time.Since(start)
st, _ := status.FromError(err)
log.Printf("elapsed=%s err=%v code=%v message=%q", elapsed, err, st.Code(), st.Message())
_ = resp
code=DeadlineExceeded면 대개 클라이언트 컨텍스트 deadline 초과입니다.code=Unavailable또는Internal등으로 바뀌면 네트워크/프록시/서버 오류일 가능성이 큽니다.
원인 1) Dial(연결) 단계가 느리거나 막힘: DNS/TCP/TLS
증상
- 첫 호출에서만 특히 느림(콜드 스타트처럼 보임)
- 서버 로그에는 요청이 아예 안 찍힘
grpc.DialContext자체가 오래 걸리거나 실패
확인 방법
- Dial에 별도 timeout을 걸어 “연결 단계”와 “RPC 단계”를 분리합니다.
// 연결 단계 타임아웃
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
conn, err := grpc.DialContext(
ctx,
target,
grpc.WithTransportCredentials(creds),
grpc.WithBlock(), // Dial이 완료될 때까지 블록
)
WithBlock()없이 Dial하면 연결이 백그라운드에서 진행되어, 첫 RPC에서 timeout처럼 보일 수 있습니다.
해결
- DNS가 느리면 ndots/search 튜닝 및 CoreDNS 성능을 점검하세요: EKS에서 Pod DNS만 느릴 때 ndots·search 튜닝
- TLS 핸드셰이크가 무거우면 커넥션 재사용(keepalive), 인증서 체인/OCSP 확인
DialContext + WithBlock로 연결 실패를 조기에 감지
원인 2) deadline을 너무 짧게 잡았거나 “중첩 타임아웃”이 있음
증상
- 특정 환경(부하/피크)에서만 간헐적으로 발생
- 서버 처리시간 p95/p99가 타임아웃보다 큼
확인 방법
- 클라이언트 timeout과 서버 내부 timeout(예: DB, 외부 API) 관계를 확인합니다.
- 서버에서
ctx.Deadline()을 로깅하여 실제 남은 시간을 봅니다.
func (s *Server) SomeRPC(ctx context.Context, req *pb.Request) (*pb.Response, error) {
if dl, ok := ctx.Deadline(); ok {
log.Printf("deadline in %s", time.Until(dl))
}
// ...
}
해결
- SLO 기반으로 timeout을 설계: p99 + 네트워크 여유 + 재시도 여유
- 체인 호출이 있다면 “전체 요청 예산”을 먼저 정하고, 하위 호출에 분배
원인 3) 서버 핸들러가 ctx 취소를 무시하고 블로킹됨
Go에서 흔한 실수는 ctx를 받았는데도, 실제 I/O에 ctx를 전달하지 않아 취소 불가능한 블로킹이 생기는 것입니다.
증상
- 클라이언트는 deadline exceeded
- 서버는 계속 작업하다가 뒤늦게 완료하거나 goroutine 누수
확인 방법
- DB/HTTP/Redis 호출이
Context를 받는 API인지 확인 - goroutine dump에서 특정 함수가 오래 대기하는지 확인
해결
- 모든 외부 호출에 ctx를 전달
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
- 채널/락 대기에도 ctx를 반영(가능하면 select로 탈출)
select {
case v := <-ch:
_ = v
case <-ctx.Done():
return nil, status.Error(codes.DeadlineExceeded, ctx.Err().Error())
}
원인 4) 서버/클라이언트 리소스 병목: CPU, GC, 스레드, 커넥션 풀
증상
- 특정 Pod/노드에서만 유독 timeout
- p99 latency 급증, GC pause 증가
- 동시 요청 수가 늘면 급격히 악화
확인 방법
- 서버의 goroutine 수, GC, CPU 사용률, run queue 확인
- 외부 의존성(특히 DB) 커넥션 풀 고갈 여부 확인
DB 풀 고갈은 gRPC 타임아웃의 대표 원인입니다. (Java 예시지만 패턴은 동일: Spring Boot HikariCP 풀 고갈·DB 타임아웃 10분 진단)
해결
- 서버 동시성 제한(세마포어) + 큐잉/백프레셔
- DB/캐시 커넥션 풀 크기와 타임아웃 조정
- 핫패스에서 할당 줄이기, 큰 응답 스트리밍화
원인 5) 메시지/페이로드가 커서 전송 시간이 deadline을 초과
gRPC는 기본 max message size 제한이 있고, 큰 메시지는 직렬화/네트워크 전송/수신 버퍼까지 비용이 큽니다.
증상
- 큰 요청/응답에서만 timeout
- 서버는 처리 자체는 빠른데 응답 전송 중 지연
확인 방법
- payload 크기 로그
grpc.max_receive_message_length/MaxCallRecvMsgSize설정 확인
해결
- 큰 응답은 pagination 또는 server streaming으로 분할
- 필요한 필드만 보내도록 proto 설계(필드 마스킹)
원인 6) gRPC Keepalive/Idle timeout 불일치로 커넥션이 끊김
프록시/LB/서버가 HTTP/2 커넥션을 idle로 판단해 끊어버리면, 다음 RPC가 재연결/재시도 중에 deadline을 소진할 수 있습니다.
증상
- 유휴 후 첫 요청이 자주 timeout
- 간헐적으로만 발생
확인 방법
- LB idle timeout, 서버 keepalive 정책, 클라이언트 keepalive ping 주기 비교
해결(예시)
ka := keepalive.ClientParameters{
Time: 30 * time.Second,
Timeout: 10 * time.Second,
PermitWithoutStream: true,
}
conn, err := grpc.Dial(target,
grpc.WithTransportCredentials(creds),
grpc.WithKeepaliveParams(ka),
)
- 단, 과도한 keepalive는 인프라 비용/차단(특히 L7 프록시 정책) 리스크가 있으니 최소화
원인 7) 로드밸런서/인그레스가 HTTP/2(gRPC)를 제대로 처리하지 못함
특히 Kubernetes Ingress/ALB/Nginx/Envoy 설정이 gRPC(HTTP/2)와 충돌하면, 요청이 지연되거나 특정 조건에서만 실패합니다.
증상
- Pod 직접 호출은 정상인데, Ingress/LB 경유 시 timeout
- 특정 경로/호스트에서만 발생
확인 방법
- LB 액세스 로그/타겟 그룹 헬스/프록시 타임아웃 설정 확인
- gRPC는 HTTP/2 기반이므로, 프록시가 HTTP/1.1로 다운그레이드하는지 확인
해결
- Ingress에 gRPC 백엔드 프로토콜/HTTP2 설정 명시
- idle timeout, upstream timeout, max connection 설정 점검
ALB/Ingress 계층에서의 5xx/지연 패턴은 아래 글의 진단 프레임을 그대로 적용할 수 있습니다: EKS에서 ALB Ingress 502 Bad Gateway 원인 9가지
원인 8) 재시도/백오프가 deadline 예산을 소모(클라이언트 인터셉터/서비스 메시)
gRPC 재시도는 “성공률”을 올리지만, 잘못 설계하면 최종적으로 deadline exceeded만 늘립니다. 예: 2초 deadline인데 내부적으로 3번 재시도하면, 각 시도는 짧게 끝나도 총합이 2초를 넘기 쉽습니다.
증상
- 서버는 짧게 처리했는데도 클라이언트는 timeout
- 동일 요청이 서버에 여러 번 도착(중복 처리)
확인 방법
- 클라이언트 인터셉터/서비스 메시(Envoy/Istio)의 retry policy 확인
- 서버 로그에서 request-id로 중복 호출 추적
해결
- 전체 deadline을 “예산”으로 보고, (시도 횟수 × per-try timeout) 합이 예산을 넘지 않게 설계
- 멱등성 없는 RPC는 재시도 제한(또는 request-id 기반 중복 방지)
원인 9) 서버 종료/스케일링/드레이닝 중 연결이 끊겨 지연 발생
Kubernetes에서 Pod가 종료되는 동안(rolling update, scale-in) 커넥션 드레이닝이 제대로 되지 않으면, 클라이언트는 재연결/재시도하다 deadline을 소진합니다.
증상
- 배포/스케일링 이벤트 직후 timeout 급증
- 특정 Pod로 라우팅될 때만 문제
확인 방법
- Pod 이벤트에서 SIGTERM 이후 처리 시간, readiness 전환 시점 확인
- terminationGracePeriodSeconds와 preStop hook 확인
해결
- SIGTERM 시 readiness를 먼저 내려 트래픽 차단 후 드레이닝
- gRPC 서버 graceful stop 사용
// 종료 시그널 처리
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-sigCh
// 신규 커넥션/요청을 줄이고
grpcServer.GracefulStop()
}()
Pod 종료/드레이닝 디버깅은 아래 글의 체크리스트가 매우 유용합니다: Kubernetes Pod가 Terminating에 멈출 때 - finalizer·grace·SIGTERM 실전 디버깅
실전 트러블슈팅 순서(10분 컷 체크리스트)
- 클라이언트에서 Dial과 RPC 타임아웃 분리 (
DialContext + WithBlock) - 서버 로그로 요청 도착 여부 확인(도착 안 하면 네트워크/DNS/LB)
ctx.Deadline()로깅으로 서버가 받은 남은 예산 확인- 핸들러 내부에서 ctx 미전달 I/O(HTTP/DB/Redis) 점검
- p95/p99와 비교해 deadline 과소 여부 확인
- LB/Ingress의 HTTP/2, idle timeout, upstream timeout 확인
- 재시도 정책이 deadline 예산을 초과하지 않는지 확인
- 배포/스케일 이벤트와 상관관계 확인(드레이닝/GracefulStop)
- 큰 메시지/스트리밍 필요 여부 점검
마무리
context deadline exceeded는 “서버가 느리다”의 동의어가 아니라, 타임아웃 예산이 어디에서 어떻게 소진되었는지를 알려주는 신호입니다. Dial 단계, DNS/LB, 서버의 ctx 전파, 리소스 병목, 재시도 예산, 종료 드레이닝까지 9가지 축으로 나누어 보면 대부분의 케이스는 빠르게 좁혀집니다.
원하시면 사용 중인 환경(예: EKS + ALB Ingress, Istio/Envoy 여부, unary/streaming, 타임아웃 값)을 알려주시면 위 9가지 중 어디부터 보는 게 가장 빠른지 우선순위를 잡아드릴게요.