Golang HTTP client timeout이 날 때 무엇부터 볼까
마지막 업데이트

Golang HTTP client timeout이 날 때 무엇부터 볼까


Go에서 outbound HTTP 호출이 계속 timeout 난다면 원인은 remote service일 수도 있지만, 실제로는 client transport, dial path, idle connection pool, retry 동작 쪽에 있을 수도 있습니다.

그래서 하나의 timeout 에러 메시지 뒤에 여러 다른 문제가 숨어 있을 수 있습니다. 느린 upstream, 비싼 DNS lookup, TLS handshake 지연, connection reuse 부족, retry 증폭이 모두 비슷한 모습으로 드러날 수 있습니다.

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

  • 전체 request timeout과 세부 경계를 어떻게 나눠서 볼지
  • upstream latency와 로컬 client 지연을 어떻게 구분할지
  • timeout 값을 바꾸기 전에 transport reuse, pool, retry를 어떻게 볼지

짧게 말하면 HTTP client timeout을 하나의 숫자 문제로 보면 안 됩니다. dial, TLS, response wait, retry 동작으로 예산을 나눈 뒤, 실제로 어느 경계가 시간을 태우는지 먼저 확인해야 합니다.

더 넓은 Go 분기부터 다시 보고 싶다면 Golang 트러블슈팅 가이드로 가세요.


먼저 timeout 경계부터 나누기

첫 질문은 단순합니다. 어느 레이어가 먼저 timeout 나는가입니다.

하나의 큰 timeout 값만 보면 실제 병목이 아래 중 무엇인지 놓치기 쉽습니다.

  • connection establishment
  • DNS lookup
  • TLS handshake
  • response header wait
  • upstream body delivery 지연
  • retry가 예산을 다 태우는 상황

그래서 이 문제를 단일 timeout 문제라고 부르기 시작하면 디버깅이 오히려 어려워집니다.


실제로 무엇이 섞여 보이는가

Go에서 outbound request latency는 여러 구간으로 나뉠 수 있습니다.

  • 재사용 가능한 idle connection을 기다리는 시간
  • 새 connection을 dial하는 시간
  • TLS negotiation 시간
  • response header를 기다리는 시간
  • response body를 읽는 시간
  • 실패 후 retry하는 시간

평범한 http.Client{Timeout: ...}는 이 전체를 한 번에 감쌉니다. 편리하긴 하지만, 어느 구간이 느렸는지 숨기기도 합니다.

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

client := &http.Client{
	Timeout: 2 * time.Second,
}

resp, err := client.Get(url)

이 호출이 timeout 나도 실제 원인이 remote latency인지, local connection setup인지, retry 때문인지는 아직 알 수 없습니다.


자주 나오는 원인

1. 느린 upstream 응답

dependency가 단순히 허용 예산보다 느릴 수 있습니다.

자주 보이는 신호는 아래와 같습니다.

  • 하나의 endpoint가 timeout을 대부분 차지함
  • upstream 쪽 latency가 지배적으로 길어짐
  • 약간 더 큰 budget에서는 같은 요청이 성공함

개념적으로는 가장 단순하지만, 처음부터 이 원인이라고 단정하지는 마세요. upstream 문제처럼 보여도 실제로는 local connection이나 pool 문제인 경우가 꽤 있습니다.

2. dial, DNS, TLS 지연

connection setup만으로도 예상보다 많은 시간이 소모될 수 있습니다.

먼저 아래를 보세요.

  • DNS resolution 지연
  • TCP connect 지연
  • TLS handshake 지연
  • 부하 때 새 connection이 예상보다 많이 생기는지

connection reuse가 약하면 실제 애플리케이션 요청이 시작되기 전 setup cost만 계속 내다가 timeout에 걸릴 수 있습니다.

3. idle pool과 connection reuse 불일치

HTTP client 성능은 connection reuse와 transport 설정에 크게 좌우됩니다.

