Back to articles
Technology Insight

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

June 1, 2026

Introduction: The Architectural Bottleneck of Scale

In modern web architectures, handling a surge in concurrent users is a common milestone. However, when operating under tight hardware constraints—such as a Virtual Private Server (VPS) with only 2GB of RAM—traditional scaling methods fail. By default, PostgreSQL follows a process-based model where each client connection forks a backend worker process. Each process can consume between 2MB to 10MB of memory immediately, scaling upward based on query complexity.

Mathematically, attempting to serve 10,000 concurrent connections directly via PostgreSQL on a 2GB RAM instance is impossible. The system would instantly trigger the Linux Out-Of-Memory (OOM) Killer, terminating the database service and causing severe downtime. To overcome this limitation, software engineers utilize PgBouncer, a lightweight connection pooler designed to dramatically reduce resource overhead. This article provides an operational blueprint for optimizing PostgreSQL with PgBouncer to sustain tens of thousands of concurrent connections on minimal hardware.

1. Understanding the Resource Crisis: PostgreSQL vs. High Connection Counts

Before implementing a solution, it is vital to analyze why PostgreSQL struggles with massive connection counts on low-memory instances. PostgreSQL relies on a Process-per-Connection architecture. This design guarantees isolation and stability but scales poorly regarding memory usage.

  • Memory Overhead: Every connection consumes RAM for connection state, caching, and local sort operations (work_mem). Even an idle connection consumes valuable memory.
  • Context Switching Costs: When thousands of operating system processes compete for a limited number of CPU cores, the CPU spends more time switching contexts than executing queries.
  • Lock Contention: Internal database locks (lwlocks) become highly contested as thousands of processes attempt to access shared memory buffers simultaneously.

The Golden Rule of Database Scaling: Active database connections should closely align with the number of available CPU cores. For a low-tier VPS, forcing PostgreSQL to manage thousands of active processes directly degrades performance exponentially.

2. Introducing PgBouncer: The Lightweight Savior

PgBouncer acts as an intermediary proxy between your application clients and the PostgreSQL engine. Instead of opening a dedicated backend process for every client, PgBouncer maintains a small pool of persistent connections to the actual database and routes client traffic through them efficiently.

While a PostgreSQL process requires megabytes of memory, a PgBouncer client connection requires only a few kilobytes. This allows PgBouncer to easily hold open 10,000 idle or active client connections while utilizing less than 100MB of RAM, passing only active queries to a limited pool of PostgreSQL backend processes.

Choosing the Right Pooling Mode

PgBouncer operates in three distinct pooling modes. Selecting the correct mode is critical for maximizing connection capacity on low-RAM environments:

  1. Session Pooling (Default): PgBouncer allocates a server connection to the client for the entire duration the client stays connected. This offers minimal benefits for handling tens of thousands of connections on a 2GB VPS, as the underlying database processes are still bound to the clients.
  2. Transaction Pooling: A server connection is only assigned to the client for the duration of a single database transaction. Once the transaction completes (COMMIT or ROLLBACK), the connection returns to the pool. This is the optimal choice for scaling to 10,000+ connections.
  3. Statement Pooling: The connection is destroyed or returned after every single SQL statement. This mode breaks multi-statement transactions and is rarely suitable for standard web applications.

3. Configuring PgBouncer for 10,000+ Connections

To implement this setup on a 2GB RAM VPS, you must configure PgBouncer using the Transaction Pooling mode. Below is an optimized pgbouncer.ini configuration tailored for this extreme constraint scenario:

[databases]
* = host=127.0.0.1 port=5432 auth_user=postgres

