Fixing AWS RDS Proxy Pinning That Drains Your Connection Pool Silently

July 03, 2026 9 min read 3 views

Your RDS Proxy is running. Connections look healthy at a glance. Then, under moderate load, your application starts timing out on database calls and you can't figure out why β€” because there are no errors, no alarms, and no obvious smoking gun in your logs. The culprit is almost always connection pinning.

Pinning is the mechanism where RDS Proxy is forced to dedicate a single backend database connection to a single client connection for the entire session. Multiplexing stops, your pool fills up, and new requests queue or fail β€” all silently.

What You'll Learn

  • Exactly what triggers RDS Proxy to pin a connection
  • How to find pinning events in CloudWatch before they become outages
  • Code-level fixes for the most common pinning causes: SET statements, prepared statements, and session state
  • How to set up ongoing monitoring so pinning never sneaks up on you again

What RDS Proxy Pinning Actually Is

RDS Proxy sits between your application and your database and multiplexes many client connections onto a smaller pool of real database connections. When a client isn't actively executing a query, its connection can be reused by another client. This is the entire value proposition of the proxy.

Pinning breaks that contract. When RDS Proxy detects session state it cannot safely transfer between clients β€” like an open transaction, a custom session variable, or a prepared statement handle β€” it pins that client to one specific backend connection. That connection is now unavailable to anyone else until the client disconnects or the session ends.

The key word is silently. The proxy does not return an error when pinning occurs. Your application continues to work, but your effective connection pool shrinks toward zero as more clients get pinned. Eventually, you hit the database's max_connections and everything falls apart at once.

Why Pinning Kills Your Pool Silently

Consider a service with 200 application instances, each maintaining a persistent connection through RDS Proxy. Under normal multiplexing, those 200 client connections might only hold 20 real database connections. If 80% of your clients get pinned due to a SET SESSION call somewhere in your ORM configuration, you now need up to 160 real connections for a load level that previously required 20.

Aurora MySQL defaults to a max_connections value derived from instance memory. A db.t3.medium allows roughly 90 connections. You do the math: 160 required connections against a hard ceiling of 90 means failures for 70 clients, with no clear error message explaining why.

This is also why the problem is insidious at low traffic. With 10 clients, even full pinning may not exhaust the pool. The bug ships to production, traffic grows, and one day the pool drains at 2 AM.

How to Detect Pinning Before It Takes Down Production

RDS Proxy exposes a CloudWatch metric called DatabaseConnectionsCurrentlySessionPinned. This is your primary detection tool. If this metric is non-zero during normal operation, you have a pinning problem worth investigating immediately.

Enable proxy logging first. In the AWS Console, go to your RDS Proxy, click Modify, and set the Connection borrow timeout and enable Enhanced logging. Or do it via CLI:

aws rds modify-db-proxy \
  --db-proxy-name your-proxy-name \
  --debug-logging true

With enhanced logging active, every pinning event writes a log line to CloudWatch Logs under /aws/rds/proxy/your-proxy-name. The line looks like this:

The client session was pinned to the database connection [connId=42] for the remainder of the session. The reason for pinning is: SET option not supported for multiplexing.

This tells you exactly which statement triggered pinning. Run a CloudWatch Logs Insights query to find the most frequent causes across your fleet:

fields @timestamp, @message
| filter @message like /pinned/
| parse @message "reason for pinning is: *" as reason
| stats count(*) as pin_count by reason
| sort pin_count desc
| limit 20

Sort by count and fix the top offenders first. You'll usually find one or two root causes covering 90% of your pinning events.

Common Causes of Pinning (and How to Reproduce Each)

RDS Proxy documents the operations that trigger pinning. In practice, these four cause the vast majority of real-world incidents.

  • SET statements β€” Any SET command that changes session state: SET NAMES, SET time_zone, SET SESSION transaction_isolation, etc.
  • Prepared statements β€” Native binary protocol prepared statements (not text-mode emulated ones) that maintain server-side state.
  • Temporary tables β€” Creating a TEMPORARY TABLE scopes it to the session; the proxy cannot hand that connection to anyone else.
  • Explicit transactions left open β€” An autocommit-disabled connection that begins a transaction but doesn't commit or roll back before the query completes.

ORMs are the most common source. SQLAlchemy's default MySQL dialect, for example, emits SET NAMES utf8mb4 and SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED on connection checkout if you haven't configured it explicitly. Django's MySQL backend does the same for time zones if USE_TZ = True and the database timezone doesn't match.

