Engineering

Connection Pooling Done Right: PgBouncer vs Built-in Node.js Pools

Explore efficient PostgreSQL connection pooling strategies for Node.js! Compare PgBouncer vs built-in pools to optimize performance at scale, based on real-world experience.

· Founder & Engineer · · 8 min read
Diagram illustrating the connection flow between Node.js application, PgBouncer, and PostgreSQL database server.

Connection pooling is essential for any application that relies on frequent database interactions, but the wrong approach can quickly become a performance bottleneck. We’ve learned a lot from scaling MisuJob to efficiently process 1M+ job listings from multiple sources, and we’re here to share our experience with choosing the right connection pooling strategy for PostgreSQL.

The Challenge: Database Connections and Scale

At MisuJob, efficient database operations are paramount. Our AI-powered job matching system requires frequent database access to analyze and categorize listings, understand skills, and provide relevant recommendations to our users across Europe. Each user searching for a new opportunity, each algorithm updating job categorization, and each background process running all rely on a solid database layer.

A naive approach of opening a new database connection for each request quickly becomes unsustainable. Every new connection incurs overhead: authentication, negotiation, and resource allocation. This overhead adds up, especially under heavy load, causing latency spikes and impacting overall application performance.

Built-in Node.js connection pools offer a seemingly simple solution, but they can fall short in certain scenarios. This is especially true at the scale we operate at, where many Node.js processes might be trying to connect to the same database instance. We explored and ultimately adopted PgBouncer, a lightweight connection pooler, as a more robust and scalable solution. This blog post details why and how.

Understanding Connection Pooling Options

Before diving into the specifics of PgBouncer, let’s briefly explore the connection pooling options available to Node.js developers working with PostgreSQL.

Built-in Node.js Connection Pools

Most Node.js PostgreSQL libraries (e.g., pg, node-postgres) provide built-in connection pooling. These pools manage a set of active connections to the database, reusing them for subsequent requests.

Here’s a basic example using the pg library:

const { Pool } = require('pg');

const pool = new Pool({
  user: 'dbuser',
  host: 'db.example.com',
  database: 'mydb',
  password: 'secretpassword',
  port: 5432,
  max: 20, // Maximum number of connections in the pool
  idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
  connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
});

async function queryDatabase(queryText, values) {
  const client = await pool.connect();
  try {
    const result = await client.query(queryText, values);
    return result.rows;
  } finally {
    client.release(); // Release the connection back to the pool
  }
}

// Example usage
queryDatabase('SELECT * FROM users WHERE id = $1', [1])
  .then(rows => console.log(rows))
  .catch(err => console.error(err));

This approach is relatively easy to implement and works well for smaller applications with moderate database load. However, it has limitations when scaling horizontally with multiple Node.js processes or when dealing with a large number of concurrent requests. Each Node.js process maintains its own connection pool, leading to:

  • Connection Exhaustion: The total number of database connections can easily exceed the PostgreSQL server’s max_connections limit, leading to errors and service disruption.
  • Inefficient Resource Utilization: Idle connections consume server resources. Multiple pools with idle connections waste resources compared to a single, shared pool.
  • Connection Thrashing: Constant connection creation and destruction can strain the database server, especially when request patterns are spiky.

PgBouncer: A Lightweight Connection Pooler

PgBouncer is a lightweight connection pooler that sits between your application and the PostgreSQL database. It acts as a proxy, managing a pool of connections to the database and multiplexing client connections onto those pooled connections.

Key benefits of PgBouncer:

  • Reduced Database Load: By pooling connections at a central point, PgBouncer significantly reduces the number of active connections to the database, preventing connection exhaustion.
  • Improved Performance: Connection reuse minimizes the overhead of establishing new connections, leading to lower latency and improved throughput.
  • Connection Management: PgBouncer provides robust connection management features, such as connection limits, timeouts, and connection draining.
  • Scalability: PgBouncer allows you to scale your application horizontally without overwhelming the database server with connection requests.

Why We Chose PgBouncer at MisuJob

At MisuJob, we faced the challenges described above as we scaled our services to handle the increasing volume of job listings and user traffic across Europe. We observed frequent database connection errors and performance degradation during peak hours. We needed a solution that could efficiently manage database connections and scale with our growth.

After evaluating different connection pooling strategies, we decided to implement PgBouncer for several key reasons:

  1. Connection Limits: We were hitting the max_connections limit on our PostgreSQL instances regularly. PgBouncer allows us to configure a much smaller pool of database connections, which are then shared amongst all the Node.js processes. This drastically reduced the load on the database server.

  2. Reduced Latency: By reusing existing connections, PgBouncer significantly reduced the latency associated with establishing new connections for each request. This improved the overall responsiveness of our application.

  3. Simplified Configuration: Centralizing connection pooling logic in PgBouncer simplified the configuration of our Node.js applications. We no longer needed to configure connection pool parameters in each application instance.

  4. Improved Observability: PgBouncer provides detailed metrics on connection usage, allowing us to monitor database performance and identify potential bottlenecks.

Implementing PgBouncer