[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 = 50
min_pool_size = 10
reserve_pool_size = 5
reserve_pool_timeout = 5
max_db_connections = 60

# Timeouts & Performance Tuning
query_timeout = 30
client_idle_timeout = 60
server_idle_timeout = 30
pkt_buf = 4096
listen_backlog = 4096

In this configuration, max_client_conn is scaled to 15,000 to comfortably accommodate our target threshold. Conversely, max_db_connections restricts PgBouncer from opening more than 60 total concurrent backend processes inside PostgreSQL, safeguarding our 2GB RAM threshold from exhaustion.

4. Tuning PostgreSQL to Cooperate with PgBouncer

With PgBouncer intercepting connections, PostgreSQL must be tuned down to run efficiently with a small, highly dense connection pool. If you leave PostgreSQL at default configurations, it will still attempt to allocate memory buffers that exceed the physical 2GB capacity of the VPS.

Modify your postgresql.conf file with the following production parameters tailored for low-memory, high-throughput environments:

# Connection Limits
max_connections = 80 # Must be slightly higher than PgBouncer's max_db_connections

# Memory Allocations for 2GB RAM
shared_buffers = 512MB # 25% of total system RAM
effective_cache_size = 1500MB # ~75% of total RAM
work_mem = 4MB # Limits per-query sort memory allocation
maintenance_work_mem = 64MB

# Write-Ahead Log (WAL) Tuning
max_wal_size = 1GB
min_wal_size = 80MB
checkpoint_completion_target = 0.9
checkpoint_timeout = 15min

# Performance Optimization for Shared Pools
synchronous_commit = off # Increases throughput significantly if minor data loss on crash is acceptable

By setting max_connections to 80, we eliminate the risk of PostgreSQL spawning thousands of heavy OS processes. Setting shared_buffers to 512MB ensures that the operating system retains roughly 1.5GB of RAM for the OS file system cache, PgBouncer overhead, and necessary application services.

5. Crucial Operating System Tweaks

Handling 10,000+ network connections simultaneously requires adjusting the underlying Linux kernel parameters. By default, Linux is configured to protect itself from resource starvation by imposing strict limits on open file descriptors and network queues.

Adjusting File Descriptor Limits

Every network connection in Linux is treated as an open file. To prevent the notorious "Too many open files" error, modify /etc/security/limits.conf:

* soft    nofile          32768
* hard    nofile          32768
root            soft    nofile          32768
root            hard    nofile          32768

Optimizing Network Stack (sysctl.conf)

Add the following configurations to /etc/sysctl.conf to optimize network socket recycling and buffer limits:

# Increase the maximum number of open files globally
fs.file-max = 100000

# Maximize network connection backlog
net.core.somaxconn = 4096

# Enable TCP time-wait socket reuse for high-frequency connections
net.ipv4.tcp_tw_reuse = 1

# Adjust TCP buffer sizes to conserve memory per connection
net.ipv4.tcp_rmem = 4096 87380 16777216

# Adjust Linux virtual memory management to avoid sudden OOM situations
vm.overcommit_memory = 2
vm.overcommit_ratio = 80

Apply these changes instantly by running sudo sysctl -p in your terminal execution space.

6. Important Trade-offs and Limitations of Transaction Pooling

While utilizing PgBouncer in transaction mode allows your application to handle immense connection loads seamlessly, it introduces specific technical limitations that developers must be aware of during application design:

  • Prepared Statements: Because subsequent queries within the same session may route through different PostgreSQL server processes, standard server-side prepared statements are broken by default. Solution: Configure your application ORM to use client-side prepared statements, or utilize modern PgBouncer versions that offer native prepared statement tracking.
  • Temporary Tables: Temporary tables exist only for the duration of a specific database session. In transaction mode, a temporary table created in transaction A may disappear or become visible to an entirely different client in transaction B. Do not use temporary tables in this architectural model.
  • Session-Level Variables: Settings adjusted using SET TIMEZONE or SET search_path will persist on that specific connection and leak into other users' transactions. Avoid session modifications; set these globally or pass them explicitly within queries.

Conclusion: High Availability on Minimal Budgets

Optimizing a database for scale does not always require purchasing expensive hardware upgrades. By decoupling connection management from query execution using PgBouncer, you can transform a modest 2GB RAM VPS into a high-throughput powerhouse capable of sustaining 10,000+ concurrent connections.

Through strict memory boundaries in postgresql.conf, transaction pooling via pgbouncer.ini, and adjusted Linux kernel thresholds, your architectural stack becomes highly resilient, predictable, and remarkably cost-effective. Ensure you monitor your configuration via the PgBouncer administrative console (psql -p 6432 -U pgbouncer pgbouncer) to observe connection trends and continually refine your production boundaries.

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