AWS Database Blog

Connection pooling strategies in Amazon Aurora DSQL

In this post, you’ll learn four concrete strategies that help you reduce Aurora DSQL connection overhead, stay within the 100-connections-per-second rate limit, and avoid thundering-herd reconnection storms. By the end, you’ll have a production-ready checklist for configuring connection pools that support reliable performance at scale.

Connection pooling strategies determine whether your Amazon Aurora DSQL applications scale reliably or fail under load. The unique architecture of Amazon Aurora DSQL makes connection pooling even more impactful. With transaction-level multiplexing and authentication based on AWS Identity and Access Management (IAM), Aurora DSQL provides a foundation where the right pooling strategy improves performance in ways that traditional PostgreSQL connection management cannot.

Note: We assume you are familiar with PostgreSQL connection management concepts and basic IAM authentication. If you’re new to Amazon Aurora DSQL, start with the Getting Started guide before applying these strategies.

Connection pooling in a serverless world

Although Amazon Aurora DSQL handles operational complexity automatically, connection pooling remains critical. Every new connection requires an expensive TLS handshake and credential exchange. Pooling reuses established connections to reduce this overhead and cut latency. Applications can also hit the 100 new connections per second rate limit (burst: 1,000) during traffic spikes, so reusing existing connections can keep you safely within the service’s limits. Finally, Amazon Aurora DSQL enforces a maximum connection age, and clients will be disconnected after 1 hour. A well-configured pool recycles connections before this limit, keeping sessions alive transparently.

Transaction-level pooling: Built into the architecture

Amazon Aurora DSQL uses a transaction pooling model. The service assigns connections to a Query Processor only during active transactions, not for entire sessions. In standard PostgreSQL deployments, connections maintain a persistent 1:1 session relationship with backend server processes. By contrast, Amazon Aurora DSQL breaks this mapping, so a smaller number of Query Processors can serve a much larger number of connections.

This design assumes low duty cycles for connections, meaning most connections are idle most of the time. The lower the duty cycle, the fewer Query Processors needed for a pool of connections. As a result, you should not use PgBouncer, pgpool-II, or similar database-side proxy solutions. The service’s connection architecture is redundant with these tools because transaction-level connection multiplexing is built in by default.

Additionally, certain PostgreSQL features are not supported: SQL-level PREPARE/DEALLOCATE statements (though prepared statements through the extended query protocol work normally), WITH HOLD cursors, and session-level advisory locks.

Four connection pooling strategies for Amazon Aurora DSQL

Each of the following four strategies addresses a specific aspect of connection management in Amazon Aurora DSQL. Applied together, they help your application stay within service limits, and maintain stable performance under load. Start with Strategy 1 if you’re building a new application or use the strategies as a checklist if you’re migrating an existing PostgreSQL application.

Strategy 1: Use official AWS connectors

We recommend using the official Amazon Aurora DSQL connectors. They handle the full IAM token lifecycle automatically, including token generation, caching at 80% of token lifetime, and transparent refresh.

Table 1: Recommended connection pool libraries and key configuration parameters by programming language

Language Pooling Library Key Configuration
Java HikariCP maximumPoolSize
Python psycopg ConnectionPool min_size
Node.js node-postgres (pg.Pool) max
Go pgxpool MaxConns

Java (HikariCP) — Use HikariCP for Spring Boot, Quarkus, or Micronaut applications.

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://<cluster-endpoint>:5432/<db>?ssl=true&sslmode=verify-full&sslrootcert=<path>");
config.setMaximumPoolSize(20);
config.setMaxLifetime(55 * 60 * 1000); // 55 minutes
config.setIdleTimeout(10 * 60 * 1000); // 10 minutes
config.setConnectionTimeout(30 * 1000); // 30 seconds
config.setKeepaliveTime(5 * 60 * 1000); // 5 minutes
HikariDataSource ds = new HikariDataSource(config);

Python (psycopg ConnectionPool) — Use psycopg ConnectionPool for Django, Flask, or FastAPI applications.

from psycopg_pool import ConnectionPool

pool = ConnectionPool(
    conninfo="host=<endpoint> dbname=<db> sslmode=verify-full",
    min_size=2,
    max_size=20,
    max_lifetime=55 * 60,  # 55 minutes in seconds
    max_idle=10 * 60,  # 10 minutes idle timeout
    reconnect_timeout=30,  # 30 seconds to reconnect
)

Node.js (node-postgres) — Use node-postgres Pool for Express.js, NestJS, or serverless Node.js functions.

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

