RDS Too Many Connections: The max_connections Math
Amazon RDS sets your max_connections ceiling automatically from a formula tied to your instance's memory — not a round number Amazon picked, and not the same number MySQL or PostgreSQL would use installed on your own server. The counterintuitive part: when you hit that ceiling, cranking the number up is usually the wrong move, because every connection eats real memory, and a database that runs out of memory doesn't throw a polite error — it crashes.
Jake called on a Saturday morning, which is never a good sign. His shop's point-of-sale system — the one that logs every phone sale and trade-in against the customer database sitting on a small RDS instance — had frozen mid-sale. The error on the screen just said the database couldn't be reached. A customer stood at the counter holding a trade-in phone, and Jake had no way to close the sale.
He called Ethan, who pulled up the RDS console from his phone in about ninety seconds and found the answer sitting right in the error log: ERROR 1040 (HY000): Too many connections. Nothing was broken. The database was healthy. It had simply run out of connection slots, and it was refusing every new one — including the one Jake's POS software needed to close that sale.
What "too many connections" actually means
Every relational database has a hard ceiling on how many client sessions it will hold open at once. In MySQL and MariaDB the setting is called max_connections; in PostgreSQL it's also max_connections; Oracle splits the same idea into processes and sessions; SQL Server calls it user connections. Once the number of open sessions touches that ceiling, the engine stops accepting new ones and tells the next connection attempt no.
The exact wording depends on the engine. MySQL and Aurora MySQL show ERROR 1040 (HY000): Too many connections. PostgreSQL and Aurora PostgreSQL show FATAL: remaining connection slots are reserved for non-replication superuser connections — a longer message because PostgreSQL deliberately holds a handful of slots back for an administrator to get in and fix things, even when everything else is full. Oracle throws ORA-00020: maximum number of processes exceeded. Neither message means the database is unhealthy. It means every slot is occupied, and the engine is doing exactly what it's configured to do: protect itself from taking on more work than its memory can hold.
🙋♂️ Jake's Reality Check
Jake typed it into the shop's group chat while Ethan was still on the call: "So the fix is just… bump the number up, right? Set it to something huge so this never happens again?"
Ethan shook his head before Jake had even finished the sentence. "No," he said. "That instinct is the single most common mistake people make with this error. A higher max_connections doesn't create more memory. It just tells the database it's allowed to try to use memory it doesn't have — and that usually ends worse than a refused connection."
The max_connections math — how AWS actually calculates it
Unless you've explicitly overridden it in a custom parameter group, RDS doesn't use a fixed default for max_connections — it calculates one from a variable called DBInstanceClassMemory, which is the amount of memory (in bytes) available to the database engine on that instance class, after RDS has already carved out what it needs for the operating system and its own management processes. That subtraction matters: it's why the number you get is always smaller than what you'd predict from the instance's advertised RAM.
Each engine divides that memory figure by a different constant, and some cap the result. Here's the formula for every engine RDS supports, straight from AWS's own quotas documentation:
| Engine | Parameter | Default formula | Allowed range |
|---|---|---|---|
| MySQL / Aurora MySQL | max_connections | {DBInstanceClassMemory/12582880} | 1–100,000 |
| MariaDB | max_connections | Same style of memory-based formula | 1–100,000 |
| PostgreSQL / Aurora PostgreSQL | max_connections | LEAST({DBInstanceClassMemory/9531392},5000) | 6–262,143 |
| Oracle | processes | LEAST({DBInstanceClassMemory/9868951},20000) | 80–20,000 |
| Oracle | sessions | Derived from processes, not user-set | 100–65,535 |
| SQL Server | user connections | 0 (unlimited, engine default) | 0–32,767 |
| Db2 | — | Not user-configurable | Fixed limit of 64,000 |
Notice the shape of every formula: memory divided by a constant, sometimes wrapped in a "whichever is smaller" cap. MySQL divides by roughly 12 MB per connection slot, PostgreSQL by roughly 9 MB, and Oracle by a similar figure — but Oracle and PostgreSQL also refuse to let the number climb past a hard ceiling (20,000 and 5,000 respectively) no matter how much memory the instance has, because past that point the bookkeeping overhead of tracking that many slots becomes its own problem. SQL Server is the outlier: its default is 0, which actually means "unlimited," bounded only by server resources — a very different failure mode, which we'll come back to.
Why the number is lower than you'd guess
DBInstanceClassMemory is measured in bytes — and it is not the number of gibibytes (GiB) AWS advertises for the instance class. That advertised figure is total physical RAM on the box. DBInstanceClassMemory is what's left after RDS reserves a slice for the operating system and its own management agent, which is why the math always lands lower than a back-of-envelope calculation using the sticker RAM number.
AWS's own documentation walks through the gap using a MySQL instance with 8 GiB of memory — a class like db.m7g.large. Convert 8 GiB to bytes and you get 8,589,934,592. Divide that by the MySQL constant (12,582,880) and you'd expect roughly 683 connections. But because DBInstanceClassMemory has already had the OS and RDS-process reservation subtracted out before that division happens, the real-world result comes out closer to 630 — noticeably below the naive calculation. The gap is a fixed reservation, not a percentage, so it hits small instances the hardest.
That reservation is proportionally brutal on the smallest instance classes. On something like db.t3.micro, so much of the available memory goes to that baseline overhead that the default max_connections for MySQL lands around 60 — nowhere close to what a naive "gigabytes times some big number" guess would produce. If your app opens even a modest connection pool of 20–30 connections per instance and you're running a couple of app servers, you can burn through that ceiling before you've done anything wrong.
🕐 What changed between versions
- Before RDS Proxy existed (pre-2019), the only lever for connection pressure on a small instance was raising
max_connectionsby hand and hoping the memory held, or building your own pooling layer on EC2. - Now: RDS Proxy sits between the application and the database, pooling and multiplexing connections so the instance itself sees far fewer physical connections than the application opens.
- What that means for you: on genuinely bursty or serverless-style workloads, a pooler is usually a better lever to pull than the formula itself.
Do the math yourself, step by step
You don't have to trust a rule of thumb. You can pull the exact value your instance is running with right now, and you can work out roughly why it landed there.
- Connect to the database directly. For MySQL/MariaDB, run
SHOW VARIABLES LIKE 'max_connections';or the shorterSHOW max_connections;. For PostgreSQL, runSHOW max_connections;in psql, or querypg_settingswithSELECT name, setting FROM pg_settings WHERE name = 'max_connections';for the boot value versus the currently active one. - Note the number you get back. This is the real, currently-enforced ceiling — not an estimate. If you haven't touched the parameter group, it was calculated by the formula in the table above for your engine and instance class.
- Sanity-check it against your instance class. Look up your instance class's memory in GiB on the RDS instance-class page, convert to bytes (multiply by 1,073,741,824), and run it through the formula for your engine. Expect your real number to land a bit below this raw estimate — that gap is the OS/management reservation described above, and it's larger, proportionally, on smaller instance classes.
- If the numbers are wildly different — not just "a bit below" but off by a large margin — check whether someone has already set an explicit numeric override in your parameter group, rather than leaving the parameter on the
{DBInstanceClassMemory/…}formula. An explicit override replaces the formula entirely and won't move when you resize the instance.
Jake watched Ethan run the first command over screen share. The number that came back was 66. Jake's POS software, it turned out, opened a fresh connection for every single register transaction and never explicitly closed it — it just let the connection time out on its own, which on a busy Saturday meant dozens of "sleeping" connections stacking up in the background while real transactions kept trying to open new ones.
Finding out how close you actually are
Knowing the ceiling is only half the picture. You also need to know how close your live traffic is running to it, and whether the connections piling up are actually doing work or just sitting idle.
- Open the DatabaseConnections metric in CloudWatch. In the CloudWatch console, go to Metrics → All metrics → RDS → Per-Database Metrics, and find DatabaseConnections for your instance. This is the count AWS itself tracks, and it's the one to graph against your
max_connectionsvalue over the incident window — if the two lines meet, that's your smoking gun. - Cross-check FreeableMemory in the same window. If FreeableMemory was dropping toward zero before connections hit the ceiling, the real constraint isn't the connection count — it's that the instance ran low on memory for other reasons (a runaway query, an undersized buffer pool) and the connection errors are a symptom, not the disease.
- List what's actually connected, right now, from a working session. For MySQL/MariaDB, run
SHOW FULL PROCESSLIST;to see every connection, its state, and how long it's been sitting there. For PostgreSQL, runSELECT pid, state, query_start, state_change, query FROM pg_stat_activity ORDER BY state_change;. Look at thestatecolumn: a long list of MySQL connections stuck atSleep, or PostgreSQL sessions showingidleoridle in transaction, points at connections the application opened and never closed — not genuine traffic. - If you can't connect at all because every slot is full, pull the metric from the AWS CLI instead of the console:
aws cloudwatch get-metric-statistics --namespace AWS/RDS --metric-name DatabaseConnections --dimensions Name=DBInstanceIdentifier,Value=<your-instance-id> --start-time <ISO timestamp> --end-time <ISO timestamp> --period 60 --statistics Maximum. This works even when the instance itself is refusing new sessions, because it's reading from CloudWatch, not the database.
⚠️ What this actually breaks
A CLI or CLI-adjacent tool grabbing "just one more" connection to diagnose the problem can be the thing that finally pushes a database that's one slot away from full over the edge — and if you weren't a superuser on PostgreSQL, that diagnostic connection might be the one that gets refused, while a genuinely stuck admin connection sits there for you to kill.
Why connections pile up in the first place
Once you know how many connections are open and how many are actually doing work, you can usually sort the cause into one of a handful of buckets. AWS's own troubleshooting guidance for this error lists the same handful every time, because they cover almost every real case:
A genuine traffic increase
More users, a marketing push, a batch job that fired off a wave of parallel workers — the workload really did grow, and the connection count grew with it. This is the "good" reason, in the sense that the fix is capacity planning rather than a bug hunt.
A connection leak
The application opens connections and doesn't reliably close them at the end of an operation — often because of an exception path that skips the cleanup code, or a framework that assumes something else will close the handle. Jake's POS software was doing exactly this. Each new sale opened a connection; nothing closed the old ones; by Saturday afternoon the shop had accumulated enough sleeping connections to starve out the one new connection it actually needed.
Timeouts set too generously
MySQL's wait_timeout and interactive_timeout parameters control how long an idle connection is allowed to sit before the server closes it automatically. If those are set high (or left at a large default), a connection that would otherwise have been reclaimed in a minute or two instead sits open for hours, quietly occupying a slot.
Too many application instances, each with its own pool
This is the one container-based and auto-scaled workloads run into constantly. If your connection pool library is configured for, say, 10 connections per instance and you scale from 5 pods to 50 during a traffic spike, you just went from 50 potential connections to 500 — against a max_connections value that didn't move, because it's still tied to the same database instance class.
Locking that holds connections mid-transaction
Table- or row-level locks can leave sessions holding connections open while they wait their turn, rather than the connections being idle by choice. This looks similar to a leak in the connection count, but the fix is query tuning and lock analysis, not connection cleanup.
The fixes, cheapest to most drastic
Work down this list in order. Most incidents resolve in the first two or three steps — raising max_connections itself, or scaling the instance, should be near the bottom, not the first reflex.
| Fix | Cost / risk | Use it when |
|---|---|---|
| Kill idle/sleeping sessions | Free, immediate, no restart | You need slots back right now |
| Fix the app's connection pool sizing / leaks | A deploy, no infra change | Connections are idle, not genuinely busy |
Lower wait_timeout/interactive_timeout | Parameter-group change, minor | Idle connections linger for hours |
| Add RDS Proxy in front | Ongoing per-vCPU-hour charge | Many short-lived or bursty connections (Lambda, containers) |
Raise max_connections explicitly | Risk of OOM crash if set too high | Traffic genuinely grew and you have memory headroom to spare |
| Scale to a larger instance class | Highest ongoing cost, possible brief downtime | You're genuinely out of memory headroom for the workload, not just connections |
Step 1: free up slots immediately
If you can get in through any working session — often an admin connection, since PostgreSQL specifically reserves a few slots for superusers — you can terminate the connections that are just sitting idle. In MySQL/MariaDB, find the connection ID from SHOW FULL PROCESSLIST; and run KILL <id>;. In PostgreSQL, find the process ID from pg_stat_activity and run SELECT pg_terminate_backend(<pid>); for a hard stop, or pg_cancel_backend(<pid>) to cancel just the running query first. This buys you breathing room in seconds, but it's triage, not a fix — whatever opened those connections will open them again unless the underlying cause is addressed.
Step 2: fix the pool, not the database
Most connection-exhaustion incidents trace back to the application layer, not the database's memory. If your framework has a connection pool (HikariCP for Java, SQLAlchemy's pool for Python, most Node.js and Ruby ORMs, PDO's persistent connections for PHP), check three things: the maximum pool size per app instance, whether connections are being closed or returned to the pool on every code path (including error paths), and whether the pool's own idle timeout is shorter than the database's. A pool sized for "10 connections per instance" that scales out to 50 instances during a traffic spike will hit the wall long before the database is under any real load — the fix there is capping total pool size across the fleet, not raising max_connections to match.
Step 3: trim how long idle connections are allowed to linger
If diagnostic logging shows a lot of long-sleeping MySQL connections, lowering wait_timeout in a custom parameter group means the server reclaims abandoned connections faster instead of holding them open for hours. This is a modest, low-risk change, but it's also a band-aid on top of whatever is failing to close connections cleanly — it reduces the blast radius, it doesn't remove the leak.
Step 4: put RDS Proxy in front of the database
RDS Proxy is a fully managed connection pooler that sits between your application and the database, holding a smaller, stable pool of physical database connections while absorbing a much larger number of application-side connections on top of it. AWS specifically recommends it for applications that "frequently open and close connections, or keep a large number of long-lived connections open" — which describes serverless functions and heavily auto-scaled containers almost exactly. It's priced per vCPU-hour of the underlying database instance (per Aurora Capacity Unit-hour for Aurora Serverless), on top of the instance's own cost, so it's an ongoing line item, not a free fix — check the current rate on the RDS Proxy pricing page before committing.
✅ Why this is the one to use
For genuinely bursty or serverless-style traffic — Lambda functions, autoscaled containers, anything where the number of app instances swings wildly — RDS Proxy is the right lever more often than raising max_connections, because it decouples "connections the app makes" from "connections the database actually holds." Raising max_connections alone doesn't fix a pattern where the number of app-side connections is itself unbounded.
Step 5: raise max_connections explicitly (with a number, not a guess)
If you've ruled out a leak, confirmed the traffic growth is real, and you have memory headroom to spare, you can override the formula in a custom DB parameter group by setting max_connections to an explicit value instead of the {DBInstanceClassMemory/…} expression. Do this cautiously: PostgreSQL's own documentation is blunt that setting max_connections too high for your actual workload can leave the instance unable to start at all, throwing errors in the Postgres log. Raise it in modest increments, watch FreeableMemory after each change, and never treat the ceiling as free capacity — each additional connection reserves real memory whether or not it's ever used for a query.
Step 6: scale to a larger instance class
This is the most expensive lever, and it's the right one only when the underlying constraint really is total memory — not just the connection count. A bigger instance class raises DBInstanceClassMemory, which raises the formula-derived max_connections automatically (assuming you haven't overridden it with an explicit number), and gives every connection more headroom to actually do work without contending for buffer pool or cache memory. If you've already fixed the app-side leaks and you're still routinely running close to the ceiling under legitimate load, this is the honest answer, not a workaround.
The RDS Proxy connection pooling math
If you do add RDS Proxy, it introduces its own math on top of the database's max_connections, and it's worth understanding before you configure it. The proxy's MaxConnectionsPercent setting caps how many of the database's total connection slots the proxy is allowed to use, expressed as a percentage of max_connections. If your database allows 1,000 connections and you set MaxConnectionsPercent to 95, the proxy will open at most 950 connections to the database — leaving the remaining 50 for anything connecting directly, like an admin session.
MaxIdleConnectionsPercent works the same way but governs how many of those pooled connections the proxy is willing to leave open while idle, rather than returning them to the database. A database allowing 500 connections with MaxIdleConnectionsPercent set to 50% will let up to 250 sit idle in the pool before the proxy starts closing them back down. AWS's own guidance recommends leaving at least 30% headroom above your measured peak usage, because the proxy redistributes connection quotas internally and needs slack to do that without adding latency to new connection requests.
One genuinely surprising wrinkle: because MaxConnectionsPercent is a percentage of max_connections, and max_connections is itself calculated from DBInstanceClassMemory, resizing your database instance silently changes the proxy's effective connection pool size too — without you touching the proxy's configuration at all. That's convenient when you're scaling up, but it's worth remembering if you ever scale down and the proxy's pool shrinks along with it.
Connection storms after a failover
A Multi-AZ failover, a planned maintenance window, or an unplanned instance restart all do the same thing to your connection count: every application instance that was holding a connection open loses it at the same moment, and every one of them tries to reconnect within seconds of each other. That reconnection storm can spike DatabaseConnections far above your normal steady-state traffic for a short window, even though nothing about your actual workload changed. If your max_connections value is only sized for steady state, a failover can trip the same "too many connections" error a genuine traffic surge would — and it will happen at the worst possible time, in the middle of a failover you're already trying to recover from.
The fix here isn't a bigger max_connections value, it's jitter. Most modern connection pool libraries support randomized backoff on reconnect attempts specifically so that a thousand clients don't all retry in the same half-second window. If your pool doesn't support it natively, even a small random delay of a few hundred milliseconds before each reconnect attempt spreads the storm out enough that the database can absorb it without hitting the ceiling. Ethan's blunt about this one: "If your application reconnects instantly and simultaneously after every failover, you built a denial-of-service attack against your own database — Multi-AZ just triggers it for you, for free."
IAM database authentication and connection churn
IAM database authentication changes the shape of this problem too. When an application authenticates to RDS using IAM credentials instead of a database password, each connection still counts against the same max_connections ceiling — the authentication mechanism doesn't change the underlying limit. What it does change is how often connections tend to be opened and closed, because IAM auth tokens expire after fifteen minutes, so an application that isn't caching and reusing connections properly will often reconnect more frequently than one using a static password, simply to refresh its token. That higher churn rate doesn't raise the ceiling, but it does raise the odds of a leak showing up, because every reconnect is another opportunity for a connection to fail to close cleanly on an error path. If you're on IAM auth and seeing more connection churn than expected, check the token refresh logic before you look anywhere else.
Read replicas: each one has its own math
Read replicas complicate the math in a way that catches people off guard the first time. Each read replica is its own RDS instance, with its own instance class, and therefore its own independently calculated max_connections value, derived from its own DBInstanceClassMemory. Connections don't pool or share across a cluster — a primary and its replicas — a read replica running on a smaller instance class than the primary will have a correspondingly smaller connection ceiling, even though it's serving the same application traffic pattern.
It's common to see a primary sized generously for write traffic while the read replicas, sized down to save cost, end up with the tighter connection ceiling in the fleet — and become the first thing to throw "too many connections" during a read-heavy traffic spike, while the primary still has headroom to spare. If your application load-balances reads across replicas, size each replica's instance class with its own connection math in mind, not just the primary's.
RDS Proxy vs. PgBouncer vs. tuning your own pool
If you're weighing RDS Proxy against a self-hosted pooler like PgBouncer, or against just tuning your application's own connection pool harder, it helps to see the tradeoffs side by side rather than picking on reputation alone.
| Approach | What you get | What it costs you |
|---|---|---|
| Tune your app's own pool | Free, no new infrastructure | Doesn't help when the number of app instances itself is unbounded |
| PgBouncer (self-hosted) | Transaction-level pooling, no per-vCPU-hour fee | You run, patch, and build redundancy for it yourself |
| RDS Proxy | Fully managed, IAM/Secrets Manager integration, fails over with the database | Recurring per-vCPU-hour fee; some session-level features don't pool cleanly |
The same math plays out differently again for Lambda-backed applications, and it's worth spelling out because the pattern looks nothing like a traditional app server. Each Lambda invocation can, if the function isn't careful, open a brand-new database connection and never explicitly close it before the execution environment freezes, because Lambda's execution model doesn't guarantee a clean shutdown hook runs every time. Under low concurrency this is invisible. Under a burst of concurrent invocations, each one opening its own connection, the count can spike into the hundreds within seconds — far faster than a traditional autoscaling group of app servers would ramp up. This is precisely the scenario RDS Proxy's own documentation calls out by name, and it's also why raising max_connections is close to useless here: no formula-driven ceiling comfortably absorbs an unbounded, bursty fan-out of short-lived connections. Only a pooler that multiplexes many client connections onto a small, stable set of database connections actually solves it.
Engine-specific traps
PostgreSQL reserves slots for superusers. That's precisely why the PostgreSQL error message says "reserved for non-replication superuser connections" rather than just "connection refused" — PostgreSQL deliberately holds a small buffer back so an administrator can always get in to diagnose and fix a full instance, even when application traffic has consumed everything else. If your application connects as a superuser-equivalent role, you lose that safety margin entirely.
Oracle splits the limit into two different numbers. processes is the one you can actually configure, and it's what the memory-based formula controls. sessions is derived from processes rather than set independently, and it isn't something you tune directly — if you're hitting a session limit on Oracle, the fix is adjusting processes, not hunting for a separate sessions parameter.
SQL Server's default is unlimited — which is its own trap. A default of 0 means "no configured cap," bounded only by whatever memory and resources the instance actually has. That sounds safer than a hard formula-based ceiling, but it means SQL Server won't warn you with a clean "too many connections" error the way MySQL or PostgreSQL will — instead, you're more likely to see general resource exhaustion and performance degradation with no single clear signal pointing at connection count as the cause.
MariaDB follows the same style of memory-based formula as MySQL, with the same 1–100,000 allowed range, but the exact divisor is tuned slightly differently for MariaDB's own memory footprint per connection — don't assume a MariaDB instance and a MySQL instance of the identical class will land on the identical default.
Aurora's memory overhead is different from RDS
If you're comparing an Aurora MySQL instance to a standard RDS for MySQL instance of the same instance class, don't expect the same max_connections value. Aurora MySQL and RDS for MySQL carry different amounts of memory overhead, so the same DBInstanceClassMemory-driven formula produces different results even on identical hardware classes. On Aurora specifically, the smaller T2/T3 burstable instance classes get a noticeably lower connection ceiling than the memory-optimized R-class instances, and AWS's own documentation is explicit that this is intentional: the burstable classes are meant for development and test scenarios, not production traffic, and the lower connection limit reflects that.
Aurora's scaling behavior also matters here. A larger instance class raises the connection ceiling roughly in step with memory — R3, R4, and R5 instances see the connection limit climb by roughly 1,000 with each doubling of memory, while T2 and T3 instances see much smaller jumps of around 45 per size step. If you're chronically running close to the connection ceiling on a T-class instance in production, that's a strong signal you're on the wrong instance family for the workload, not just an undersized number.
Aurora Serverless v2 adds one more wrinkle worth knowing before you build on it. Its max_connections value is a static parameter, meaning a change to it only takes effect after an instance restart, and it's calculated from the memory implied by your maximum ACU setting, not whatever capacity the instance happens to be running at right now. That matters because a Serverless v2 cluster idling near its minimum ACU still reports the connection ceiling calculated from its maximum, which can be misleading if you're eyeballing the number and assuming it reflects current capacity. There's also a specific trap on PostgreSQL-compatible Serverless v2 instances: if you set a minimum capacity of 0 or 0.5 ACU, AWS caps max_connections at 2,000 regardless of how high your maximum ACU is set — and having a limit of 2,000 doesn't mean a database sitting at 0.5 ACU can actually service 2,000 real connections doing work. The connection-slot count and the compute capacity to use those slots are two different constraints. If your workload genuinely needs a high connection count, AWS's own guidance is to set a minimum capacity of 1 ACU or higher rather than relying on the capped default at the lowest tier.
Preventing this from coming back
Fixing today's incident and preventing the next one are different jobs. A few habits close the gap permanently rather than just buying time:
Put a connection pooling library in front of every application, on every language. On the Java side, HikariCP, Apache DBCP, or c3p0 all cap and reuse connections instead of opening a fresh one per request. On Python, SQLAlchemy's built-in pool does the same. On PHP, PDO persistent connections avoid the overhead of a full handshake per request. On Node.js, the mysql2 and equivalent PostgreSQL drivers both ship pooling out of the box — the mistake is usually not using it, not that it's unavailable.
Cap total pool size across the whole fleet, not per instance. If your app can autoscale from 5 instances to 50, a pool configured for "10 connections each" is really a promise of up to 500 connections to a database that might only allow a few hundred. Either size the per-instance pool down as a function of your maximum expected instance count, or move to RDS Proxy so the database-side connection count stops scaling linearly with your application fleet.
Turn on logging before you need it, not after. Enabling general_log and slow_query_log (MySQL) or the equivalent Postgres logging parameters through a custom parameter group, ahead of time, means that the next time connections spike you can identify the source IP addresses and query patterns immediately instead of reconstructing the timeline after the fact. One caution before you flip that switch: statement logging captures the actual SQL text of every query, which means real customer data — in Jake's case, names, phone numbers, and device identifiers tied to trade-ins — can end up sitting in plain text inside your database logs. Treat those logs with the same access controls as the database itself, turn logging off again once you've diagnosed the issue rather than leaving it running indefinitely, and never forward raw query logs to a third-party monitoring tool without checking what's actually captured in them first.
Automate the watch instead of checking by hand. For teams that want this monitored without any new AWS spend, a small scheduled script does the job:
- Pull the current max_connections value and the current DatabaseConnections reading in the same script, using the AWS CLI commands already covered above, and calculate the percentage of the ceiling currently in use.
- Set a threshold — 80% is a reasonable starting point for most workloads — and have the script post to whatever the team already uses for alerts (Slack, email, an SNS topic) rather than inventing a new notification channel nobody checks.
- Schedule it to run every few minutes during business hours at minimum, and consider running it continuously if the workload sees traffic outside normal hours — a Saturday-afternoon spike at a phone shop doesn't wait for a Monday-morning standup.
- Log every alert it sends, even if nobody acts on a given one immediately, because a pattern of near-misses at the same time of day or the same day of the week is often the clearest signal that a fix, not just a bigger number, is overdue.
Ethan's fix for Jake's shop, in the end, wasn't a bigger database. It was two lines in the POS software's database library, telling it to actually close the connection when a sale finished instead of letting it time out on its own. The instance stayed exactly the size it had always been. The connection count that used to creep toward 66 by Saturday afternoon now sits in the single digits.
Frequently asked questions
What is Amazon RDS max_connections?
max_connections is the hard ceiling on how many client sessions your RDS or Aurora database instance will hold open simultaneously. By default it isn't a fixed number Amazon chose; it's calculated from a formula based on your instance class's available memory (called DBInstanceClassMemory), so it changes automatically when you resize the instance, unless you've overridden it with an explicit value in a custom parameter group.
Why does raising max_connections to a huge number make things worse, not better?
Every open connection reserves memory on the instance, whether or not it's actually running a query. Setting max_connections far above what the instance's memory can comfortably support doesn't create more capacity — it lets the database try to hold open more connections than it has room for, which risks running the instance out of memory. An out-of-memory database doesn't politely refuse new connections the way a well-tuned max_connections value does; it can crash outright, which is a far worse outage than a connection error.
How do I check my current max_connections value?
Connect to the database and run SHOW max_connections; for MySQL, MariaDB, or PostgreSQL. This returns the actual, currently-enforced ceiling, whether it was set by the automatic formula or overridden explicitly in your parameter group.
How do I check how many connections are currently in use?
The most reliable source is the DatabaseConnections metric in Amazon CloudWatch, since it works even if the database itself is refusing new sessions. If you can connect, run SHOW FULL PROCESSLIST; on MySQL/MariaDB or query pg_stat_activity on PostgreSQL to see the live list, including which connections are idle versus actively running a query.
What does DBInstanceClassMemory actually mean?
It's the amount of memory, in bytes, that AWS considers available to the database engine on a given instance class — after subtracting what's reserved for the operating system and RDS's own management processes. It's always smaller than the advertised GiB figure for the instance class, which is why doing the formula math with the advertised RAM number overestimates the real max_connections value.
Why is my max_connections lower than I expected for my instance size?
Two reasons, usually stacking together. First, the OS/management memory reservation subtracted from DBInstanceClassMemory is a fixed amount, not a percentage, so it eats proportionally more of a small instance's memory than a large one's. Second, if you're on a burstable T-class instance, AWS designed those classes with lower connection ceilings on purpose, because they're intended for development and testing rather than production traffic at scale.
Can I set max_connections to a fixed number instead of the formula?
Yes. Create a custom DB parameter group, set max_connections to an explicit numeric value instead of the {DBInstanceClassMemory/…} expression, and apply that parameter group to your instance. Be conservative: an explicit value that's too high for the instance's actual memory can prevent the database from starting at all.
What happens if I exceed max_connections?
New connection attempts are refused with an engine-specific error — ERROR 1040: Too many connections on MySQL/Aurora MySQL, or a "remaining connection slots are reserved" message on PostgreSQL. Existing connections aren't affected; they keep running. Only new connection attempts fail until a slot frees up.
How do I kill idle connections without restarting the database?
On MySQL/MariaDB, find the connection ID from SHOW FULL PROCESSLIST; and run KILL <id>;. On PostgreSQL, find the process ID from pg_stat_activity and run SELECT pg_terminate_backend(<pid>);. Neither requires a restart, and both take effect immediately.
Does connection pooling in my app framework really help?
Yes, and it's usually the single highest-leverage fix available, because it addresses the actual cause — too many connections opened, or connections held open too long — rather than just raising the ceiling to accommodate bad behavior. A properly configured pool caps how many connections each app instance opens and reuses them across requests instead of opening a fresh one every time.
Should I use RDS Proxy or just raise max_connections?
It depends on the shape of your traffic. If the problem is that your application fleet itself opens an unbounded or highly variable number of connections — Lambda functions, autoscaled containers — RDS Proxy addresses that directly by pooling those connections down to a stable, smaller set against the database. Raising max_connections alone doesn't fix an unbounded app-side connection count; it just moves the point where things break.
How much does RDS Proxy cost extra?
RDS Proxy is priced separately from your database instance: per vCPU-hour of the underlying instance for provisioned engines, or per Aurora Capacity Unit-hour for Aurora Serverless. Partial hours bill in one-second increments with a ten-minute minimum after a billable status change. Check the current per-vCPU-hour rate on AWS's RDS Proxy pricing page for your region, since it's a separate line item from the instance cost.
Why do I still hit connection limits with a connection pooler in front?
Check your pooler's own configuration first. RDS Proxy's MaxConnectionsPercent setting caps how much of the database's total max_connections the proxy itself is allowed to use — if that percentage is set too low for your traffic, or too high with too little headroom, you can still see connection pressure even with pooling in place. AWS recommends at least 30% headroom above your measured peak usage for exactly this reason.
Is max_connections different for Aurora vs standard RDS?
Yes, even on an identical instance class. Aurora MySQL and RDS for MySQL carry different memory overhead, so the DBInstanceClassMemory-based formula produces different default connection ceilings for the same instance class name. Don't assume a number you measured on one will match the other.
Does upgrading my instance class raise max_connections automatically?
Yes, as long as you haven't overridden the parameter with an explicit numeric value. Because the default is a formula tied to DBInstanceClassMemory, a larger instance class with more available memory produces a higher calculated max_connections the moment it's applied — no manual parameter-group edit required.
What's a safe max_connections number for a small production database?
There isn't a single safe number that applies across instance classes and engines, because the whole point of the default formula is that "safe" is a function of available memory, not a fixed count. The more useful question is whether your current, formula-derived value is actually being approached by real, active traffic (via the DatabaseConnections metric) — if it is, look at fixing pool sizing and leaks before touching the number itself, and treat scaling the instance class as the honest answer once you've ruled those out.
Revision note. Written September 2026, covering the current RDS connection-limit formulas for MySQL, MariaDB, PostgreSQL, Oracle, and SQL Server, plus RDS Proxy's MaxConnectionsPercent/MaxIdleConnectionsPercent settings and Aurora Serverless v2's connection behavior, all as documented by AWS at the time of writing. AWS periodically adjusts instance-class memory reservations and RDS Proxy pricing, so it's worth double-checking the exact numbers on your own instance before making a change. If you're staring at a "too many connections" error right now, take a breath — it almost always has a calm, traceable cause, and you don't have to solve it by guessing.