RDS Proxy for Lambda: Timeout Defaults and Best Practices

2025-12-07 - 8 min read
Daniel Young
Daniel Young
Founder, DRYCodeWorks

RDS Proxy defaults: IdleClientTimeout 30 minutes, ConnectionBorrowTimeout 120 seconds. When to change each for Lambda, and how pinning caps concurrency.

Many small arrows converge into one manifold that feeds a few thick pipes into a database, one of them clamped

You've added RDS Proxy to your Lambda functions. The AWS documentation promises connection pooling, reduced database load, and better scalability. Everything works in development—then you hit production scale and start seeing intermittent errors that disappear on refresh:

text
The transaction associated with the current connection has completed but has not been disposed. The transaction must be disposed before the connection can be used to execute SQL statements.

Your database has capacity. Your Lambda functions aren't timing out. So what's happening?

The issue is typically how connections are managed between Lambda and RDS Proxy. The standard documentation covers the basics, but the nuances that matter at scale require deeper understanding of connection pinning, multiplexing behavior, and timeout interactions. This guide consolidates lessons from extensive load testing with RDS Proxy and SQL Server, covering the hybrid connection pattern, pinning avoidance strategies, and timeout configuration that work reliably under load.

Understanding RDS Proxy Connection Architecture

Understanding how RDS Proxy manages connections is essential before implementing best practices. The architecture involves more complexity than initial setup guides typically cover.

  • Client Connection: a connection from your Lambda function (or any application) to the RDS Proxy. Each Lambda invocation that needs database access establishes a client connection to the proxy.
  • Database Connection: a connection from the RDS Proxy to your actual RDS database instance. The proxy maintains a pool of these connections and shares them across multiple client connections.
  • Multiplexing: the process where RDS Proxy reuses database connections across multiple client connections. This is the ideal state that maximizes efficiency—many Lambda invocations can share a small pool of database connections.
  • Connection Pinning: a state where RDS Proxy locks a specific database connection to a single client connection for the duration of the session. While pinned, that database connection cannot be shared with other clients, reducing the benefits of connection pooling.

RDS Proxy maintains two distinct types of connections: client connections (from your Lambda to the proxy) and database connections (from the proxy to your RDS instance). This separation is the entire point of the proxy layer, but it creates nuances that catch developers off guard.

When your Lambda function connects to RDS Proxy, it establishes a client connection. The proxy then either assigns an existing database connection from its pool (called multiplexing) or creates a new one. Multiplexing is the ideal state—it's why you're using RDS Proxy in the first place. But in certain conditions, the proxy enters a state called connection pinning, where it locks a client connection to a specific database connection for the duration of that session.

Connection pinning occurs when "RDS Proxy can't be sure that it's safe to reuse a database connection outside of the current session." This is common, especially with SQL Server. Common triggers include prepared statements, temporary tables, session variables, and certain transaction isolation levels. Once pinned, that database connection remains unavailable to other Lambda invocations until the session ends.

SQL Server is particularly prone to session pinning. If you're using RDS Proxy with SQL Server, expect pinning to be the default behavior rather than the exception. Review the AWS documentation on avoiding pinning for your specific database engine.

Here's why this matters for Lambda: when you have high concurrency with frequent pinning, you can exhaust your database connection pool even though your database itself has plenty of capacity. Each pinned connection is locked to a single warm Lambda instance, potentially sitting idle while other invocations wait for available connections.

Lambda 1Lambda 2Lambda 3client connsRDS ProxyConnection Pool(multiplexing)db connsRDSMany connectionsFew connections

Connection Initialization Placement

A fundamental design decision for Lambda database access is where to initialize connections: inside the handler function or outside in the global scope.

The conventional wisdom recommends global scope to reuse connections across invocations. With RDS Proxy, however, this approach introduces edge cases that require careful handling.

Initializing Inside the Handler

When you initialize your connection inside the Lambda handler, you're creating client connection overhead on every single invocation. Your function must establish a new connection to RDS Proxy, perform the handshake, and then execute your query. This works reliably—you'll never have a stale connection—but it eliminates much of the performance benefit that RDS Proxy provides.

