Skip to content

RDS too many connections: fixing it now and preventing it properly

The RDS too many connections error means the engine hit max_connections. Here is how to free connections immediately, find what is leaking them, and prevent the recurrence with pooling.

ERROR 1040: Too many connections on MySQL or FATAL: remaining connection slots are reserved on Postgres means the instance hit its max_connections ceiling and is refusing new sessions. The immediate fix is to get in through a session that still works and kill idle connections; the real fix is almost always in the application — a connection pool sized wrong, a leak, or too many app instances each holding their own pool. Raising max_connections buys time but trades memory for it.

What’s happening

Every RDS engine has a max_connections limit, and by default it is derived from instance memory. On RDS MySQL the default formula is {DBInstanceClassMemory/12582912}; on RDS Postgres it is LEAST({DBInstanceClassMemory/9531392}, 5000). A db.t3.medium therefore lands somewhere in the low hundreds, and a busy application with a misconfigured pool can exhaust that in seconds. RDS reserves a few slots for its own management connections, which is why Postgres refuses you slightly before the number you calculated.

The ceiling exists for a reason: each connection costs memory. On MySQL, per-session buffers (sort_buffer_size, join_buffer_size, thread stacks) multiply by connection count. On Postgres, every connection is a full OS process. Cranking max_connections to 5000 on a small instance does not give you 5000 usable connections — it gives you an out-of-memory crash under load instead of a connection error, which is worse.

The usual sources of exhaustion, in order of frequency we see them: an application pool with max set too high multiplied across many app instances or Lambda concurrency; connections opened per-request and never closed (a leak); long-running transactions holding sessions that the pool then works around by opening more; and a traffic spike that scaled the app tier while the database limit stayed fixed.

What to check first

  • Can you still get in? Try connecting as the master user. If even that fails, you may need to reboot — but try the checks below first from any session that is already open.
  • The DatabaseConnections metric in RDS > Databases > your-instance > Monitoring — is this a slow climb over days (leak) or a vertical spike (deploy, scale-out, retry storm)?
  • What deployed recently. A new app version, a Lambda trigger, or an autoscaling event that added app instances is the most common trigger.
  • Idle vs active. Hundreds of sessions sitting idle points at pool sizing or leaks; hundreds active points at slow queries piling up.

How to verify

Pull the recent connection count from CloudWatch:

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name DatabaseConnections \
  --dimensions Name=DBInstanceIdentifier,Value=prod-db \
  --start-time 2026-07-04T00:00:00Z --end-time 2026-07-04T12:00:00Z \
  --period 300 --statistics Maximum

Check what the limit actually is on your parameter group:

aws rds describe-db-parameters \
  --db-parameter-group-name prod-mysql8 \
  --query "Parameters[?ParameterName=='max_connections'].[ParameterValue,Source]"

Then, from a database session, see who is holding the slots:

-- MySQL: count sessions by user, host, and state
SELECT user, SUBSTRING_INDEX(host, ':', 1) AS host, command, COUNT(*)
FROM information_schema.processlist
GROUP BY user, host, command ORDER BY COUNT(*) DESC;

-- Postgres: count sessions by application and state
SELECT usename, application_name, state, COUNT(*)
FROM pg_stat_activity
GROUP BY usename, application_name, state ORDER BY COUNT(*) DESC;

A pile of Sleep (MySQL) or idle (Postgres) sessions from one application user is your smoking gun.

The runbook

  1. Free slots without a reboot. On MySQL, kill idle sessions with CALL mysql.rds_kill(<thread_id>); — RDS does not grant SUPER, so the plain KILL statement will fail for other users’ threads. On Postgres, run SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle' AND state_change < now() - interval '10 minutes';. This restores service in seconds.
  2. Stop the bleeding at the source. If a specific deploy or Lambda caused the spike, roll it back or cap its concurrency. Killing sessions is pointless while the source keeps opening new ones.
  3. Right-size the application pools. Total pool max across all app instances must stay comfortably under max_connections minus a headroom of 10-15% for admin, migrations, and monitoring. A pool of 20 per instance across 15 instances is 300 connections — do that math explicitly.
  4. Set idle timeouts on both ends. Pool idle timeout in the app, plus wait_timeout (MySQL) or idle_in_transaction_session_timeout and idle_session_timeout (Postgres 14+) in the parameter group, so leaked sessions die on their own.
  5. Raise max_connections only with the memory math done. A modest bump via a custom parameter group is fine if freeable memory supports it; the parameter is dynamic on both engines so no reboot is needed. But it is a buffer, not a fix.
  6. Put RDS Proxy in front for spiky or serverless workloads. Proxy multiplexes thousands of client connections onto a small pool of database connections and holds them through failovers. We covered when it helps and when it adds latency in RDS Proxy connection pooling.

When this is the wrong move

If connections are exhausted because queries are slow — sessions stack up waiting on locks or full scans — killing them and raising limits just resets the timer. Fix the query problem first; the connection count is the symptom. Likewise, scaling the instance class purely to inherit a higher default max_connections is an expensive way to avoid pool configuration you could do today for free. The official RDS documentation covers the per-engine parameter formulas if you want to verify the defaults for your class.

Common mistakes

  • Setting max_connections to a huge static number and then hitting out-of-memory crashes under real load — a harder failure than the one you fixed.
  • Counting one pool, not all pools: every app instance, worker, cron job, and Lambda copy holds its own pool.
  • Rebooting the instance as the first move — it drops all sessions, interrupts in-flight writes, and the leak refills the slots within minutes.
  • Ignoring Sleep/idle sessions because “they are not doing anything” — an idle session holds a slot and its memory just the same.

Prevention

  • Alarm on DatabaseConnections at ~80% of your actual max_connections value, not a round number.
  • Document the connection budget: limit, headroom, and pool max per service, reviewed whenever the app tier scales.
  • Use RDS Proxy for Lambda and any workload with elastic concurrency.
  • Load-test the connection path, not just query throughput — pools behave differently at saturation.
  • Keep an admin session path that does not go through the app pool, so you can always get in during an incident.

Connection exhaustion is one of the most recoverable RDS incidents when someone knows the runbook, and one of the most repeated when nobody owns the pool math. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.