idle connection이 충분히 재사용되지 않거나, pool 설정이 concurrency와 맞지 않으면 요청이 빠르게 나가지 못하고 connection 생성 비용을 반복해서 치르게 됩니다.

확인할 점은 아래와 같습니다.

  • 공유되는 http.ClientTransport를 재사용하는지
  • idle connection 제한이 실제 트래픽과 맞는지
  • 요청마다 새 client를 만드는지

너무 자주 새 client를 만드는 것은 숨은 latency 원인이 되기 쉽습니다.

4. retry 증폭

retry는 전체 latency를 키우고, 하나의 약한 dependency 경로를 훨씬 심각해 보이게 만들 수 있습니다.

예를 들어 첫 번째 시도에서 이미 timeout 예산 대부분을 써버리면 뒤의 retry는 거의 여지가 없습니다.

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

retry loop가 outer context budget을 조심스럽게 보지 않으면 timeout 패턴이 랜덤해 보일 수 있습니다. 하지만 실제 원인은 거의 timeout 직전까지 가는 시도를 여러 번 반복하는 것일 수 있습니다.


실전 점검 순서

outbound call timeout이 반복될 때는 아래 순서가 빠릅니다.

  1. 전체 request timeout과 dial, TLS, response wait를 분리
  2. connection setup 시간과 upstream service latency 비교
  3. client와 transport를 제대로 재사용하는지 확인
  4. idle pool 동작과 concurrency 불일치 확인
  5. retry 규칙이 outer budget을 어떻게 소모하는지 검토

이 순서가 중요한 이유는, 시간이 로컬에서 사라졌는지 원격에서 사라졌는지 모른 채 timeout부터 키우는 실수를 막아주기 때문입니다.

증상이 HTTP client 한 호출보다 더 넓은 timeout 문제처럼 보인다면 Golang context deadline exceeded와 같이 비교해 보세요.


조금 더 안전한 client 기본선

많은 서비스에서는 shared client와 explicit transport를 두는 편이 기준선을 잡기 쉽습니다.

transport := &http.Transport{
	MaxIdleConns:        100,
	MaxIdleConnsPerHost: 10,
	IdleConnTimeout:     90 * time.Second,
}

client := &http.Client{
	Timeout:   2 * time.Second,
	Transport: transport,
}

이 설정이 모든 서비스의 정답은 아닙니다. 하지만 중요한 메시지는 분명합니다. request timeout 하나만 보는 것으로는 client 동작을 이해하기 어렵고, pooling과 reuse가 timeout 예산 사용 방식에 큰 영향을 줍니다.


upstream 지연과 local client 문제를 나누는 법

빠르게 나누면 이렇게 볼 수 있습니다.

  • upstream 지연: connection setup은 정상인데 server response time이 예산을 대부분 차지함
  • local client 문제: dial, TLS, pool wait가 이미 예산 상당 부분을 소모함

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

  • upstream이 느리다면 dependency 상태, endpoint latency, timeout budget을 점검
  • local client가 느리다면 transport reuse, DNS, TLS, idle pool, client 생성 패턴을 점검

이 구분 없이 보면 실제로는 local client 설정 문제인데도 remote service만 의심하게 되기 쉽습니다.


FAQ

Q. http.Client.Timeout은 전체를 다 감싸나요?

네. 클라이언트 관점에서 요청 생애주기를 넓게 감싸기 때문에, 어느 내부 구간이 느린지 오히려 가릴 수도 있습니다.

Q. 요청마다 새 http.Client를 만들어도 되나요?

보통은 권하지 않습니다. shared client와 transport를 재사용하는 편이 connection reuse와 latency 안정성에 유리합니다.

Q. 부하 때 timeout이 급증하면 무엇부터 봐야 하나요?

시간이 upstream response에 쓰였는지, connection setup에 쓰였는지, client reuse 부족 때문인지 먼저 나눈 뒤 timeout 값을 조정하세요.


Sources:

먼저 읽어볼 가이드

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