Sửa lỗi PostgreSQL FATAL: remaining connection slots are reserved for non-replication superuser connections

intermediate🐘 PostgreSQL2026-03-21| PostgreSQL 12–16, Linux/Ubuntu/Debian, mọi ứng dụng sử dụng kết nối DB liên tục

Error Message

FATAL: remaining connection slots are reserved for non-replication superuser connections
#postgresql#max-connections#connection-pool#pg_hba#performance

Lỗi gặp phải

FATAL: remaining connection slots are reserved for non-replication superuser connections

PostgreSQL đã chạm giới hạn max_connections và ngừng chấp nhận kết nối mới từ người dùng thông thường. Hệ thống luôn giữ lại một số slot — mặc định là 3 — dành riêng cho đăng nhập superuser. Nhờ vậy, quản trị viên vẫn có thể kết nối và xử lý sự cố. Tất cả người dùng khác sẽ gặp lỗi này.

Nguyên nhân gốc rễ

Mỗi PostgreSQL instance có giới hạn cứng về số kết nối đồng thời, được kiểm soát bởi tham số max_connections trong postgresql.conf. Mặc định là 100. Khi ứng dụng chiếm hết các slot đó (trừ 3 slot dành cho superuser), yêu cầu kết nối tiếp theo bị từ chối ngay lập tức — không có hàng chờ, không có thời gian chờ.

Các nguyên nhân phổ biến:

  • Không có connection pooler — mỗi thread hoặc process của ứng dụng mở một kết nối riêng
  • Connection leak — code mở kết nối nhưng không bao giờ đóng lại
  • Tăng đột biến lưu lượng — tải tăng đột ngột làm cạn kiệt pool nhanh hơn tốc độ tái sử dụng kết nối
  • Nhiều instance ứng dụng dùng chung một DB mà không có pooling (ví dụ: 10 Node.js pod × 10 kết nối mỗi pod = 100 kết nối ngay lập tức)
  • max_connections đặt quá thấp so với tải thực tế

Bước 1: Kiểm tra trạng thái kết nối hiện tại

Vì các slot dành cho superuser vẫn còn, hãy kết nối với tư cách postgres:

psql -U postgres -d mydb

Xem chi tiết những gì đang chiếm các kết nối đó:

SELECT count(*), state, usename, application_name
FROM pg_stat_activity
GROUP BY state, usename, application_name
ORDER BY count DESC;

Sau đó kiểm tra mức độ gần đến giới hạn:

SELECT max_conn, used, res_for_super, max_conn - used - res_for_super AS available
FROM
  (SELECT count(*) used FROM pg_stat_activity) t1,
  (SELECT setting::int res_for_super FROM pg_settings WHERE name = 'superuser_reserved_connections') t2,
  (SELECT setting::int max_conn FROM pg_settings WHERE name = 'max_connections') t3;

Nếu available bằng 0 hoặc âm — đó chính là nguyên nhân đã được xác nhận.

Cách sửa 1: Ngắt kết nối nhàn rỗi (giải pháp tức thời)

Phát hiện nhiều kết nối idle hoặc idle in transaction? Hãy ngắt chúng ngay:

-- Kill idle connections sitting for 10+ minutes
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
  AND state_change < NOW() - INTERVAL '10 minutes'
  AND usename != 'postgres';

Xử lý riêng các kết nối idle in transaction — chúng đang giữ lock và gây ảnh hưởng nhiều hơn:

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND state_change < NOW() - INTERVAL '5 minutes';

Cách này giúp bạn có thêm thời gian xử lý. Nhưng sẽ không giải quyết được triệt để nếu không xử lý nguyên nhân gốc rễ.

Cách sửa 2: Tăng max_connections

Trước tiên, tìm đường dẫn file cấu hình:

SHOW config_file;

Sau đó chỉnh sửa postgresql.conf:

# postgresql.conf
max_connections = 200          # was 100
reserved_connections = 3       # keep some for superuser

Khởi động lại để áp dụng:

sudo systemctl restart postgresql

Trước khi tăng lên 500: mỗi kết nối PostgreSQL tiêu tốn khoảng 5–10 MB RAM. Với 200 kết nối, đó là 1–2 GB chỉ riêng cho overhead kết nối — chưa kể đến dữ liệu mà các query của bạn xử lý. Nếu bạn thường xuyên chạm mức 100 kết nối, vấn đề gần như chắc chắn là thiếu pooling, chứ không phải giới hạn quá thấp.

