Back to articles
Technology Insight

Scaling PostgreSQL: How to Handle 10,000+ Concurrent Connections on a 2GB RAM VPS Using PgBouncer

June 2, 2026

The Database Bottleneck: Connection Scaling on Lightweight Infrastructure

In modern web architecture, scaling an application to support tens of thousands of concurrent users is a milestone achievement. However, for engineering teams operating on lean infrastructure—such as a Virtual Private Server (VPS) with just 2GB of RAM—this milestone often brings severe performance bottlenecks. When using PostgreSQL as the primary database engine, the barrier to scaling isn't usually the storage engine or the query optimizer; it is the connection model.

By default, PostgreSQL allocates a dedicated backend process for every single client connection. Each process can consume anywhere from 2MB to 10MB of RAM immediately upon allocation, even when idling. Do the math: 1,000 idle connections could instantly swallow up to 10GB of RAM, far exceeding the physical limitations of a 2GB VPS. This leads to heavy swapping, extreme CPU throttling, or the notorious Out-Of-Memory (OOM) Killer terminating your database process. To resolve this, businesses must implement an enterprise-grade connection pooler. Enter PgBouncer.

Understanding PgBouncer and the Power of Connection Pooling

PgBouncer is a lightweight, open-source connection pooler designed specifically for PostgreSQL. It acts as an intermediary proxy between your application servers and the underlying database instance. Instead of your application opening direct, long-lived connections to PostgreSQL, it talks to PgBouncer. PgBouncer then manages a significantly smaller, highly optimized pool of actual connections to the real database.

The Three Pooling Modes of PgBouncer

Choosing the correct pooling mode is critical to achieving high throughput on constrained hardware:

  • Session Pooling: The most conservative approach. PgBouncer assigns a server connection to the client for the entire duration the client stays connected. This provides full compatibility but saves minimal memory if clients remain idle.
  • Transaction Pooling: The optimal choice for high-concurrency systems. A server connection is only allocated to the client for the duration of a specific database transaction. As soon as the transaction ends (via COMMIT or ROLLBACK), the connection returns to the pool to serve another client. Note: This mode restricts the use of certain session-level features like prepared statements (without specific workarounds) and temporary tables.
  • Statement Pooling: The most aggressive mode. The connection is broken down at the individual SQL statement level. Multi-statement transactions are not allowed, making this mode unsuitable for standard relational applications.
Business Impact: For a 2GB RAM VPS aiming to handle tens of thousands of simultaneous connections, Transaction Pooling is mandatory. It decouples client connections from physical database processes, allowing 10,000 virtual application connections to be safely processed by as few as 20 to 50 actual PostgreSQL database backends.

Step-by-Step Implementation and Configuration Guide

1. Installing PgBouncer

On a standard Ubuntu/Debian-based VPS, installation is straightforward via the official package manager:

sudo apt-get update
sudo apt-get install pgbouncer

2. Configuring PgBouncer (pgbouncer.ini)

The primary configuration file is located at /etc/pgbouncer/pgbouncer.ini. Below is an optimized configuration blueprint designed specifically for a 2GB RAM architecture running under heavy connection duress:

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

[pgbouncer]
logfile = /var/log/postgresql/pgbouncer.log
pidfile = /var/run/postgresql/pgbouncer.pid
listen_addr = *
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

# Pooling Configuration
pool_mode = transaction
max_client_conn = 15000
default_pool_size = 30
min_pool_size = 10
reserve_pool_size = 5
reserve_pool_timeout = 5
max_db_connections = 50

Let us break down why these parameters are critical for a 2GB RAM system:

  • max_client_conn = 15000: This tells PgBouncer to accept up to 15,000 simultaneous incoming client connections from your web servers. Because these are managed by PgBouncer's low-overhead asynchronous I/O loop, they consume negligible memory.
  • default_pool_size = 30: This restricts the maximum number of actual server connections PgBouncer will ever open to the PostgreSQL database for a single database user combination. 30 processes will easily fit within your 2GB RAM envelope.
  • max_db_connections = 50: A hard ceiling to guarantee PostgreSQL never spawns more than 50 backend processes across all pools combined, completely eliminating OOM risks.

3. Configuring Authentication (userlist.txt)

PgBouncer needs to verify credentials before passing requests to PostgreSQL. Edit /etc/pgbouncer/userlist.txt and add your database users and their MD5-hashed or plaintext passwords:

"db_user" "password123"

Tuning PostgreSQL for Co-existence on a 2GB RAM VPS

An unoptimized PostgreSQL instance will fight PgBouncer for system memory. To ensure stability, you must adjust PostgreSQL's native configuration (postgresql.conf) to reflect that it is operating behind a pooler and running on limited hardware.

Key Parameter Adjustments:

  1. max_connections: Reduce this from large arbitrary numbers down to 60 or 100. Since PgBouncer handles the thousands of incoming client requests and funnels them into a maximum pool of 30-50, PostgreSQL does not need a high native connection limit.
  2. shared_buffers: Set this to 512MB (25% of your 2GB total RAM). This is the dedicated memory PostgreSQL uses for caching data blocks.
  3. work_mem: Set this conservatively to 4MB or 8MB. This memory is allocated per-backend-process for complex sorting and joins. With 30 active pool connections, 30 * 4MB = 120MB, a safe allocation that prevents memory exhaustion during intensive query executions.
  4. maintenance_work_mem: Set to 64MB for utility operations like indexing and vacuuming.

Monitoring, Benchmark Results, and Best Practices

Once both systems are configured, restart the services to apply changes:

sudo systemctl restart postgresql
sudo systemctl restart pgbouncer

To monitor your connection pool health in real-time, connect directly to PgBouncer’s administrative console:

psql -p 6432 -U pgbouncer -d pgbouncer

Run the command SHOW POOLS; to inspect active, waiting, and idle connections. If you notice high cl_waiting (clients waiting for a database connection), it indicates your application workloads are bottlenecked by the default_pool_size, and you may need to optimize your transaction execution speeds or slightly increase the pool limit.

Production Best Practices

  • Keep Transactions Short: Because you are utilizing transaction pooling, keeping your code blocks tight and minimizing long-running transactions is vital. Avoid placing external API calls or heavy processing inside a database transaction block.
  • Handle Prepared Statements Appropriately: If your application framework relies heavily on server-side prepared statements, ensure you set track_prepares or configure your framework to use client-side caching to prevent errors under transaction mode.
  • Implement Health Checks: Set up automated alerts for VPS memory utilization and PgBouncer log errors to proactively catch anomalies before they impact end-users.

Conclusion

Optimizing high-concurrency database systems does not always require purchasing expensive hardware upgrades. By placing PgBouncer in front of your PostgreSQL database and tuning configurations specifically for a 2GB RAM VPS environment, you transform a potential system failure into a resilient, high-throughput data layer. Embracing transaction pooling allows lean startups and enterprise teams alike to scale seamlessly to tens of thousands of concurrent connections while maximizing hardware efficiency and maintaining rock-solid system stability.

Scaling PostgreSQL: How to Handle 10,000+ Concurrent Connections on a 2GB RAM VPS Using PgBouncer | DPTCloud