const pool = new Pool({
    host: "<cluster-endpoint>",
    database: "<db>",
    ssl: { rejectUnauthorized: true },
    max: 20,
    idleTimeoutMillis: 10 * 60 * 1000, // 10 minutes
    connectionTimeoutMillis: 30 * 1000, // 30 seconds
});

Go (pgxpool) — Use pgxpool for Go applications built with Gin, Echo, or AWS Lambda Go runtime.

connStr := "host=<cluster-endpoint> dbname=<db> sslmode=verify-full"
config, _ := pgxpool.ParseConfig(connStr)
config.MaxConns = 20
config.MinConns = 2
config.MaxConnLifetime = 55 * time.Minute // 55 minutes
config.MaxConnIdleTime = 10 * time.Minute // 10 minutes idle
pool, _ := pgxpool.NewWithConfig(ctx, config)

AWS Lambda: Instantiate the pool outside the handler

AWS Lambda is a common compute service for Aurora DSQL workloads. Lambda’s execution model requires a specific pooling pattern: create the connection pool at module scope (outside the handler function), so it persists across warm invocations of the same execution environment.

When Lambda reuses an execution environment, the pool created at module scope remains active. Subsequent invocations skip the TLS handshake and IAM token exchange entirely. On a cold start, the pool initializes once and is reused for the lifetime of that environment.

Because each concurrent Lambda invocation runs in its own isolated execution environment, each gets its own pool. Keep the pool size small (1–3 connections per Lambda instance) to avoid over-provisioning connections across many concurrent executions.

Key guidelines for Lambda:

  • Instantiate the pool outside the handler. This is the most important rule.
  • Set max pool size to 1–3. Each Lambda instance handles one request at a time (unless using buffered invocations), so large pools are wasteful.
  • Do not set idle timeout. Let connections persist for the life of the execution environment.
  • Set max lifetime to 55 minutes with jitter, the same as non-Lambda workloads, to avoid hitting the 1-hour hard limit.
  • Account for cold start latency. The first invocation incurs TLS and IAM authentication overhead. Subsequent warm invocations reuse the pooled connection.

Python (psycopg ConnectionPool) — Lambda

# pool is created ONCE at module scope, reused across warm invocations
from psycopg_pool import ConnectionPool
import boto3

pool = ConnectionPool(
    conninfo="host=<cluster-endpoint> dbname=<db> sslmode=verify-full",
    min_size=1,
    max_size=2,
    max_lifetime=55 * 60,  # 55 minutes
    reconnect_timeout=30,
)

def handler(event, context):
    """Lambda handler — pool is already initialized."""
    with pool.connection() as conn:
        result = conn.execute("SELECT * FROM orders WHERE id = %s", [event["order_id"]])
        return result.fetchone()

Node.js (node-postgres) — Lambda

// Pool is created ONCE at module scope, reused across warm invocations
const { Pool } = require("pg");

const pool = new Pool({
    host: "<cluster-endpoint>",
    database: "<db>",
    ssl: { rejectUnauthorized: true },
    max: 2, // small pool per Lambda instance
    idleTimeoutMillis: 0, // don't close idle connections
    connectionTimeoutMillis: 30 * 1000, // 30 seconds
});

exports.handler = async (event) => {
    // Pool is already warm on subsequent invocations
    const client = await pool.connect();
    try {
        const result = await client.query("SELECT * FROM orders WHERE id = $1", [event.orderId]);
        return result.rows[0];
    } finally {
        client.release();
    }
};

Go (pgxpool) — Lambda

package main

import (
    "context"
    "time"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/jackc/pgx/v5/pgxpool"
)

// Pool created at package scope — initialized once per execution environment
var pool *pgxpool.Pool

func init() {
    connStr := "host=<cluster-endpoint> dbname=<db> sslmode=verify-full"
    config, _ := pgxpool.ParseConfig(connStr)
    config.MaxConns = 2
    config.MinConns = 1
    config.MaxConnLifetime = 55 * time.Minute
    config.MaxConnLifetimeJitter = 5 * time.Minute

    pool, _ = pgxpool.NewWithConfig(context.Background(), config)
}

func handler(ctx context.Context, event map[string]string) (string, error) {
    row := pool.QueryRow(ctx, "SELECT name FROM orders WHERE id = $1", event["order_id"])
    var name string
    err := row.Scan(&name)
    return name, err
}

func main() {
    lambda.Start(handler)
}

Tip: If your Lambda function has high concurrency (hundreds of simultaneous executions), each execution environment creates its own pool. With a max pool size of 2 and 500 concurrent Lambda invocations, you could have up to 1,000 connections to Aurora DSQL. Monitor your total connection count and stay within the 10,000 concurrent connections per cluster quota.