During our load testing, we measured this approach and found the overhead significant enough that we were essentially bypassing the proxy's connection pooling advantage. You're trading reliability for performance, and at scale, that performance hit adds up.

Initializing Outside the Handler

Placing the connection initialization in global scope preserves connections between invocations of the same Lambda instance. This is faster and more efficient, but introduces potential failure modes.

The problem emerges with warm Lambda instances that hold stale connections. Between invocations, Lambda freezes the execution environment and the client connection sits idle. Once it has been idle longer than the proxy's IdleClientTimeout (default 1,800 seconds), the proxy closes it, and RDS Proxy closes every client connection after 24 hours whether it's idle or not. A frozen function can't react to either, so it still believes it holds a valid connection.

Pinning makes that idle time expensive too. A pinned session keeps its database connection until the client connection is dropped, so a warm instance sitting idle on a pinned connection keeps a database connection out of the pool the whole time.

When that Lambda instance receives its next invocation and tries to reuse the connection, you get cryptic errors like "failed to resume transaction" or connection reset exceptions. These errors are intermittent, hard to reproduce in testing, and disappear when you retry—because the retry gets a different Lambda instance with a fresh connection.

  • High concurrency: Many Lambda instances competing for limited database connections
  • Frequent pinning: Queries that prevent connection multiplexing
  • Warm instance pool: Lambdas sitting idle with stale connections
  • Idle client timeouts: The proxy closing client connections while the Lambda instance is frozen and can't notice

This combination of factors leads to connection failures at scale.

The Hybrid Connection Pattern

The recommended approach combines both strategies: declare the connection in global scope but validate it before use in the handler.

This pattern gives you the performance benefits of connection reuse while protecting against stale connection errors.

Lambda InvokedConnectionexists?NoYesis_connection_valid?NoCreate connectionYesUse connectionExecute & Return

Here's the implementation:

python
import db_client

# Global scope - connection persists across invocations
db = None

def lambda_handler(event, context):
    global db

    # Validate connection before use
    if not db or not is_connection_valid(db):
        db = db_client.connect()

    # Use the validated connection
    result = db.execute_query(event['query'])
    return {
        'statusCode': 200,
        'body': result
    }

def is_connection_valid(connection):
    """
    Test if the connection is still alive and responsive.
    Returns False if connection is stale or broken.
    """
    try:
        connection.execute("SELECT 1")
        return True
    except Exception:
        return False

This approach ensures that:

  1. Warm Lambda instances reuse connections when they're valid (performance)
  2. Stale connections are detected and replaced (reliability)
  3. The validation query is lightweight (minimal overhead)
  4. New Lambda instances establish connections naturally

The key is the is_connection_valid() check. It catches stale connections before they cause errors in your actual query logic. The overhead of running SELECT 1 is negligible compared to the cost of handling connection errors mid-transaction.

Handling Timeouts and Error Scenarios

Database timeouts come in two flavors, and how you handle them depends on which type you're encountering.

Query Timeouts

If your database client reports that a specific query has timed out, there's no need to reset your connection to RDS Proxy. The connection itself is still valid—the query just took too long. You can retry the request or return an error to the caller, but the connection remains usable.

This is particularly important for operations that might occasionally hit slow query paths. Your connection validation will confirm the connection is still healthy, and you can continue using it for subsequent invocations.

Connection Timeouts

If the client reports that the connection itself has timed out (not just a query), this typically means the RDS Proxy closed the client connection due to it remaining idle beyond the idle timeout threshold. This is less common in practice but requires reconnection.

The hybrid pattern handles this automatically. Your is_connection_valid() check will fail, triggering a reconnection before your actual query runs. You avoid the error entirely rather than handling it after the fact.

Lambda Timeouts

Lambda function timeouts add another layer of complexity. When a function times out, Lambda resets the execution environment and shuts down the runtime, so a transaction in progress never gets a clean commit or rollback. The database connection behind it, attached for the transaction or pinned for the session, can't serve anyone else until the proxy sees that client connection drop. If it's never closed cleanly, IdleClientTimeout is the backstop that eventually reclaims it.

