
September 08, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
PostgreSQL has a hard limit on open connections. Each PHP-FPM worker, queue worker, and Horizon process can hold one. Under traffic, a Laravel app on PostgreSQL 18 exhausts max_connections fast. Database Connection Pooling with PgBouncer sits between your app and the database. It multiplexes hundreds of client sessions onto a smaller pool of real server connections. If you run booking systems or legal-tech portals on shared VPS hardware, this pattern is often the difference between stable uptime and midnight pager alerts. This guide covers install, pool modes, Laravel wiring, and the production mistakes I see repeatedly on real deployments.
For broader context on why pooling exists at all, see our database connection pooling explained overview first. It frames the problem before you touch PgBouncer config files.
DB_HOST at PgBouncer, and keep prepared statements disabled in PHP.What is Database Connection Pooling with PgBouncer and why does it matter?
PostgreSQL allocates roughly 5–10 MB of RAM per connection. A modest EC2 instance with 4 GB RAM cannot safely run 500 concurrent backends. PHP-FPM multiplies the problem. A pool of 50 workers plus 10 queue workers plus 5 schedulers already needs 65 connections before traffic spikes.
PgBouncer is a single-purpose connection pooler written in C. It accepts TCP connections from clients, assigns them to a fixed pool of PostgreSQL backends, and returns those backends to the pool when the client disconnects or the transaction ends. The database sees 20–50 connections. Your app sees no change except a different host and port.
On a production Laravel application I maintain, moving from direct PostgreSQL connections to PgBouncer eliminated recurring FATAL: sorry, too many clients already errors during peak booking hours. The app logic did not change. Only the connection path did.
Pooling complements other database performance work. Pair it with index tuning, query caching, and optionally read replicas when read load outgrows a single primary.
How do you install and configure PgBouncer on Ubuntu?
Most of my production stacks run Ubuntu 22.04 or 24.04 with Apache and PHP-FPM. PgBouncer installs from the standard repository and listens on port 6432 by default.
Install the package
sudo apt update
sudo apt install pgbouncer
sudo systemctl enable pgbouncer Confirm PostgreSQL 18 is reachable locally before you configure the pooler. Check your current connection count with:
sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_activity;" Edit pgbouncer.ini
The main config lives at /etc/pgbouncer/pgbouncer.ini. A minimal production-ready section looks like this:
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp_production
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 500
default_pool_size = 30
min_pool_size = 10
reserve_pool_size = 5
reserve_pool_timeout = 3
server_reset_query = DISCARD ALL
ignore_startup_parameters = extra_float_digits Create the user list file with credentials PgBouncer uses to authenticate clients and connect upstream:
sudo nano /etc/pgbouncer/userlist.txt "myapp_user" "SCRAM-SHA-256$4096:..." Generate the SCRAM hash from PostgreSQL or copy it from pg_authid. Never store plain-text passwords in this file.
Size the pool correctly
A practical starting formula for Laravel:
- default_pool_size = PHP-FPM workers + queue workers + 20% headroom
- max_client_conn = all possible client processes across web, queue, cron, and admin tools
- Leave 10–15 connections free on PostgreSQL for admin, replication, and monitoring
If your VPS runs 40 PHP-FPM workers and 8 queue workers, start with default_pool_size = 50. Tune from there using PgBouncer stats. For full-stack deployment help including server sizing, see our Linux system administration service.
Official parameter reference lives in the PgBouncer configuration documentation. Cross-check PostgreSQL-side limits in the PostgreSQL connection settings manual.
Which PgBouncer pool mode should Laravel and PHP use?
PgBouncer offers three pool modes. Picking the wrong one breaks sessions, prepared statements, or advisory locks. This is the decision most teams get wrong on first deploy.
| Pool mode | When server conn returns to pool | Laravel fit | Main risk |
|---|---|---|---|
| Session pooling | Client disconnects | Legacy apps using temp tables, LISTEN/NOTIFY | Minimal pooling benefit |
| Transaction pooling | Transaction ends (COMMIT/ROLLBACK) | Standard Laravel 12/13 HTTP + queue workloads | No session-scoped features across requests |
| Statement pooling | Each statement completes | Rare; mostly incompatible with ORMs | Breaks multi-statement transactions |
Transaction pooling is the correct default for Laravel on PHP 8.3 or 8.5. Each HTTP request wraps queries in implicit or explicit transactions. When the request ends, the backend returns to the pool. Eloquent, query builder, and migrations all work normally.
Session pooling only makes sense if you rely on PostgreSQL session variables, temporary tables that survive multiple requests, or LISTEN/NOTIFY. None of those apply to typical Blade or API apps.
Statement pooling breaks Laravel migrations, DB::transaction() blocks, and any multi-query unit of work. Do not use it with Eloquent.
Long-running transactions still hold a backend. Keep that in mind when debugging Laravel deadlocks and long transactions. A stuck job blocks one pool slot until it finishes or times out.
How do you connect Laravel to PostgreSQL through PgBouncer?
Laravel 12 and 13 treat PgBouncer as a standard PostgreSQL host. Change the connection target in .env and adjust two driver settings that conflict with transaction pooling.
Environment and config changes
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=6432
DB_DATABASE=myapp_production
DB_USERNAME=myapp_user
DB_PASSWORD=your_secure_password In config/database.php, disable persistent connections and prepared statements for the pgsql driver:
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '6432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
'options' => extension_loaded('pdo_pgsql') ? [
PDO::ATTR_PERSISTENT => false,
PDO::ATTR_EMULATE_PREPARES => true,
] : [],
], PDO::ATTR_EMULATE_PREPARES => true is critical. Transaction pooling reassigns server connections between statements. Named prepared statements tied to a specific backend fail with opaque errors if emulation is off.
After deploy, run:
php artisan config:cache
php artisan queue:restart Queue workers cache config at boot. Restart them whenever connection settings change. See the official Laravel database documentation for connection option details.
Separate connections for migrations and admin
Some teams keep a second Laravel connection that bypasses PgBouncer for migrations and one-off admin tasks. Add to config/database.php:
'pgsql_direct' => [
'driver' => 'pgsql',
'host' => env('DB_DIRECT_HOST', '127.0.0.1'),
'port' => env('DB_DIRECT_PORT', '5432'),
/* same credentials and database */
], Run migrations with:
php artisan migrate --database=pgsql_direct Migrations use advisory locks and DDL statements that behave more predictably on a direct session connection. For day-to-day traffic, keep the default connection on PgBouncer port 6432.
On booking platforms like Adventure Third Pole Trek, web workers, Horizon queues, and scheduled tasks all hit the database concurrently. Pooling keeps that multi-process footprint under control without rewriting application code.
What are common PgBouncer production mistakes and how do you fix them?
I've debugged pooling issues on several Deployer-managed production servers. These patterns recur across Laravel, Symfony, and custom PHP apps.
Connection leaks from persistent PDO
Setting PDO::ATTR_PERSISTENT => true pins a client connection open across requests. PgBouncer cannot recycle that slot efficiently. Always set persistent connections to false behind a pooler.
Using session-scoped features in transaction mode
SET search_path, temporary tables, and pg_advisory_lock tied to a session break when the backend swaps between transactions. Laravel's default search_path = public in config is fine. Custom session-level SQL is not.
Undersized default_pool_size
When all backends are busy, clients wait in queue. Symptoms include slow page loads with healthy CPU and no slow queries. Check wait time:
psql -p 6432 pgbouncer -c "SHOW POOLS;" Rising cl_waiting values mean you need a larger pool or fewer concurrent workers. Our testing and optimization service includes load testing that surfaces this before launch traffic hits.
Forgetting queue and scheduler workers
PHP-FPM worker count is only half the picture. Count Horizon workers, queue:work processes, cron-triggered Artisan commands, and external monitoring probes. Each opens a client connection to PgBouncer.
Skipping server_reset_query
Transaction pooling can leave session state on a backend after COMMIT. Set server_reset_query = DISCARD ALL in PgBouncer config. This clears prepared plans, temp tables, and session variables before the next client gets that backend.
Monitoring commands worth adding to cron
SHOW POOLS;— active, waiting, and idle client counts per databaseSHOW STATS;— total requests, bytes sent, average query time through the poolerSHOW CLIENTS;— which client IPs hold connections (useful when debugging runaway workers)- PostgreSQL
pg_stat_activity— confirm backend count stays neardefault_pool_size, not client count
Log PgBouncer stats to your existing monitoring stack. A JSON snapshot parsed through a JSON formatter during incident review saves time when you are under pressure.
How does PgBouncer compare to other PostgreSQL pooling options?
PgBouncer is not the only option. Understanding alternatives helps you pick the right layer for your stack and team size.
| Solution | Layer | Best for | Trade-off |
|---|---|---|---|
| PgBouncer | External proxy (port 6432) | PHP, Laravel, multi-process apps on VPS or EC2 | Extra service to install and monitor |
| Pgpool-II | External proxy with replication awareness | Automatic read/write split, failover | Heavier config; overkill for single-primary setups |
| Built-in pool (Pg 18) | Inside PostgreSQL (pg_prewarm, connection limits) | Not true pooling; only limits damage | Does not multiplex clients onto fewer backends |
| ORM-level pooling | Application (e.g. persistent PDO) | Single-process runtimes (Node, Java) | Persistent PDO fights PgBouncer in PHP-FPM |
For most Laravel deployments on a single PostgreSQL primary, PgBouncer is the right choice. Add Pgpool-II or managed proxy layers only when you need automatic failover or read/write splitting at the proxy tier. Application-level read replica routing in Laravel is often simpler — see our guide on that pattern.
If you are migrating from MySQL where connection overhead was lower, read MySQL to PostgreSQL migration notes before sizing pools. PostgreSQL connection costs are real and catch teams off guard.
Enterprise apps with strict uptime requirements benefit from pooling as part of broader architecture work. Our enterprise application development practice treats connection management as infrastructure, not an afterthought.
Schema design still matters. Pooling fixes connection count, not slow queries. Review common schema design mistakes alongside pool tuning. Good indexes reduce the time each backend stays busy, which indirectly improves pool throughput.
For ongoing ops — backups, pool stat review, PostgreSQL upgrades — support and maintenance keeps production systems stable after the initial PgBouncer rollout.
E-commerce workloads like Quick And Easy Nepalese Grocery spike connections during checkout and payment callbacks. Pooling absorbs those bursts without raising max_connections to unsafe levels.
Developers building database-driven websites in Nepal on budget VPS plans should plan for PgBouncer early. Waiting until connection errors appear in production forces emergency tuning under traffic.
Advanced Eloquent patterns — subqueries, eager loads, chunking — interact with pool wait times. See advanced Eloquent techniques for query patterns that release backends faster.
After any infrastructure change, validate backups still work against the direct PostgreSQL port. Follow database backup strategies for small servers so restore drills include both pooled and direct connection paths.
Team environments with frequent migrations should document which connection each CI job uses. Our database migrations in team environments guide covers coordination patterns that apply here.
Learn more about my production database work on the about me page or browse the full project portfolio for Laravel and PostgreSQL systems in production.
Key Takeaways
- Place PgBouncer on port 6432 between Laravel and PostgreSQL to multiplex hundreds of clients onto 20–50 real backends.
- Use transaction pooling for standard Laravel 12/13 apps; disable persistent PDO and enable emulated prepares.
- Size
default_pool_sizeto PHP-FPM workers plus queue workers plus headroom, not to peak HTTP concurrency alone. - Run migrations on a direct
pgsql_directconnection to port 5432; route normal traffic through the pooler. - Monitor
SHOW POOLS;for risingcl_waitingand setserver_reset_query = DISCARD ALLin production. - Pair pooling with index tuning and query optimization — pooling fixes connection limits, not slow SQL.
People Also Ask
Does PgBouncer work with Laravel Horizon and queue workers?
Yes. Point every worker's DB_PORT at 6432 and restart Horizon after config changes. Each worker process is a separate PgBouncer client. Include all workers when calculating max_client_conn and default_pool_size.
Can you use PgBouncer with Redis caching in Laravel?
They solve different problems. Redis caches data and sessions. PgBouncer limits PostgreSQL backend connections. Use both together on production Laravel apps without conflict.
What happens to Laravel migrations through PgBouncer?
DDL and advisory locks work more reliably on a direct PostgreSQL connection. Keep a secondary pgsql_direct database config on port 5432 for php artisan migrate. Route normal application traffic through PgBouncer only.
Is PgBouncer enough for high-traffic PostgreSQL on a small VPS?
It removes the connection ceiling bottleneck. It does not replace RAM, CPU, or disk I/O upgrades. On a Rs 2,500/month VPS (~USD 19), pooling often doubles effective connection capacity without hardware changes. Slow queries still need indexes and optimization.
Deploy Database Connection Pooling with PgBouncer on your next release
Database Connection Pooling with PgBouncer is a low-risk, high-impact change for Laravel apps on PostgreSQL 18. Install the pooler, set transaction mode, update .env, disable persistent PDO, and monitor pool stats for one week. Most teams see connection errors disappear without touching application code.
If you want help sizing pools, wiring Laravel config, or load-testing before a busy season, contact us for a production database review. Pooling is boring infrastructure — and boring infrastructure keeps client sites online when traffic spikes.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

