HTTPX AsyncClient TimeoutException 원인과 timeout 설정 방법

HTTPX AsyncClient TimeoutException을 ConnectTimeout, ReadTimeout, WriteTimeout, PoolTimeout으로 구분하고 단계별 timeout 설정과 PoolTimeout 진단 순서를 설명합니다.

HTTPX AsyncClient에서 TimeoutException이 발생하면 timeout 숫자부터 늘리기보다 실제 하위 예외를 먼저 확인하는 편이 정확합니다. ConnectTimeout, ReadTimeout, WriteTimeout, PoolTimeout은 기다리는 대상이 서로 달라 원인과 조치도 다릅니다.

목차

TimeoutException은 하나의 원인이 아닙니다

HTTPX의 TimeoutException 아래에는 네 가지 대표 하위 예외가 있습니다.

예외 기다리는 대상 먼저 확인할 항목
ConnectTimeout 서버와 연결 수립 주소, DNS, 방화벽, 서버 연결 상태
ReadTimeout 응답 데이터 수신 서버 처리 지연, 응답 스트림 중단
WriteTimeout 요청 데이터 전송 업로드 크기, 전송 경로, 서버 수신 상태
PoolTimeout 연결 풀에서 빈 연결 확보 동시 요청 수, max_connections, pool 대기 시간

따라서 로그에 TimeoutException만 남기기보다 실제 예외 클래스까지 기록해야 합니다. 특히 PoolTimeout은 원격 서버가 느리다는 뜻으로 단정할 수 없습니다. 로컬 클라이언트의 연결 풀이 모두 사용 중일 때도 발생할 수 있습니다.

기본 timeout 5초의 의미

HTTPX timeout 공식 문서는 기본 동작을 네트워크 비활성 기준으로 설명합니다. 기본 5초를 “HTTP 요청 전체가 5초 안에 끝나야 한다”는 총 실행시간 제한으로 해석하면 안 됩니다.

응답 데이터가 계속 도착하는 요청과 연결 자체가 진행되지 않는 요청은 같은 설정에서도 다르게 동작할 수 있습니다. 오류를 볼 때는 전체 요청 시간을 하나로 보지 말고 연결, 읽기, 쓰기, pool 대기 단계 중 어디에서 시간이 소진됐는지 확인해야 합니다.

AsyncClient timeout을 단계별로 설정하는 방법

모든 단계에 같은 값을 사용하려면 AsyncClient(timeout=10.0)처럼 단일 값을 지정할 수 있습니다. 단계별로 다르게 제어하려면 httpx.Timeout을 사용합니다.

import httpx

timeout = httpx.Timeout(
    connect=5.0,
    read=20.0,
    write=10.0,
    pool=3.0,
)

async def fetch(client: httpx.AsyncClient, url: str) -> str:
    response = await client.get(url)
    response.raise_for_status()
    return response.text

async def fetch_many(urls: list[str]) -> list[str]:
    async with httpx.AsyncClient(timeout=timeout) as client:
        results = []
        for url in urls:
            results.append(await fetch(client, url))
        return results

이 값들은 보편적인 권장값이 아니라 설정 형식을 보여 주기 위한 예시입니다. 실제 값은 대상 API의 응답 특성, 네트워크 환경, 동시 요청 수를 기준으로 정해야 합니다. 반복 요청에서는 함수 호출마다 새 AsyncClient를 만들기보다 위 예제처럼 하나의 client를 여러 요청에서 재사용해야 connection pooling의 이점을 유지할 수 있습니다.

timeout=None을 지정하면 timeout 자체를 비활성화할 수 있지만, 오류 원인을 모르는 상태에서 일반 해결책으로 쓰는 것은 피하는 편이 좋습니다. 서버가 응답하지 않는 경우 작업이 예상보다 오래 남을 수 있기 때문입니다.

PoolTimeout과 connection limits의 관계

PoolTimeout은 연결 풀에서 사용할 연결을 얻기 전에 대기 시간이 끝났을 때 발생합니다. HTTPX AsyncClient API는 Limits를 통해 전체 연결 수와 keep-alive 연결 수를 제한할 수 있습니다.

limits = httpx.Limits(
    max_connections=20,
    max_keepalive_connections=10,
)

client = httpx.AsyncClient(
    timeout=timeout,
    limits=limits,
)

동시 task가 많고 max_connections가 작다면 요청은 pool에서 기다릴 수 있습니다. 이 경우 read timeout만 늘려서는 문제 지점을 건드리지 못합니다. 동시에 실행되는 작업 수, max_connections, pool 값을 함께 봐야 합니다.

여러 외부 API 호출에서 클라이언트 수명과 연결 재사용까지 같이 점검해야 한다면 httpx AsyncClient로 외부 API 비동기 호출하기에서 해당 구조를 이어서 확인할 수 있습니다.

예외 종류별 처리 예시

하위 예외를 먼저 처리하고 마지막에 부모인 TimeoutException을 처리하면 로그에서 원인을 구분하기 쉽습니다.

try:
    response = await client.get(url)
except httpx.ConnectTimeout:
    print("연결 시간 초과")
except httpx.ReadTimeout:
    print("응답 읽기 시간 초과")
except httpx.WriteTimeout:
    print("요청 전송 시간 초과")
except httpx.PoolTimeout:
    print("연결 풀 대기 시간 초과")
except httpx.TimeoutException:
    print("기타 timeout")

실제 서비스에서는 print() 대신 요청 URL, 예외 타입, 시도 횟수처럼 장애 분석에 필요한 값을 구조화된 로그로 남기는 편이 좋습니다. 다만 인증정보나 토큰은 로그에 포함하지 않아야 합니다.

예제 환경과 적용 범위

Python 3.11.15와 HTTPX 0.28.1을 기준으로 Timeout·Limits 객체와 네 가지 timeout 하위 예외의 TimeoutException 상속 관계, AsyncClient 사용 형태를 설명합니다.

timeout 하위 예외의 의미와 Timeout·Limits 설정 형식은 HTTPX 공식 문서로 확인했습니다. 근거는 Timeouts, Exceptions, Async Support, Developer Interface 문서입니다. 실제 API에 맞는 값은 예외 타입과 요청 지연을 측정해 정해야 하며, 위 숫자는 설정 형식을 보여 주는 예시입니다.

문제를 좁히는 점검 순서

  1. 로그에서 TimeoutException의 실제 하위 타입을 확인합니다.
  2. 연결, 읽기, 쓰기, pool 대기 중 어느 단계인지 분류합니다.
  3. connect, read, write, pool 설정을 각각 확인합니다.
  4. PoolTimeout이면 동시에 실행되는 task 수와 max_connections를 함께 확인합니다.
  5. timeout 값을 바꾸기 전에 같은 조건에서 예외 종류가 반복되는지 확인합니다.
  6. 조정 후에는 정상 응답뿐 아니라 실패 상황에서도 요청이 무기한 남지 않는지 확인합니다.

핵심은 TimeoutException을 하나의 오류로 처리하지 않는 것입니다. 하위 예외를 기준으로 문제 구간을 먼저 좁힌 뒤 해당 단계의 설정을 조정해야 불필요한 timeout 확대를 줄일 수 있습니다.