Here’s a brief overview of how we implemented PgBouncer in our environment:

  1. Installation: We installed PgBouncer on a dedicated server (or alongside the database server in smaller deployments).

    sudo apt-get update
    sudo apt-get install pgbouncer
    
  2. Configuration: We configured PgBouncer with the database connection details and connection pool parameters. The pgbouncer.ini file is the main configuration file.

    [databases]
    mydb = host=db.example.com port=5432 dbname=mydb user=dbuser
    
    [pgbouncer]
    listen_addr = *
    listen_port = 6432
    auth_type = md5
    auth_file = /etc/pgbouncer/userlist.txt
    pool_mode = transaction
    server_reset_query = DISCARD ALL
    max_client_conn = 1000
    default_pool_size = 20
    
    • pool_mode = transaction : This is the most common and recommended mode. Connections are returned to the pool after each transaction.
    • max_client_conn: The maximum number of client connections PgBouncer will accept.
    • default_pool_size: The number of server connections to keep in the pool per database.
  3. User Authentication: We created a userlist.txt file containing the database user credentials.

    "dbuser" "secretpassword"
    
  4. Node.js Application Configuration: We updated our Node.js applications to connect to PgBouncer instead of directly to the database.

    const { Pool } = require('pg');
    
    const pool = new Pool({
      user: 'dbuser',
      host: 'pgbouncer.example.com', // Connect to PgBouncer
      database: 'mydb',
      password: 'secretpassword',
      port: 6432, // PgBouncer's port
      max: 10, // Smaller pool size in the Node.js application
    });
    

Performance Improvements and Monitoring

After implementing PgBouncer, we observed significant improvements in database performance:

  • Reduced Connection Errors: The number of connection errors decreased dramatically, as PgBouncer effectively managed the connection pool.
  • Lower Latency: Average query latency decreased by 20-30%, resulting in a more responsive application.
  • Improved Throughput: The database server could handle a higher volume of requests without performance degradation.

We also set up monitoring to track PgBouncer’s performance. PgBouncer provides a virtual database called pgbouncer that exposes various metrics. We used the following query to monitor connection statistics:

-- Connect to the pgbouncer database as the pgbouncer user
SHOW STATS;

This query returns metrics such as:

  • total_requests: Total number of requests processed.
  • total_received: Total bytes received.
  • total_sent: Total bytes sent.
  • total_query_time: Total query processing time.
  • avg_req: Average requests per second.
  • avg_recv: Average bytes received per second.
  • avg_sent: Average bytes sent per second.
  • avg_query: Average query processing time (in microseconds).

These metrics helped us to fine-tune PgBouncer’s configuration and ensure optimal performance.

Beyond Performance: Other Considerations

While performance improvements are a primary driver for adopting PgBouncer, other considerations are important:

  • Connection Pooling Mode: PgBouncer offers different pooling modes: session, transaction, and statement. transaction mode is generally recommended for most applications. It releases the connection back to the pool after each transaction, ensuring efficient connection reuse.
  • Prepared Statements: PgBouncer’s transaction pooling mode and statement pooling mode do not support prepared statements. This can be a limitation if your application relies heavily on prepared statements. You’ll need to evaluate the trade-offs and potentially adjust your query patterns.
  • Failover: Ensure proper failover mechanisms are in place for PgBouncer to maintain high availability. This might involve running multiple PgBouncer instances behind a load balancer.

Salary Insights: PostgreSQL Engineers Across Europe

Having a performant database layer is key to providing reliable salary insights. We use our aggregates from multiple sources to provide accurate salary ranges. Here’s a snapshot of estimated salaries for PostgreSQL Engineers in different European regions:

RegionAverage Salary (€/year)Salary Range (€/year)
DACH (Germany)75,00060,000 - 95,000
UK65,00050,000 - 85,000
Netherlands70,00055,000 - 90,000
Nordics72,00058,000 - 92,000
France60,00048,000 - 78,000
Spain50,00040,000 - 65,000

These numbers represent an approximate range and can vary based on experience, skills, and company size. MisuJob’s AI-powered job matching can help you discover opportunities aligned with your desired salary and location.

Comparing Connection Pooling Strategies

Here’s a comparison table summarizing the key differences between built-in Node.js connection pools and PgBouncer:

FeatureBuilt-in Node.js PoolsPgBouncer
Connection ManagementDecentralized (per process)Centralized (single pool)
ScalabilityLimited (horizontal scaling)Excellent (designed for scaling)
Resource UtilizationLess efficient (idle connections)More efficient (connection reuse)
ComplexitySimpler configurationMore complex configuration
Prepared StatementsSupportedLimited support (depending on pool mode)
Connection LimitsCan easily exceed database limitsEnforces connection limits effectively
ObservabilityLimitedComprehensive metrics available
Best Use CaseSmall to medium-sized applicationsLarge-scale applications, high load

Conclusion

Choosing the right connection pooling strategy is crucial for building scalable and performant applications. While built-in Node.js connection pools are sufficient for smaller applications, PgBouncer provides a more robust and scalable solution for handling high database load. At MisuJob, PgBouncer has been instrumental in ensuring the reliability and performance of our AI-powered job matching system as we continue to grow and serve users across Europe. By implementing PgBouncer, we’ve been able to significantly reduce database load, improve application responsiveness, and simplify our infrastructure.

If you’re experiencing database connection issues or performance bottlenecks in your Node.js application, we highly recommend considering PgBouncer as a connection pooling solution. The initial investment in configuration and setup is well worth the long-term benefits of improved scalability, resource utilization, and application performance.

postgresql nodejs connection pooling pgbouncer performance database
Share
P
Pablo Inigo

Founder & Engineer

Building MisuJob - an AI-powered job matching platform processing 1M+ job listings daily.

Engineering updates

Technical deep dives delivered to your inbox.

Find your next role with AI

Upload your CV. Get matched to 50,000+ jobs. Apply to the best fits effortlessly.

Get Started Free

User

Dashboard Profile Subscription