To mitigate this, enforce query-level timeouts that are shorter than your Lambda timeout. This gives you time to capture debugging information and gracefully close the connection before Lambda kills the function. For example, if your Lambda timeout is 30 seconds, set your database query timeout to 25 seconds.

python
def lambda_handler(event, context):
    global db

    if not db or not is_connection_valid(db):
        db = db_client.connect()

    # Set query timeout to leave buffer before Lambda timeout
    remaining_time = context.get_remaining_time_in_millis()
    query_timeout = max(1, (remaining_time - 5000) / 1000)  # 5s buffer

    try:
        result = db.execute_query(
            event['query'],
            timeout=query_timeout
        )
        return {'statusCode': 200, 'body': result}
    except TimeoutError:
        # Log debugging info before Lambda kills the function
        log_timeout_context(event, remaining_time)
        raise

Avoiding Connection Pinning

The most effective way to maximize RDS Proxy efficiency is to minimize connection pinning in the first place. This requires understanding which operations trigger pinning for your specific database engine.

AvailableIn UsePinnedTimed OutBorrowedReleased(multiplexed)State changedSession endedTimeoutReset

One universal trigger applies to all database engines: any SQL statement exceeding 16 KB in text size will cause pinning. This is easy to overlook when building dynamic queries or working with large IN clauses.

For SQL Server, common pinning triggers include:

  • Prepared statements: Use parameterized queries but avoid explicitly preparing statements when possible
  • Temporary tables and cursors: Prefer CTEs (Common Table Expressions) or table variables instead of temp tables, and avoid server-side cursors
  • Session variables: Avoid SET statements that change session state. For SQL Server, RDS Proxy tracks a short list without pinning, including SET ANSI_NULLS, SET QUOTED_IDENTIFIER and SET TRANSACTION ISOLATION LEVEL; others, such as SET NOCOUNT and SET XACT_ABORT, pin the session. Move common SET statements to the proxy's initialization query instead.
  • Transaction isolation changes: Stick with default isolation levels unless absolutely necessary
  • MARS (Multiple Active Result Sets): Avoid if possible
  • Distributed transactions: Avoid Distributed Transaction Coordinator (DTC) communication

Where pinning is unavoidable, at least be intentional about it. If you know a particular query will pin, structure your Lambda to complete all related operations in a single invocation rather than spreading them across multiple invocations that could land on different instances.

Where to Start?

  1. Audit your queries for common pinning triggers in your database engine
  2. Refactor queries to use multiplexing-friendly patterns (CTEs instead of temp tables, etc.)
  3. Monitor RDS Proxy metrics for DatabaseConnectionsCurrentlySessionPinned
  4. If pinning is unavoidable, ensure your connection limits account for worst-case concurrency

Configuration Guidelines

Your RDS Proxy configuration plays a critical role in how well the hybrid connection pattern performs.

Connection Pool Settings

Set your maximum connections based on your database instance capacity and expected Lambda concurrency. A common mistake is setting this too high, which can overwhelm your database during traffic spikes.

Unlike MySQL and PostgreSQL—where RDS sets a default max_connections limit based on instance memory—SQL Server has no such default. Its equivalent setting, user connections, defaults to 0 (unlimited), meaning your database will accept connections until it runs out of resources. For production workloads, explicitly configure this parameter to a value appropriate for your instance size to prevent resource exhaustion during traffic spikes.

For SQL Server specifically, remember that each connection has overhead. Keep MaxConnectionsPercent below 100 so direct connections for debugging or administrative tasks still fit. AWS also recommends setting it at least 30% above your peak monitored usage, to absorb changes in your workload and the proxy's internal rebalancing of connections across its nodes.

RDS Proxy Timeout and Pool Defaults

Four settings control how the proxy holds and hands out connections. These are the defaults from the RDS API reference:

SettingDefaultWhat it controls
IdleClientTimeout1,800 seconds (30 minutes)How long a client connection can sit idle before the proxy closes it
ConnectionBorrowTimeout120 seconds (range 0–300)How long a client waits for a pooled database connection when none is free
MaxConnectionsPercent100 (10 for SQL Server)The pool's ceiling, as a percentage of the database's max_connections
MaxIdleConnectionsPercentHalf of MaxConnectionsPercent; if that's unset, 50 (5 for SQL Server)Upper limit on idle database connections in the pool. It's a cap, not a floor: the proxy doesn't hold connections open to reach it