Fixing Pinning Caused by SET Statements and Transaction Isolation

The cleanest fix is to set these values at the parameter group level so the proxy doesn't see them as session-scoped changes. Open your RDS parameter group and set transaction_isolation, character_set_server, and time_zone to the values your application needs. Once they're server defaults, your ORM stops needing to emit SET at all.

For SQLAlchemy specifically, you can suppress the charset SET call by setting the charset in the connection string and disabling the auto-set behavior:

from sqlalchemy import create_engine, event

engine = create_engine(
    "mysql+pymysql://user:pass@proxy-endpoint:3306/mydb?charset=utf8mb4",
    # PyMySQL will NOT emit SET NAMES if charset is set in the URL
    # and you disable the init command:
    connect_args={"init_command": None},
)

For Django, set the database timezone at the server parameter group level to match your Django TIME_ZONE setting, then remove any CONN_HEALTH_CHECKS or OPTIONS keys that issue SET statements on connect. Confirm the fix by watching DatabaseConnectionsCurrentlySessionPinned drop to zero after deploying.

If you absolutely must issue a SET at runtime and cannot move it to the parameter group, consider reviewing how connection pool settings interact with session state to understand the full blast radius before accepting the pinning as a trade-off.

Fixing Pinning Caused by Prepared Statements

Prepared statements are trickier because they're often enabled by default in database drivers. MySQL's binary prepared statement protocol stores statement handles server-side, which the proxy cannot transfer between connections.

The simplest fix is to switch to emulated prepared statements, which the driver handles client-side in text mode. No server-side state is created, so no pinning occurs.

In PyMySQL:

engine = create_engine(
    "mysql+pymysql://user:pass@proxy-endpoint:3306/mydb",
    connect_args={"client_flag": 0},  # disables CLIENT_PS_MULTI_RESULTS
)

In the Node.js mysql2 driver:

import mysql from 'mysql2/promise';

const pool = mysql.createPool({
  host: 'your-proxy-endpoint',
  user: 'app_user',
  password: process.env.DB_PASSWORD,
  database: 'mydb',
  namedPlaceholders: true,
  // Use text protocol instead of binary prepared statements:
  enableKeepAlive: true,
  // mysql2 uses prepared statements by default; use execute() only
  // for truly repeated queries, or switch to query() for one-offs
});

// Prefer pool.query() over pool.execute() when you don't need
// the performance of binary prepared statements:
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [userId]);

With mysql2, pool.query() uses text protocol and avoids pinning. pool.execute() uses binary prepared statements and will pin. Use execute() only for queries you run thousands of times per second where the binary protocol's overhead savings are measurable.

For Java applications using JDBC, add useServerPrepStmts=false to your JDBC URL:

jdbc:mysql://proxy-endpoint:3306/mydb?useServerPrepStmts=false&cachePrepStmts=true

Setting cachePrepStmts=true alongside it lets the driver cache parsed statements client-side, preserving most of the performance benefit without triggering server-side pinning.

Fixing Pinning Caused by Application-Level Session State

Temporary tables and user-defined variables (SET @myvar = 1) pin the connection for the same reason: they're session-scoped state the proxy can't replicate. The fix here is architectural rather than a driver flag.

Replace temporary tables with CTEs or subqueries. Most queries that use CREATE TEMPORARY TABLE can be rewritten as a WITH clause (common table expression) or an inline subquery. CTEs exist entirely within the scope of a single query and generate no session state:

-- Before: pins the connection
CREATE TEMPORARY TABLE filtered_orders AS
  SELECT * FROM orders WHERE status = 'pending';
SELECT o.*, c.name FROM filtered_orders o JOIN customers c ON o.customer_id = c.id;

-- After: no session state, no pinning
WITH filtered_orders AS (
  SELECT * FROM orders WHERE status = 'pending'
)
SELECT o.*, c.name
FROM filtered_orders o
JOIN customers c ON o.customer_id = c.id;

Move user-defined variables into query parameters. If legacy code sets @user_id as a session variable and then reads it in a stored procedure, pass the value as a direct parameter instead. Stored procedures that internally SET user-defined variables will also trigger pinning, so audit any proc your application calls.

For open transactions, ensure autocommit is enabled at the connection level if you're not explicitly managing transactions. A missing COMMIT after an ORM operation is a common source of pinning in frameworks that start implicit transactions. This is especially worth auditing if you're using Django's ATOMIC_REQUESTS = True setting, which wraps every HTTP request in a transaction β€” you want those transactions to complete and close within the request cycle, not linger.

