지금 이 에러로 검색해서 들어왔죠?
FATAL: sorry, too many clients already — 이 원문을 검색창에 붙여넣고 들어오셨다면, 지금 서비스가 DB에 붙지 못하고 있는 상황일 겁니다. 개념 설명은 뒤로 미루겠습니다. 이 글은 복구 순서입니다. 스크롤을 내리는 순서대로 명령을 치면 장애가 끝납니다.
진행 순서는 이렇습니다.
- 30초 판정표로 원인 유형(스파이크 / 누수 / 설정 미스)을 먼저 가릅니다.
- 복붙 진단 SQL로 현재 상태를 확인하고, 안전하게 응급 종료합니다.
- 원인 유형별 영구 조치로 넘어갑니다.
- pgbouncer 도입 판단과 재발방지 알림으로 마무리합니다.
한 가지만 먼저 짚고 갑니다. MySQL의 ERROR 1040: Too many connections는 대부분 max_connections 단순 상향으로 해결하지만, PostgreSQL은 커넥션 1개당 OS 프로세스 1개를 띄우는 프로세스 모델이라 무작정 상한만 올리면 메모리가 터집니다. 그래서 PostgreSQL은 풀링(pooling)이 핵심입니다. MySQL 사용자라면 별도의 ERROR 1040 런북을 참고하시고, PostgreSQL은 이 글에서 끝냅니다.
적용 범위: PostgreSQL 10 이상, RDS/Aurora PostgreSQL, 자체 호스팅 리눅스 환경 공통. 명령은 psql 접속 후 실행 기준입니다.
먼저 접속부터: 관리 세션 확보
일반 커넥션이 다 차서 psql조차 못 붙을 수 있습니다. PostgreSQL은 superuser_reserved_connections(기본 3개)를 슈퍼유저용으로 예약해 둡니다. 슈퍼유저 계정으로 접속하면 이 예약분을 통해 들어갈 수 있습니다.
# 슈퍼유저(postgres)로 접속 시도 — 예약 커넥션 사용
psql -U postgres -h <host> -p 5432 -d postgres접속되면 다음 판정표로 넘어갑니다. 이것마저 안 되면 예약 슬롯까지 소진된 심각한 상황이니, 애플리케이션 인스턴스를 일부 내려 커넥션을 강제로 반납시킨 뒤 재접속하세요.
30초 판정표: 내 상황은 어느 쪽인가
지금 상황의 시간적 패턴을 보면 원인이 갈립니다. 아래 표에서 내 증상을 찾고 해당 섹션으로 점프하세요.
| 증상 신호 | 판정 | 바로 갈 섹션 |
|---|---|---|
| 배포·배치·트래픽 폭증 직후 몇 분간 터지다 자연 회복 | ① 순간 스파이크 | 응급 복구 후 → max_connections/풀 사이징 |
시간이 갈수록 활성 커넥션 우상향, idle in transaction이 다수 쌓임 | ② 커넥션 누수 | idle in transaction 종료 + ORM 트랜잭션 점검 |
| DB 재시작 직후부터 곧바로 상한에 근접 | ③ 설정 미스 | 풀 사이즈 vs max_connections 계산 |
핵심 구분법: 누수(②)는 시간이 지날수록 나빠지고, 스파이크(①)는 순간에 몰렸다 풀립니다. 설정 미스(③)는 처음부터 잘못돼 있습니다. 어느 쪽인지 애매하면 일단 다음 진단 SQL을 치면 데이터가 답을 줍니다.
즉시 진단 & 응급 복구 SQL
1단계 — state별 커넥션 분포 확인
가장 먼저 이걸 칩니다. 커넥션이 어떤 상태로 몰려 있는지 한눈에 보입니다.
-- 현재 커넥션을 상태별로 집계 (많은 순)
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count DESC;결과 해석 분기:
active가 대부분 → 실제 쿼리가 몰린 스파이크(①). 느린 쿼리·락 대기를 의심.idle in transaction이 많다 → 누수(②) 확정. 트랜잭션을 열고 커밋/롤백을 안 한 세션들입니다. 아래 2단계로.idle이 많다 → 풀은 잡고 있는데 안 쓰는 상태. 풀 사이즈 과다 설정(③) 가능성.
2단계 — 오래 방치된 idle in transaction 찾기
idle in transaction은 PostgreSQL 커넥션 고갈의 대표 범인입니다. 트랜잭션을 연 채 애플리케이션이 커밋을 안 하면 그 커넥션은 계속 점유되고, 다른 트랜잭션의 정리(VACUUM)까지 방해합니다.
-- 오래 방치된 idle in transaction 세션을 오래된 순으로
SELECT pid,
usename,
state,
now() - state_change AS idle_dur,
query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY idle_dur DESC;idle_dur이 수 분 이상인 세션들이 보이면, query 컬럼으로 어느 애플리케이션 코드가 트랜잭션을 안 닫았는지 역추적할 단서를 얻습니다.
3단계 — 안전 종료 (반드시 2단계 워크플로우)
⚠️ pg_terminate_backend를 바로 실행하지 마세요. 먼저 SELECT로 종료 대상을 눈으로 확인한 뒤 실행합니다. 시스템 프로세스나 내 세션을 죽이면 안 됩니다.
-- (1) 종료 대상 먼저 확인: 5분 넘게 방치된 idle in transaction, 내 세션 제외
SELECT pid, usename, now() - state_change AS idle_dur, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - state_change > interval '5 minutes'
AND pid <> pg_backend_pid();목록이 예상과 맞는지 확인했다면, 같은 조건으로 종료합니다.
-- (2) 확인된 대상만 종료
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - state_change > interval '5 minutes'
AND pid <> pg_backend_pid();pg_backend_pid() 조건이 내 psql 세션을 보호하는 장치입니다. 종료 후 다시 1단계 집계 SQL을 쳐서 커넥션이 풀렸는지 확인하세요. 커넥션이 반납되면 애플리케이션이 다시 붙기 시작합니다. 여기까지가 응급 복구입니다. 서비스가 돌아왔다면 한숨 돌리고 아래 근본 조치로 넘어가세요.
근본 원인별 영구 해결
(a) max_connections 상향 — 메모리 트레이드오프부터 계산
먼저 현재 값을 확인합니다.
SHOW max_connections;
SHOW superuser_reserved_connections;
SHOW work_mem;max_connections를 올리기 전에 반드시 짚어야 할 함정이 있습니다. PostgreSQL은 정렬·해시 연산마다 세션당 work_mem을 할당하며, 복잡한 쿼리는 한 세션이 work_mem을 여러 번 쓸 수 있습니다. 워스트케이스 추정은 대략 이렇습니다.
워스트케이스 추정 메모리 ≈ work_mem × max_connections × (쿼리당 정렬/해시 노드 수)
예) work_mem 16MB × max_connections 200 = 약 3.2GB
(쿼리 하나가 정렬을 여러 번 하면 이 값의 몇 배까지)즉 max_connections를 무작정 200 → 500으로 올리면 트래픽 몰릴 때 OOM으로 DB 프로세스가 죽습니다. RDS/Aurora는 max_connections가 인스턴스 클래스(메모리)에 종속돼 파라미터 그룹으로 관리되므로, 스펙을 넘겨 잡으면 안 됩니다.
결론: 상한을 조금 올리는 건 응급책이고, 근본 해법은 커넥션 수 자체를 줄이는 풀링입니다. 상향이 불가피하다면 superuser_reserved_connections를 최소 3~5로 확보해 관리 접속 경로는 남겨두세요.
(b) 커넥션 풀 설정 오류 — 인스턴스 수 × 풀 사이즈 계산
재시작 직후부터 상한 근접(③)의 전형적 원인은 애플리케이션 풀 총합이 max_connections를 초과하는 것입니다. 계산부터 하세요.
애플리케이션 인스턴스 수 × 인스턴스당 최대 풀 사이즈 ≤ max_connections − reserved
예) 파드 20개 × HikariCP maximumPoolSize 10 = 200
→ max_connections 200이면 이미 여유 0, 관리 접속조차 불가서버리스·컨테이너 오토스케일링 환경에서 파드가 40개, 80개로 늘면 이 곱셈값이 폭증해 커넥션이 순식간에 고갈됩니다. 이게 요즘 이 에러가 늘어나는 핵심 배경입니다.
| 항목 | HikariCP (Java) | psycopg_pool (Python) |
|---|---|---|
| 최대 커넥션 | maximumPoolSize | max_size |
| 최소 유지 | minimumIdle | min_size |
| 누수 감지 | leakDetectionThreshold(ms) | 반납 누락 시 로그 확인 |
| 권장 시작점 | 인스턴스당 (코어수×2) 내외 | max_size 보수적으로 |
HikariCP는 leakDetectionThreshold(예: 30000ms)를 켜두면 반납 안 된 커넥션을 로그로 잡아줍니다. 누수 코드를 찾는 데 결정적입니다.
(c) ORM 트랜잭션 미종료 누수 패턴
idle in transaction이 계속 쌓이는 근본 원인은 코드에 있습니다. 자주 보고되는 패턴은 다음과 같습니다.
- 트랜잭션 블록 안에서 외부 API 호출·긴 연산을 하며 커넥션을 잡고 대기
- 예외 발생 시
rollback/close가 누락되는 경로 - 자동 커밋을 끄고(수동 트랜잭션) 커밋을 잊는 코드
- 세션 스코프를 요청 단위로 닫지 않는 웹 프레임워크 설정
대응: 트랜잭션은 짧게 열고 짧게 닫고, 외부 I/O는 트랜잭션 밖으로 빼세요. 서버 측 안전장치로 idle_in_transaction_session_timeout을 설정하면 방치된 트랜잭션을 자동 종료할 수 있습니다.
-- 방치된 트랜잭션을 5분 후 자동 종료 (세션/전역 설정 가능)
SET idle_in_transaction_session_timeout = '5min';pgbouncer 도입 의사결정표
인스턴스가 많아 커넥션 곱셈값을 감당할 수 없다면, 답은 커넥션 풀러입니다. pgbouncer(또는 RDS Proxy)가 애플리케이션과 DB 사이에서 커넥션을 재사용해, 수백 개의 클라이언트 요청을 소수의 실제 DB 커넥션으로 소화합니다. 모드 선택이 핵심입니다.
| 조건 | 권장 모드 | 이유·주의점 |
|---|---|---|
| 짧은 웹 트랜잭션이 대부분, 커넥션 재사용 극대화가 목표 | transaction 모드 | 효율 최고. 단, prepared statement·세션 변수(SET)·advisory lock이 세션에 종속되면 깨질 수 있음 |
prepared statement, 세션 변수, advisory lock, LISTEN/NOTIFY를 많이 씀 | session 모드 | 호환성 높음. 대신 풀 효율은 낮아 커넥션 절감 효과 축소 |
| 레거시·ORM 호환성이 불확실 | session 모드로 시작 | 안정화 후 transaction 모드 검토 |
도입 판단 조건: 애플리케이션 인스턴스 수가 가변적(오토스케일)이고, 인스턴스 수 × 풀 사이즈가 max_connections에 근접하기 시작했다면 도입 시점입니다. transaction 모드를 쓸 땐 애플리케이션 드라이버의 prepared statement 캐시 동작을 반드시 검증하세요(구성 확인 필요).
재발방지: 알림 쿼리
응급 복구로 끝내지 말고, 같은 장애를 미리 잡는 모니터링을 겁니다.
-- 활성+대기 커넥션이 max_connections의 80%를 넘으면 경보
SELECT count(*) AS current_conns,
current_setting('max_connections')::int AS max_conns,
round(100.0 * count(*) / current_setting('max_connections')::int, 1) AS pct
FROM pg_stat_activity
HAVING count(*) > current_setting('max_connections')::int * 0.8;-- idle in transaction이 10개 초과로 쌓이면 감지
SELECT count(*) AS idle_in_tx
FROM pg_stat_activity
WHERE state = 'idle in transaction'
HAVING count(*) > 10;이 두 쿼리를 모니터링 도구(예: Prometheus postgres_exporter, CloudWatch 커스텀 지표)에 주기 실행으로 걸어두면, 커넥션이 임계에 다가갈 때 터지기 전에 알림을 받습니다.
MySQL 계열에서 ERROR 1040: Too many connections를 만났다면 프로세스 모델과 대응이 다르므로 별도의 MySQL 커넥션 런북을 참고하세요.
자주 묻는 질문 (FAQ)
Q. max_connections만 올리면 안 되나요? A. 임시로는 됩니다. 하지만 PostgreSQL은 커넥션당 프로세스를 띄우고 세션당 work_mem을 할당하므로, 상한을 크게 올리면 트래픽 피크에 OOM 위험이 커집니다. 근본 해법은 커넥션 수를 줄이는 풀링(pgbouncer/RDS Proxy)입니다.
Q. pg_terminate_backend로 세션을 죽이면 데이터가 손상되나요? A. 해당 세션의 진행 중이던 트랜잭션은 롤백됩니다. 커밋되지 않은 작업만 취소되므로 데이터 정합성은 유지됩니다. 다만 종료 전 SELECT로 대상을 반드시 확인해 엉뚱한 세션을 죽이지 않도록 하세요.
Q. idle in transaction과 idle의 차이가 뭔가요?
A. idle은 트랜잭션 없이 다음 명령을 기다리는 정상 대기 상태입니다. idle in transaction은 트랜잭션을 연 채 다음 명령을 기다리는 상태로, 커넥션과 락을 계속 점유하고 VACUUM을 방해하므로 누수의 주범입니다.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.