For Lambda workloads with the hybrid pattern, we recommend:

  • Lower IdleClientTimeout below the 1,800s default if pinned connections from idle warm instances are tying up the pool. A pinned database connection stays with its client connection until that connection closes, and a frozen instance can't close it, so a shorter timeout frees it sooner. The cost is more reconnects from instances that sit idle longer than the timeout
  • Keep ConnectionBorrowTimeout at the default 120s unless you want clients to fail faster when the pool is exhausted. It limits how long a client waits for a pooled connection; it doesn't reclaim connections that idle or timed-out functions are holding
  • For SQL Server, consider raising MaxIdleConnectionsPercent above the default if your traffic is bursty or unpredictable, so the proxy can absorb a surge without opening many new database connections

Don't set timeouts based on wishful thinking. If your queries regularly hold connections for 90 seconds, a saturated pool can leave other clients waiting past a 120-second borrow timeout. Either optimize your queries or increase the timeout to match reality.

Reserved Concurrency

In our testing, we found that reducing reserved concurrency on our Lambda functions helped alleviate connection pool exhaustion during high pinning scenarios. This seems counterintuitive—why limit concurrency when you're trying to scale?

The answer is that with connection pinning, more concurrency doesn't equal more throughput. It just means more Lambda instances competing for the same limited pool of database connections. By throttling Lambda concurrency to match your available connections, you get more predictable performance and fewer timeout errors.

To size it, use Little's law: the number of connections in use at once is roughly the request rate multiplied by how long each request holds a connection. Lambda's own concurrency estimate has the same shape: requests per second × average duration in seconds. With heavy pinning, a request holds its connection for the life of the client connection, not one transaction, and a warm instance keeps its pinned connection between invocations until IdleClientTimeout closes it. So each concurrent Lambda instance holds one database connection, and reserved concurrency becomes your connection cap.

Keep reserved concurrency at or below the number of connections the proxy can open (the database's connection limit × MaxConnectionsPercent), with the 30% headroom AWS recommends for peak load. A proxy allowed 200 database connections supports roughly 150 concurrently pinned instances.

Monitoring and Debugging

The following CloudWatch metrics are essential for diagnosing connection issues:

  • DatabaseConnectionsCurrentlySessionPinned: High values indicate you're not benefiting from multiplexing
  • DatabaseConnectionsCurrentlyInTransaction: Should correlate with active Lambda invocations
  • ClientConnections: Total client connections to the proxy
  • DatabaseConnections: Total database connections from the proxy
  • QueryDatabaseResponseLatency: End-to-end query time through the proxy

Set up CloudWatch alarms for:

  • Session pinning percentage above 50%
  • Client connections approaching max connections
  • Query latency spikes that correlate with high concurrency

When debugging connection errors, log the Lambda request ID, the query being executed, and the connection state. This correlation is invaluable for reproducing issues in testing.

Putting It Into Practice

If you're experiencing intermittent connection errors with RDS Proxy and Lambda, here's your action plan:

  • Are you initializing connections outside the handler without validation? This is the most common cause of "failed to resume transaction" errors.
  • Do you know which of your queries trigger session pinning? Check AWS documentation for your engine and audit accordingly.
  • Do your query timeouts fire before your Lambda timeout? A function killed mid-transaction leaves its database connection tied up until the proxy sees the client connection drop.
  • Is your connection pool sized appropriately for your concurrency? Too small causes contention; too large overwhelms your database.

Start by implementing the hybrid connection pattern. This single change eliminates the majority of stale connection errors we encountered during testing. Then audit your queries for pinning triggers and refactor where feasible.

These patterns aren't theoretical. We've packaged them into a database-helper library that's now used across multiple production repositories handling thousands of requests per minute. The investment in getting this right pays dividends in reduced error rates and simplified debugging.

If you're chasing the same intermittent errors, start with idle and pinned client connections. The borrow timeout only decides how long a client waits for a pooled connection, so it's rarely the culprit.


Resources