Skip to content

Connection Pooling

Optimize database connections for performance and scalability.

Database connections are expensive resources:

ProblemImpact
Connection overhead~100-300ms per new connection
Memory usage~10MB per PostgreSQL connection
Connection limitsPostgreSQL default: 100 connections

Connection pooling solves these by maintaining reusable connections.

Mantis components include built-in connection pooling.

Mandible is the only component with a database pool. The [database] fields are url, max_connections, min_idle, and connection_timeout_secs:

mandible.toml
[database]
url = "postgres://mantis:password@localhost:5432/mantis"
max_connections = 20 # Maximum pool size
min_idle = 5 # Minimum idle connections
connection_timeout_secs = 30 # Connection acquisition timeout
Terminal window
# Mandible (the connection URL; layered overrides use the MANDIBLE__DATABASE__ prefix)
DATABASE_URL=postgres://mantis:password@localhost:5432/mantis
MANDIBLE__DATABASE__MAX_CONNECTIONS=20
MANDIBLE__DATABASE__MIN_IDLE=5
MANDIBLE__DATABASE__CONNECTION_TIMEOUT_SECS=30

Formula:

pool_size = (core_count * 2) + spinning_disk_count

For SSD storage:

pool_size = core_count * 2

Example for 4-core server with SSD:

pool_size = 4 * 2 = 8
DeploymentMandible pool
Development5
Small (< 100 targets)10
Medium (< 1000 targets)20
Large (1000+ targets)50

(Only Mandible connects to PostgreSQL, so it is the only pool to size.)

Ensure PostgreSQL can handle every Mandible instance’s pool:

max_connections >= (mandible_pool * mandible_instances) + overhead
Example:
max_connections >= (20 * 2) + 20 = 60
# postgresql.conf
max_connections = 200

For large deployments, use PgBouncer as an external connection pooler.

Terminal window
# Ubuntu/Debian
sudo apt install pgbouncer
# RHEL/Rocky/Alma
sudo dnf install pgbouncer
/etc/pgbouncer/pgbouncer.ini
[databases]
mantis = host=localhost port=5432 dbname=mantis
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
# Pool settings
pool_mode = transaction # transaction | session | statement
default_pool_size = 50 # Connections per database/user
max_client_conn = 1000 # Max client connections
min_pool_size = 10 # Minimum pool size
reserve_pool_size = 5 # Emergency connections
# Connection limits
max_db_connections = 100 # Max connections to database
# Timeouts
server_connect_timeout = 15
server_idle_timeout = 600
server_lifetime = 3600
client_idle_timeout = 0 # 0 = disabled
# Logging
log_connections = 1
log_disconnections = 1
stats_period = 60

Create userlist:

Terminal window
# Generate password hash
psql -U postgres -c "SELECT concat('\"mantis\" \"', passwd, '\"') FROM pg_shadow WHERE usename='mantis'"
# Add to userlist.txt
echo '"mantis" "SCRAM-SHA-256$..."' >> /etc/pgbouncer/userlist.txt
ModeDescriptionUse Case
sessionConnection per client sessionLegacy apps
transactionConnection per transactionMost applications
statementConnection per statementSimple queries
Terminal window
sudo systemctl enable pgbouncer
sudo systemctl start pgbouncer

Update application configuration:

mandible.toml
[database]
url = "postgres://mantis:password@localhost:6432/mantis"
max_connections = 50 # Can be higher with PgBouncer

The /api/v1/health endpoint performs a database connectivity check and returns {"status":"ok"} (200) when healthy or {"status":"unavailable"} (503) when the database is unreachable. It does not expose pool internals. For a pure process-liveness check that never gates on external dependencies, use /api/v1/health/live instead (always 200 while the process is running). Monitor pool saturation from the PostgreSQL side:

-- Active vs idle connections for the Mandible DB user
SELECT state, count(*) FROM pg_stat_activity
WHERE usename = 'mantis' GROUP BY state;
-- Current connections
SELECT count(*), state
FROM pg_stat_activity
WHERE datname = 'mantis'
GROUP BY state;
-- Connections by application
SELECT application_name, count(*)
FROM pg_stat_activity
WHERE datname = 'mantis'
GROUP BY application_name;
-- Long-running connections
SELECT pid, usename, application_name,
now() - backend_start as connection_age,
state, query
FROM pg_stat_activity
WHERE datname = 'mantis'
ORDER BY backend_start;
Terminal window
# Connect to PgBouncer admin console
psql -p 6432 -U pgbouncer pgbouncer
# Show pools
SHOW POOLS;
# Show clients
SHOW CLIENTS;
# Show servers (actual PostgreSQL connections)
SHOW SERVERS;
# Show statistics
SHOW STATS;

Symptoms:

  • “too many connections” errors
  • Slow response times
  • Connection timeouts

Diagnosis:

SELECT count(*) FROM pg_stat_activity;
SELECT * FROM pg_stat_activity WHERE state = 'idle' ORDER BY backend_start;

Solutions:

  1. Increase max_connections in PostgreSQL
  2. Reduce pool sizes
  3. Use PgBouncer
  4. Identify connection leaks

Symptoms:

  • Pool exhaustion over time
  • Idle connections accumulating

Diagnosis:

-- Find idle connections
SELECT pid, usename, application_name,
now() - state_change as idle_time
FROM pg_stat_activity
WHERE state = 'idle'
ORDER BY idle_time DESC;

Solutions:

  1. Lower min_idle so fewer idle connections are held open, or use PgBouncer’s server_idle_timeout to recycle idle server connections
  2. Check application code for unclosed connections
  3. Enable connection lifecycle logging

Symptoms:

  • High latency on first requests
  • Timeout during connection

Diagnosis:

Terminal window
# Test connection time
time psql -h localhost -U mantis -c "SELECT 1"

Solutions:

  1. Increase min_idle to pre-warm the pool
  2. Check network latency to database
  3. Reduce SSL negotiation overhead (connection reuse)

Client authentication failed:

Terminal window
# Check userlist.txt format
cat /etc/pgbouncer/userlist.txt
# Verify auth type matches PostgreSQL
grep auth_type /etc/pgbouncer/pgbouncer.ini

No free connections:

Terminal window
# Check pool status
psql -p 6432 pgbouncer -c "SHOW POOLS"
# Increase pool size or max_db_connections
  • Don’t over-provision (wastes memory)
  • Don’t under-provision (causes contention)
  • Monitor and adjust based on metrics

The [database] section accepts only url, max_connections, min_idle, and connection_timeout_secs. Set the acquisition timeout here; for idle-recycling and max-lifetime behavior use an external pooler (PgBouncer).

[database]
connection_timeout_secs = 30 # Connection acquisition timeout

Built-in health checks verify database connectivity:

Terminal window
curl https://localhost:3000/api/v1/health/ready

Mantis does not currently export pool-utilization gauges to Prometheus. Monitor pool saturation from the PostgreSQL side using pg_stat_activity:

-- Count active connections for the Mantis DB user
SELECT state, count(*) FROM pg_stat_activity
WHERE usename = 'mantis' GROUP BY state;

Alert when the active connection count approaches the configured max_connections value in your PostgreSQL instance.

For PgBouncer, transaction mode is most efficient:

pool_mode = transaction