"Too many connections"는 데이터베이스가 허용하는 최대 연결 수(max_connections)를 초과했을 때 발생하는 대표적인 오류입니다. MySQL에서는 "Too many connections", PostgreSQL에서는 "remaining connection slots are reserved for non-replication superuser connections"와 같은 메시지로 나타납니다.
이 오류는 데이터베이스에 동시에 열린 연결이 한계를 넘었다는 뜻입니다. 왜 연결이 그렇게 쌓였는지가 진짜 질문인데, 그 답은 대개 데이터베이스가 아니라 애플리케이션의 커넥션 풀에 있습니다. 커넥션 풀이 소진(connection pool exhaustion)되면 CPU와 메모리에 여유가 있어도 API 응답이 느려지거나 멈춥니다.
오류 메시지는 DB에서 확인하지만, 커넥션을 쥐고 놓지 않는 주체는 애플리케이션입니다. 그래서 DB 지표만 들여다봐서는 원인을 찾기 어렵고, 애플리케이션이 커넥션을 어떻게 쓰고 있는지부터 확인해야 하는 경우가 많습니다.
최근에는 LLM(Large Language Model, 대규모 언어 모델)과 RAG(Retrieval-Augmented Generation)를 사용하는 AI 애플리케이션이 늘면서 이런 문제가 더 자주 발생하고 있습니다. 느린 AI 호출이 커넥션을 오래 붙잡고, 자동 확장(auto scaling) 때는 한꺼번에 많은 커넥션이 만들어지기 때문입니다.
Too many connections는 단순히 "사용자가 많아서" 발생하는 오류가 아닙니다. 대부분은 커넥션이 예상보다 오래 유지되거나, 애플리케이션이 데이터베이스가 감당할 수 있는 수보다 많은 연결을 동시에 요구하면서 발생합니다.
대표적인 원인은 다음 다섯 가지입니다.
idle in transaction)이 가운데 최근 특히 많이 늘어난 것이 다섯 번째 사례입니다.
커넥션 풀 소진은 오래전부터 존재했던 문제지만, AI 애플리케이션에서는 그 위험이 더 커집니다. 대표적인 이유는 다음 세 가지입니다.
요청 하나가 수 초 동안 LLM 응답을 기다리는 동안 DB 트랜잭션이 함께 유지되면 커넥션도 그 시간 동안 반환되지 않습니다. 원래 수십 밀리초 만에 반납되던 커넥션이 초 단위로 점유되면서 다른 요청이 사용할 커넥션이 부족해질 수 있습니다.
RAG는 질문마다 벡터 검색과 메타데이터 조회를 반복하고, AI 에이전트는 추론 과정에서 여러 도구를 호출하며 데이터베이스를 여러 차례 접근합니다. 일반적인 CRUD 애플리케이션보다 요청당 커넥션 사용량이 커질 가능성이 높습니다.
트래픽 증가로 애플리케이션 인스턴스가 늘어나면 각 인스턴스가 커넥션 풀을 따로 생성합니다.
예를 들어 인스턴스가 10개이고 인스턴스당 커넥션 풀이 20개라면 데이터베이스는 최대 200개의 연결을 감당해야 합니다. 데이터베이스의 max_connections보다 이 값이 커지면 결국 Too many connections 오류가 발생합니다.
즉 AI 워크로드는 긴 커넥션 점유 시간, 높은 동시성, 자동 확장에 따른 커넥션 증가가 동시에 발생하기 때문에 기존 애플리케이션보다 커넥션 풀이 훨씬 빠르게 고갈될 수 있습니다.
Too many connections의 원인을 이해하려면 먼저 커넥션 풀이 어떻게 동작하는지 알아둘 필요가 있습니다.
애플리케이션이 데이터베이스에 쿼리를 보낼 때마다 새로운 연결을 만들면 TCP 핸드셰이크, 인증, 세션 초기화 같은 비용이 반복됩니다. 경우에 따라서는 쿼리 실행보다 연결을 만드는 시간이 더 오래 걸리기도 합니다.
커넥션 풀(Connection Pool)은 이러한 비용을 줄이기 위해 데이터베이스 연결을 미리 여러 개 생성해 두고 재사용하는 방식입니다. 요청이 들어오면 풀에서 커넥션을 하나 꺼내 사용하고, 작업이 끝나면 다시 반납합니다.
공용 자전거 대여소를 떠올리면 이해하기 쉽습니다. 자전거(커넥션)가 모두 대여 중이라면 다른 사람은 반납될 때까지 기다려야 합니다. 반납이 늦어지거나 일부 자전거가 돌아오지 않으면 결국 대여소가 마비되는데, 이것이 바로 커넥션 풀 소진(connection pool exhaustion) 입니다.
커넥션 풀은 특정 언어나 프레임워크의 기능이 아니라 데이터베이스를 사용하는 거의 모든 환경에 존재합니다. 이름과 설정 키만 다를 뿐, 풀 크기·대기 타임아웃·유휴 커넥션·최대 수명 같은 개념은 공통입니다.
이 글은 가장 널리 쓰이는 Java/Spring Boot의 HikariCP를 주 예시로 설명하지만, 원인 분석과 해결 원칙은 위 어느 스택에도 그대로 적용됩니다. 또한 PostgreSQL 운영 환경에서는 애플리케이션 풀과 별개로 pgBouncer를 데이터베이스 앞단에 배치해 커넥션 수를 관리하는 경우도 많습니다.
커넥션 풀이 모두 사용 중이면 애플리케이션은 사용할 커넥션이 반환되기를 기다립니다. 이 대기 시간이 풀에 설정된 타임아웃을 넘으면 커넥션을 제시간에 얻지 못했다는 오류(HikariCP의 Connection is not available 등)가 발생합니다. 이름만 다를 뿐 어느 스택에서나 나타나는 증상입니다.
# HikariCP (Java)
HikariPool-1 - Connection is not available, request timed out after 30000ms
# SQLAlchemy (Python)
QueuePool limit of size 5 overflow 10 reached, connection timed out
# node-postgres (Node.js)
timeout exceeded when trying to connect이 오류는 데이터베이스 연결이 끊어졌다는 뜻이 아니라 커넥션 풀에서 사용할 커넥션을 제시간에 얻지 못했다는 의미입니다.
DB에서도 비슷한 증상이 나타납니다. MySQL은 Too many connections, PostgreSQL은 remaining connection slots are reserved와 같은 오류를 반환합니다.
커넥션을 기다리는 스레드가 계속 쌓이면 HTTP 요청 처리 스레드도 함께 대기하게 되고, 결국 API 전체의 응답이 멈출 수 있습니다. 이때 CPU와 메모리는 여전히 여유가 있는 경우가 많아 원인을 찾는 데 시간이 걸리기도 합니다.
Too many connections가 발생했다고 해서 무조건 커넥션 풀 크기만 늘려서는 안 됩니다. 먼저 애플리케이션 커넥션 풀 상태와 DB 세션 상태를 함께 확인해 원인을 구분해야 합니다. 커넥션을 쥔 쪽은 애플리케이션이라, 순서상 풀 상태부터 확인하면 원인을 더 빨리 좁힐 수 있습니다.
언어를 막론하고 대부분의 커넥션 풀은 활성(active), 유휴(idle), 대기(pending) 커넥션 수를 메트릭으로 제공합니다. 예를 들어 HikariCP는 Spring Boot Actuator로 hikaricp.connections.pending, SQLAlchemy는 풀 객체의 checkedout() 값으로 사용 중인 커넥션 수를 확인할 수 있습니다. 사용하는 풀이 어떤 메트릭을 노출하는지 먼저 파악해 두면 됩니다.
pending이 지속적으로 증가active가 항상 최대치에 가까움idle이 거의 없음이런 상태라면 커넥션 풀이 부족하거나 커넥션이 오래 점유되고 있을 가능성이 큽니다.
애플리케이션 메트릭만으로는 어떤 세션이 커넥션을 점유하는지 알기 어렵습니다. 이때는 DB에서 현재 세션 상태를 직접 확인합니다.
PostgreSQL이라면 pg_stat_activity로 상태별 세션 수와 오래 열린 트랜잭션을 확인할 수 있습니다.
특히 idle in transaction 상태가 많다면 트랜잭션을 시작한 뒤 LLM 호출이나 외부 API 요청처럼 시간이 오래 걸리는 작업을 수행하고 있을 가능성이 있습니다.
MySQL에서는 SHOW PROCESSLIST로 오래 유지되는 세션을 확인할 수 있습니다.
활성 세션이 많다는 이유만으로 문제가 있다고 단정할 수는 없습니다. 정상적인 피크 시간에도 활성 세션은 증가할 수 있기 때문입니다. 대신 다음과 같은 현상이 함께 나타나는지 확인하는 것이 중요합니다.
idle in transaction 세션이 계속 증가pending 커넥션이 지속적으로 증가이 세 가지가 동시에 나타난다면 커넥션 풀이 부족하기보다 트랜잭션이 오래 유지되거나 커넥션이 반환되지 않는 문제를 먼저 의심하는 것이 좋습니다.
풀 크기 조정은 원인을 제거한 뒤 마지막에 손대는 것이 좋습니다. 커넥션을 오래 점유하는 원인을 먼저 없앤 다음, 필요할 때만 풀 크기나 타임아웃을 조정합니다.
가장 효과가 큰 방법입니다. 특히 AI 애플리케이션에서는 LLM 호출, 외부 API 요청, 긴 후처리를 DB 트랜잭션 안에서 수행하지 않는 것이 중요합니다.
DB 작업은 필요한 쿼리만 짧은 트랜잭션으로 감싸고, 시간이 오래 걸리는 작업은 트랜잭션 밖에서 실행합니다. 핵심은 트랜잭션 경계 안에 느린 호출을 넣지 않는 것입니다. 예를 들어 Spring Boot라면 @Transactional 범위가, Python이라면 세션·with 블록 범위가 LLM 호출이나 외부 API 요청까지 감싸고 있지 않은지 먼저 확인합니다.
커넥션이 반환되지 않으면 풀에서는 계속 "사용 중"으로 인식합니다. 이런 상태가 반복되면 실제 사용량보다 훨씬 빨리 커넥션 풀이 소진됩니다.
수동 JDBC라면 try-with-resources를 사용하고, ORM이나 프레임워크를 사용한다면 트랜잭션 종료 시 커넥션이 정상적으로 반환되는지 확인합니다.
maximumPoolSize) 조정하기커넥션 누수나 장기 트랜잭션 문제가 없다면 풀 크기를 조정합니다. 설정 키는 스택마다 다릅니다. HikariCP는 maximumPoolSize, SQLAlchemy는 pool_size와 max_overflow, Go database/sql은 SetMaxOpenConns로 최대 커넥션 수를 정합니다.
풀 크기를 정하는 원칙은 언어와 무관하게 같습니다. 예로 HikariCP 기본값은 10이지만 모든 서비스에 적합한 값은 아니며, HikariCP Wiki는 (코어 수 × 2) + 유효 스핀들 수를 출발점으로 제안합니다. 어느 풀이든 이런 경험식은 시작점일 뿐이고, 실제 운영에서는 메트릭을 보며 조정합니다.
풀을 무작정 늘리면 DB의 max_connections를 더 빨리 소진할 수 있으므로 애플리케이션과 DB를 함께 고려해야 합니다. 예를 들어 Spring Boot(HikariCP)에서는 다음과 같이 설정합니다.
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000connectionTimeout은 짧게 설정하기커넥션 풀이 부족할 때 connectionTimeout을 너무 길게 두면 요청 스레드가 오래 대기하면서 장애가 더 커질 수 있습니다.
일반적으로는 3~5초 정도로 설정하고, 필요하면 애플리케이션에서 재시도(retry) 정책으로 보완하기도 합니다.
Kubernetes나 Auto Scaling 환경에서는 인스턴스마다 커넥션 풀이 생성됩니다.
예를 들어 데이터베이스의 max_connections가 100이고 애플리케이션 인스턴스가 최대 8개까지 늘어난다면, 인스턴스당 풀 크기를 10 정도로 제한해야 전체 커넥션 수가 DB 한도를 넘지 않습니다.
8 instances × 10 connections = 80여유 커넥션을 남겨 두어야 관리용 세션이나 장애 상황에서도 안정적으로 운영할 수 있습니다.
전체 커넥션 수가 계속 증가한다면 애플리케이션의 풀을 키우기보다 PostgreSQL 앞단에 pgBouncer를 두는 것이 효과적입니다.
pool_mode = transaction을 사용하면 적은 서버 커넥션으로 더 많은 클라이언트를 처리할 수 있습니다.
다만 transaction 모드는 Prepared Statement나 SET 같은 세션 상태를 유지하지 않으므로 애플리케이션이 이를 사용하지 않는지 확인해야 합니다.
pool_mode = transaction
max_client_conn = 200
default_pool_size = 20
reserve_pool_size = 5
같은 Too many connections라도 원인에 따라 접근 방법은 달라집니다.
원인을 해결하지 않은 채 풀 크기만 늘리면 장애가 발생하는 시점만 늦출 뿐 근본적인 문제는 해결되지 않습니다.
당장은 오류가 사라진 것처럼 보이지만 근본 원인은 그대로입니다. 커넥션 누수나 오래 유지되는 트랜잭션을 먼저 없애고, max_connection는 그다음에 조정하는 것이 순서입니다. max_connection만 키우면 장애가 터지는 시점을 미룰 뿐입니다.
아닙니다. 커넥션 풀은 데이터베이스를 호출하는 애플리케이션 쪽에 있습니다(HikariCP, SQLAlchemy 등). 오류 메시지는 DB에서 확인하지만 커넥션을 쥐고 있는 주체는 애플리케이션이므로, 진단도 애플리케이션 풀 상태부터 시작하는 편이 빠릅니다. 그래서 DB 대시보드만 보면 CPU·메모리가 멀쩡해 원인을 놓치기 쉽습니다.
아닙니다. 풀을 키우면 각 인스턴스가 더 많은 커넥션을 열어 DB의 max_connections를 더 빨리 소진합니다. 특히 자동 확장 환경에서는 (인스턴스 수 × 풀 크기)가 DB 한도를 넘지 않도록 계산해야 합니다.
Too many connections는 단순히 데이터베이스가 느려졌다는 뜻이 아닙니다. 대부분은 커넥션 점유 시간이 길거나 동시 연결 요구가 DB 한도를 넘으면서 생기는 문제입니다.
문제가 생기면 먼저 애플리케이션 풀 메트릭(HikariCP·SQLAlchemy 등) 과 DB 세션 상태(pg_stat_activity, SHOW PROCESSLIST) 를 함께 확인해 원인을 구분하는 것이 중요합니다. 그다음 트랜잭션 범위, 커넥션 반환, 풀 크기, 자동 확장 환경을 순서대로 점검하면 대부분의 문제를 해결할 수 있습니다.
특히 LLM과 RAG를 사용하는 AI 애플리케이션은 커넥션 점유가 길어지기 쉬워, 트랜잭션 범위와 커넥션 반환을 먼저 점검하면 도움이 됩니다.
이 문제는 애플리케이션과 데이터베이스 양쪽을 함께 봐야 원인이 또렷해집니다. 어떤 트랜잭션이 느려서 커넥션을 오래 붙잡는지(LLM 호출이나 외부 API 대기 구간 포함)는 와탭 APM처럼 요청 단위 트랜잭션 트레이스에서 DB 연결 대기 구간과 커넥션 풀 사용량(Pool·Active)을 함께 보여주는 도구로, 어떤 세션이 커넥션과 락을 점유하고 있는지는 와탭 데이터베이스 모니터링처럼 액티브 세션과 락 대기를 함께 확인하는 도구로 볼 수 있습니다. 느려진 트랜잭션과 커넥션을 쥔 세션을 각각 짚어 주는 셈입니다.