Back to articles
Technology Insight

Scaling PostgreSQL: How to Handle Tens of Thousands of Concurrent Connections on a 2GB RAM VPS Using PgBouncer

June 2, 2026

Introduction: The Database Connection Bottleneck

In modern web architectures, handling high concurrent traffic is a badge of honor. However, for database administrators and DevOps engineers working with limited hardware, it can quickly turn into a nightmare. PostgreSQL is a remarkably powerful and reliable relational database, but it possesses a well-known architectural constraint: its process-based connection model.

Each incoming connection to a PostgreSQL server forks a dedicated backend process. On a Virtual Private Server (VPS) with only 2GB of RAM, this architecture poses a severe scalability limit. Allowing thousands of direct connections will inevitably lead to memory exhaustion, aggressive swapping, and the dreaded Out-Of-Memory (OOM) killer terminating your database server. To achieve high concurrency without upgrading to expensive hardware, we must introduce an intermediary layer. Enter PgBouncer.

The Problem: Why PostgreSQL Struggles with High Connection Counts

Before diving into the solution, it is crucial to understand the underlying mechanics of PostgreSQL connections. Every connection process consumes roughly 2MB to 10MB of overhead memory, depending on the complexity of the queries and configuration parameters like work_mem. Let us break down the math for a standard 2GB RAM VPS:

  • Operating System Overhead: ~500MB - 750MB
  • PostgreSQL Shared Buffers: ~512MB (Recommended 25% of total RAM)
  • Remaining RAM for Connections: ~750MB

If each connection utilizes a conservative 5MB of memory, a mere 150 direct connections could completely deplete the available RAM. When tens of thousands of clients attempt to connect simultaneously, the server spends more CPU cycles on context switching and lock contention than on executing actual queries. This phenomenon is known as connection thrashing.

The Solution: What is PgBouncer?

PgBouncer is a lightweight, open-source connection pooler designed specifically for PostgreSQL. Instead of allowing every client to open a direct, long-lived process inside the database, PgBouncer sits between your application and PostgreSQL, maintaining a compact pool of heavy backend connections while serving thousands of lightweight frontend client connections.

PgBouncer acts as a high-performance traffic controller, ensuring that the database only processes an optimal number of concurrent queries while keeping external clients waiting safely in a queue.

By drastically reducing the number of active server processes, PgBouncer minimizes memory consumption and context switching, enabling a modest 2GB RAM VPS to handle a massive volume of concurrent requests.

Choosing the Right Pooling Mode

PgBouncer operates in three distinct pooling modes. Selecting the correct mode is critical for your application\'s compatibility and performance:

  1. Session Pooling (Default): PgBouncer assigns a server connection to the client for the entire duration the client stays connected. When the client disconnects, the server connection is returned to the pool. This mode offers no benefits for handling tens of thousands of connections on low RAM.
  2. Transaction Pooling: A server connection is assigned to the client only for the duration of a specific database transaction. Once the transaction ends (after a COMMIT or ROLLBACK), the connection goes back to the pool. This is the highly recommended mode for high-concurrency environments.
  3. Statement Pooling: The connection is returned to the pool immediately after a single query completes. This mode disables multi-statement transactions and is rarely used in typical application architectures.

Step-by-Step Guide: Configuring PgBouncer on a 2GB RAM VPS

Step 1: Optimizing the PostgreSQL Configuration

First, we must configure PostgreSQL to expect a small, highly efficient pool of connections. Modify your postgresql.conf file with the following strategic adjustments:

max_connections = 100
shared_buffers = 512MB
effective_cache_size = 1536MB
work_mem = 4MB
maintenance_work_mem = 64MB

By capping max_connections at 100, we ensure that PostgreSQL never over-allocates memory, reserving the remaining RAM for query execution and OS caching.

Step 2: Installing and Configuring PgBouncer

Install PgBouncer via your package manager (e.g., apt-get install pgbouncer on Ubuntu). Next, edit the primary configuration file, typically located at /etc/pgbouncer/pgbouncer.ini:

[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 = 20000
default_pool_size = 50
min_pool_size = 10
reserve_pool_size = 5
max_db_connections = 80

Key parameter explanations:

  • pool_mode = transaction: Enables transaction-level reuse, which is essential for high concurrency.
  • max_client_conn = 20000: Allows up to 20,000 simultaneous frontend application connections.
  • default_pool_size = 50: Maintains a maximum of 50 active connections to the actual PostgreSQL database under normal load.

Step 3: Setting Up Authentication

Create the /etc/pgbouncer/userlist.txt file to allow PgBouncer to authenticate users against the database. The format must match the PostgreSQL user database:

"myuser" "password_hash_or_plain_text"

Verifying Performance and Monitoring

Once both services are restarted, update your application\'s database connection string to point to the PgBouncer port (6432) instead of the standard PostgreSQL port (5432).

To monitor the performance of your pooler, log into the virtual administrative database using the following command:

psql -p 6432 -U pgbouncer -d pgbouncer

From the administrative console, execute SHOW POOLS; and SHOW STATS; to monitor client wait times, active traffic, and query throughput. You will notice that while your application registers thousands of connected clients, the actual PostgreSQL backend process count remains perfectly stable at or below your default_pool_size.

Conclusion: Maximum Efficiency on Minimal Hardware

Optimizing a 2GB RAM VPS to handle tens of thousands of concurrent connections sounds impossible until you decouple connection management from query execution. By deploying PgBouncer in transaction pooling mode, you protect your PostgreSQL instance from memory exhaustion, eliminate devastating context-switching latency, and unlock enterprise-grade concurrency on budget infrastructure.

As your application grows, remember to continuously monitor key metrics such as cl_waiting (clients waiting for a connection) to fine-tune your pool sizes for optimal performance.

Scaling PostgreSQL: How to Handle Tens of Thousands of Concurrent Connections on a 2GB RAM VPS Using PgBouncer | DPTCloud