Important: Configure your client connections with sslmode=verify-full to enforce full certificate verification and help prevent on-path attacks. Although Aurora DSQL accepts connections using weaker modes such as verify-ca or require, these do not validate the server’s identity.

Understanding the IAM token lifecycle is important for maintaining stable connection pools.

Generate a fresh IAM authentication token for each new connection. Token generation is a local signing operation with negligible overhead. The official AWS connectors handle this automatically. If you use a custom pool, call the generate-db-connect-auth-token command in your connection factory’s beforeConnect hook rather than caching tokens manually.

Note that your token cannot outlive the underlying IAM credentials. If you assume a role with a 1-hour session, the token expires in at most 1 hour regardless of the --expires-in value.

Scope database roles to least privilege. IAM authentication controls who can connect, but does not restrict what the session can do once connected. Assign each application’s database role only the minimum privileges it requires, for example, grant SELECT on read-only services and limit write access to specific schemas or tables. Avoid using admin roles in application connection pools.

For step-by-step instructions, see Authorizing database roles to use SQL in your database.

Strategy 2: Avoid connection expiration by configuring maximum connection lifetime with jitter

Amazon Aurora DSQL enforces a hard 1-hour maximum connection age. When a connection reaches this age, the service closes it regardless of whether it’s idle or mid-transaction. Your pool must recycle connections before this happens.

Why jitter matters: If most connections in your pool were created at roughly the same time (for example, at application startup), they’ll expire at roughly the same time. This creates a “thundering herd” of new connection requests that can exceed the 100 connections per second rate limit. Adding random jitter to the maximum lifetime spreads connection recycling over time.

// Go (pgxpool) — Use native per-connection jitter
config.MaxConnLifetime = 55 * time.Minute
config.MaxConnLifetimeJitter = 5 * time.Minute
// Each connection independently gets a lifetime between 55-60 minutes

// Java (HikariCP): maxLifetime already applies per-connection jitter automatically
// Python: use max_lifetime with a randomized offset in your pool factory

Expected outcome: Connection recycling spreads evenly over time across individual connections, keeping new connection requests well under the 100-per-second rate quota.

Strategy 3: Improve pool sizing

Amazon Aurora DSQL performs well with many concurrent connections, but you should size your pool to balance throughput, latency, and resource consumption.

Table 3: Recommended starting values for connection pool sizing parameters

Parameter Recommended Starting Value Rationale
Minimum pool size 2–5 connections Keeps pool warm without wasting resources at low load
Maximum pool size 10–20 per application instance Start conservative. Scale out by adding application instances
Connection timeout 30 seconds Allows the DSQL connection burst queue (100/sec) to drain during high-traffic events
Idle timeout Disabled (or match MaxConnLifetime) Idle connections have no cost. Keeping them open avoids unnecessary TLS/auth overhead on reconnection

Monitor DPU (Database Processing Unit) consumption in Amazon CloudWatch to detect when you need more capacity. Pre-warm pools before anticipated traffic spikes (for example, scheduled batch jobs or marketing campaigns). Scale out horizontally by adding more application instances with smaller pools, rather than increasing connections per instance. Amazon Aurora DSQL handles short-term load increases with vertical scaling and longer-term fluctuations with fleet-wide horizontal scaling. Take advantage of the 10,000 concurrent connections per cluster quota and contact AWS Support to increase it if needed.

Expected outcome: Pool sized correctly for your workload, minimizing both connection overhead and pool exhaustion errors.

Strategy 4: Multi-Region pooling considerations

For globally distributed applications using Amazon Aurora DSQL’s multi-Region active-active deployment, connection pooling requires additional thought.

The service’s multi-Region architecture keeps SQL execution, reads, and write spooling local to the client’s Region. Cross-Region communication only occurs at commit time through the Adjudicator and Journal replication protocols. Reads are local even in multi-Region mode, so read-only transactions complete with no cross-Region latency. Read-write transactions incur cross-Region latency only at COMMIT time, approximately 1–1.5 inter-Region round-trip times regardless of how many SQL statements were executed. This reduces latency compared to forwarding-based designs where each statement incurs a round-trip. Region selection matters: well-connected Region pairs typically offer lower commit latency, while more geographically distant pairings incur proportionally more.

For multi-Region pooling, create separate connection pools per Region, each targeting the local Amazon Aurora DSQL endpoint. Make sure your token refresh logic accounts for regional IAM endpoints. Design your application for active-active access. The service doesn’t expose a primary Region concept, so each Region operates symmetrically.

Expected outcome: Cross-Region latency is incurred only at commit time, with reads completing at local-region speeds.

Observability: Monitor your connection pool, not only the database

