FastAPI에서 비동기 DB 커넥션 풀 관리하기
문제 상황
운영 중인 FastAPI 서비스에서 트래픽이 증가하면서 PostgreSQL 연결 에러가 간헐적으로 발생했다. 로그를 확인하니 asyncpg.exceptions.TooManyConnectionsError였다.
기존에는 startup 이벤트에서 커넥션 풀을 생성하고, shutdown에서 닫는 단순한 구조였다.
@app.on_event("startup")
async def startup():
app.state.pool = await asyncpg.create_pool(
dsn=DATABASE_URL,
min_size=5,
max_size=10
)
원인 분석
문제는 두 가지였다.
- 풀 사이즈 부족: max_size=10인데 동시 요청이 그 이상 들어오면 대기하다 타임아웃
- 커넥션 누수: 일부 엔드포인트에서
async with없이 직접 커넥션을 가져와 사용 후 반환하지 않음
해결 방법
먼저 커넥션 사용 패턴을 정리했다.
async def get_user(user_id: int):
async with request.app.state.pool.acquire() as conn:
return await conn.fetchrow(
"SELECT * FROM users WHERE id = $1", user_id
)
모든 DB 접근 코드에서 async with로 커넥션을 확실히 반환하도록 수정했다.
풀 설정도 조정했다.
app.state.pool = await asyncpg.create_pool(
dsn=DATABASE_URL,
min_size=10,
max_size=50,
max_inactive_connection_lifetime=300,
command_timeout=60
)
max_size: 10 → 50으로 증가 (RDS 인스턴스 max_connections 고려)max_inactive_connection_lifetime: 5분간 미사용 시 커넥션 종료command_timeout: 쿼리 타임아웃 명시적 설정
모니터링
커넥션 풀 상태를 확인할 수 있는 엔드포인트를 추가했다.
@app.get("/health/db")
async def db_health():
pool = request.app.state.pool
return {
"size": pool.get_size(),
"free": pool.get_idle_size(),
"max": pool.get_max_size()
}
CloudWatch에서 RDS 커넥션 수와 함께 모니터링하니 안정적으로 운영되고 있다.
교훈
비동기 커넥션 풀은 동기 방식과 달리 명시적인 acquire/release가 중요하다. async with 없이 사용하면 커넥션이 반환되지 않아 풀이 고갈된다. 적절한 풀 사이즈와 타임아웃 설정, 그리고 모니터링이 필수다.