Monitoring Pinning Continuously in CloudWatch

Fixing pinning is step one. Knowing immediately when it comes back is step two. Set up a CloudWatch alarm on DatabaseConnectionsCurrentlySessionPinned with a threshold of greater than zero for at least two consecutive data points:

aws cloudwatch put-metric-alarm \
  --alarm-name rds-proxy-pinning-detected \
  --metric-name DatabaseConnectionsCurrentlySessionPinned \
  --namespace AWS/RDS \
  --dimensions Name=ProxyName,Value=your-proxy-name \
  --statistic Sum \
  --period 60 \
  --evaluation-periods 2 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:your-alerts-topic \
  --treat-missing-data notBreaching

Pair this with a second alarm on DatabaseConnectionsCurrentlyInTransaction. A spike there alongside a spike in pinning usually means open transactions, not SET statements β€” which changes your diagnosis path.

Also track ClientConnections versus DatabaseConnections. In a healthy multiplexing setup, the ratio of client connections to database connections should be well above 1:1. If that ratio approaches 1:1, multiplexing has effectively stopped regardless of whether the pinning metric has fired yet. This pattern is explored further in the context of how AWS services silently accumulate duplicate work when resource contention builds up undetected.

Keep enhanced logging enabled in staging permanently and rotate it on in production during incidents. The log volume is manageable and the diagnostic payoff is immediate. For broader AWS alarm reliability, it's worth reviewing why CloudWatch alarms sometimes report INSUFFICIENT_DATA so your pinning alarm doesn't silently fail to fire.

Wrapping Up

RDS Proxy pinning is a class of problem that scales invisibly: harmless at low traffic, catastrophic at scale, and nearly impossible to diagnose without knowing what to look for. Here are the concrete steps to take after reading this:

  1. Enable enhanced logging on your RDS Proxy in staging right now and run the CloudWatch Logs Insights query above to see whether pinning is already occurring.
  2. Set a CloudWatch alarm on DatabaseConnectionsCurrentlySessionPinned so you get paged before the pool drains.
  3. Audit your ORM's connection initialization for any SET statements and move those values to your RDS parameter group instead.
  4. Switch to text-mode prepared statements (or emulated prepared statements) in your database driver unless you have a measured reason to use binary protocol.
  5. Replace temporary tables in hot-path queries with CTEs, and verify no stored procedures set user-defined variables that linger across query boundaries.

Pinning usually has one or two dominant causes in any given codebase. Find yours with the log query, fix it at the source, and watch your connection pool efficiency recover immediately.

Frequently Asked Questions

How do I know if RDS Proxy is pinning connections and not just under-provisioned?

Enable enhanced logging on your RDS Proxy and check the CloudWatch Logs Insights query for log lines containing 'pinned' β€” each entry states the exact reason. If you see pinning events, the problem is session state, not instance size. Under-provisioning shows as high DatabaseConnections without pinning log entries.

Does switching to emulated prepared statements hurt query performance significantly?

For most workloads the difference is negligible. The binary prepared statement protocol saves round trips for queries run thousands of times per second; for typical OLTP queries run a few hundred times per second, the overhead difference is measured in single-digit microseconds. Profile your specific workload, but for the majority of applications the connection pool efficiency gain from eliminating pinning outweighs any driver-level overhead.

Can RDS Proxy pinning happen with PostgreSQL as well as MySQL?

Yes. RDS Proxy supports both MySQL-compatible and PostgreSQL-compatible engines and pinning can occur on both. The specific triggers differ slightly β€” PostgreSQL pinning is most commonly caused by SET commands, advisory locks, and LISTEN/NOTIFY usage, while MySQL is more commonly triggered by binary prepared statements and temporary tables.

Will restarting the RDS Proxy clear pinned connections?

No, you should not restart the proxy to clear pins β€” it would drop all active client connections. Pinned connections release naturally when the client disconnects or the session ends. The fix is to stop the application behavior that triggers pinning so new connections are no longer pinned going forward.

Is there a maximum number of connections RDS Proxy supports before multiplexing stops helping?

RDS Proxy enforces a MaxConnectionsPercent limit (defaulting to 100% of max_connections) as a ceiling on real database connections it will open. Once that ceiling is reached, new client connections queue until a real connection is freed. Pinning accelerates hitting that ceiling by preventing reuse, so reducing pinning directly increases effective capacity without changing instance size.

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.