Khi thay đổi max_connections, hãy xem lại cả shared_bufferswork_mem — nhiều kết nối hơn đồng nghĩa với áp lực bộ nhớ lớn hơn trên toàn hệ thống.

Cách sửa 3: Triển khai PgBouncer (giải pháp bền vững)

PgBouncer hoạt động như một proxy giữa ứng dụng và PostgreSQL. Ứng dụng của bạn nghĩ rằng nó đang kết nối trực tiếp với database — nhưng thực ra PgBouncer đang tái sử dụng một tập nhỏ các kết nối DB thực sự cho hàng nghìn yêu cầu từ ứng dụng.

sudo apt install pgbouncer

Cấu hình /etc/pgbouncer/pgbouncer.ini:

[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb

[pgbouncer]
listen_port = 6432
listen_addr = 127.0.0.1
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction          ; hoặc session
max_client_conn = 1000           ; kết nối phía ứng dụng
default_pool_size = 20           ; kết nối PostgreSQL thực tế
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
log_connections = 1
log_disconnections = 1

Tạo file /etc/pgbouncer/userlist.txt với hash MD5 của thông tin đăng nhập từng người dùng:

"myuser" "md5passwordhash"

Tạo hash:

echo -n "passwordusername" | md5sum

Kích hoạt và khởi động PgBouncer:

sudo systemctl enable pgbouncer
sudo systemctl start pgbouncer

Trỏ ứng dụng của bạn đến cổng 6432 thay vì 5432. Với cấu hình này, max_connections của PostgreSQL có thể giảm xuống còn 25–50 trong khi PgBouncer xử lý 1000+ kết nối từ ứng dụng. Nhiều team chạy 500 kết nối ứng dụng đồng thời với chỉ 20 kết nối thực tế đến PostgreSQL.

Cách sửa 4: Connection pooling ở tầng ứng dụng

Trước khi dùng đến PgBouncer, hãy kiểm tra xem framework của bạn có hỗ trợ pooling sẵn không — và liệu bạn có đang thực sự dùng nó không.

Node.js (thư viện pg):

const { Pool } = require('pg');
const pool = new Pool({
  max: 10,              // max connections in pool
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});
// Dùng pool.query() thay vì client.connect() cho mỗi request

Python (SQLAlchemy):

from sqlalchemy import create_engine
engine = create_engine(
    'postgresql://user:pass@localhost/mydb',
    pool_size=10,
    max_overflow=20,
    pool_timeout=30,
    pool_recycle=1800,
)

Django: đặt CONN_MAX_AGE để tái sử dụng kết nối giữa các request thay vì mở lại mỗi lần:

DATABASES = {
    'default': {
        ...
        'CONN_MAX_AGE': 60,  # giây; 0 = đóng sau mỗi request
    }
}

Xác nhận đã sửa xong

Theo dõi số lượng kết nối theo thời gian thực sau khi áp dụng thay đổi:

-- Chạy định kỳ hoặc tích hợp vào hệ thống monitoring
SELECT count(*) AS total_connections,
       count(*) FILTER (WHERE state = 'active') AS active,
       count(*) FILTER (WHERE state = 'idle') AS idle,
       count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_tx
FROM pg_stat_activity
WHERE datname = 'mydb';

Đang dùng PgBouncer? Kiểm tra thống kê pool trực tiếp:

psql -p 6432 -U pgbouncer pgbouncer -c 'SHOW POOLS;'

Xem giá trị cl_active so với sv_active. Nếu cl_active là 200 và sv_active là 15, PgBouncer đang hoạt động đúng như kỳ vọng.

Phòng ngừa

  • Cảnh báo khi số lượng kết nối trong pg_stat_activity vượt 80% max_connections — đừng chờ đến khi đạt 100%
  • Thêm connection pooling từ ngay đầu dự án. Việc thêm vào sau cho một ứng dụng đang chạy trên production thực sự rất phức tạp
  • Đặt idle_in_transaction_session_timeout = '5min' trong postgresql.conf để tự động ngắt các transaction bị treo
  • Đặt statement_timeout để cắt các query chạy quá lâu trước khi chúng giữ kết nối vô thời hạn
  • Trên Kubernetes: mỗi pod có pool riêng, nên tổng kết nối = số pod × pool_size. Với 20 pod mỗi pod 10 kết nối, bạn đã dùng hết 200 slot. PgBouncer là bắt buộc trong môi trường này

Related Error Notes