Aurora DSQL’s CloudWatch metrics (such as TotalTransactions or CommitLatency) tell you about database-level behavior, but they don’t reveal whether your connection pool is the bottleneck. To understand pool health, you need to instrument your application. The following client-side metrics are the key signals for connection pooling issues.

Metrics to instrument in your application

Metric What It Tells You Alert Threshold Action if Triggered
Pool acquire latency (.get() time) How long your application waits to get a connection from the pool P95 > 500 ms Pool is saturated — increase max pool size or scale out application instances
Active connection count Number of connections currently executing a transaction Sustained at max pool size All connections are in use — increase pool size or reduce transaction duration
Idle connection count Number of connections sitting unused in the pool Sustained at 0 No headroom for traffic bursts — increase min pool size or pre-warm before expected spikes
Request concurrency vs. pool size Ratio of concurrent in-flight requests to max pool size Concurrency > 80% of max pool size Approaching pool exhaustion — scale out or increase pool size
Pool exhaustion events Number of times a connection request timed out waiting for an available connection > 0 Immediate action needed — increase pool size, reduce transaction duration, or add application instances
Connection creation rate How many new connections the pool opens per second Approaching 100/sec Risk of hitting Aurora DSQL’s rate limit — add jitter, stagger instance starts, or increase min pool size to reduce churn

Most connection pool libraries expose these statistics natively:

  • Java (HikariCP): Use HikariPoolMXBean to access getActiveConnections(), getIdleConnections(), getThreadsAwaitingConnection(), and getTotalConnections(). Export through Micrometer to CloudWatch or your application performance monitoring (APM) tool.
  • Python (psycopg ConnectionPool): Use pool.get_stats(), which returns pool_min, pool_max, pool_size, pool_available, requests_waiting, and requests_num.
  • Node.js (node-postgres): Access pool.totalCount, pool.idleCount, and pool.waitingCount directly. Emit on an interval or per-request.
  • Go (pgxpool): Use pool.Stat(), which provides AcquireCount(), AcquiredConns(), IdleConns(), TotalConns(), and AcquireDuration().

Publish these as custom CloudWatch metrics (using the PutMetricData API or CloudWatch Embedded Metric Format in logs), or send them to your existing APM tool (such as AWS X-Ray, Datadog, or Prometheus/Grafana).

Supplementary Aurora DSQL CloudWatch metric

Although client-side metrics should be your primary signal, one Aurora DSQL metric remains useful for pool sizing decisions:

Metric Why It’s Useful for Pooling
TotalTransactions Correlate with your pool acquire latency — if transactions are growing but acquire latency is flat, your pool is sized correctly. If both are growing, you need more capacity.

Troubleshooting common connection pool issues

When alerts fire, use this table to diagnose and remediate the most common failure scenarios.

Table 5: Troubleshooting guide for common Amazon Aurora DSQL connection pool problems

Symptom Likely Cause Diagnostic Step Remediation
Pool exhaustion (connection timeout errors) maxPoolSize too low for traffic Monitor active vs. idle connections Increase max pool size or add application instances
Authentication failures after ~55 minutes Token expiry not handled Check token refresh logs Verify official connector is used. Check MaxConnLifetime setting
New connection rate errors Thundering herd at startup Check connection creation timestamps Add jitter to MaxConnLifetime. Stagger application instance starts

Quick reference: Key limits and settings

For up-to-date limits including connection lifetime, transaction timeout, connection rate, and concurrency, see Cluster quotas and database limits.

Conclusion

In this post, you learned how Amazon Aurora DSQL’s transaction-level pooling model differs from traditional PostgreSQL. You also learned four concrete strategies, from connector selection to multi-Region pooling, that keep your application performant and resilient. Don’t use database-side proxies such as PgBouncer or pgpool-II. The service handles transaction-level multiplexing natively. For multi-Region deployments, remember that cross-Region latency occurs only at commit time. Design your transactions to take advantage of local reads and writes.

By following these strategies, your application can take full advantage of Amazon Aurora DSQL’s automatic scaling, strong consistency, and high availability. This keeps connection overhead minimal and latency predictable at scale.


About the authors

Tejas Dubey

Tejas Dubey

Tejas is an AWS Technical Account Manager who helps enterprise customers build resilient, secure, and AI-ready cloud architectures. He’s passionate about turning emerging technology into real-world business value. Off the clock, he’s either chasing his son around, playing pickleball, or tinkering with the latest tech.

Dhvani Shah

Dhvani Shah

Dhvani is a Technical Account Manager supporting enterprise customers with strategic guidance on cloud resiliency, security, and generative AI adoption. She helps organizations architect scalable, secure solutions that accelerate digital transformation. Outside work, she enjoys traveling, playing badminton, and spending time with family.