Skip to content
All writing

Serverless scales. Your database does not.

Auto-scaling compute sounds like the whole answer until every one of those functions opens its own database connection. The ceiling is not your CPU, it is your connection limit.

4 min read
serverlessawsdatabasesarchitecturemongodblambda

If you are new to architecting on cloud, serverless is genuinely tempting. Auto-scaling without thinking about machines. Pay for what you use. No capacity planning. It reads like the problem is solved.

What the getting-started guide does not tell you is that beyond the usual caveats, cold starts, per-invocation pricing, execution time limits, there is a much sharper edge waiting: your database.

The arithmetic nobody does up front

Traditional applications hold a connection pool. One long-lived process, some fixed number of connections, reused across thousands of requests. The pool is the thing that protects the database from the application.

Serverless has no long-lived process to hold a pool. Each concurrent execution is its own isolated environment, and each one opens its own connection.

So the arithmetic becomes:

5,000 requests/second
  → 5,000 concurrent function executions
  → 5,000 database connections

And your database maxes out at 3,000.

Those other 2,000 requests do not queue politely. They fail. And they fail in the worst possible way, because your compute layer is behaving perfectly. Every dashboard you have says the functions are healthy, scaling as designed, doing exactly what you asked. The failure is at a layer most function-level monitoring does not watch.

Your compute scaled linearly. Your database did not scale at all.

What it actually looks like in production

I hit this properly at Techception, on an interactive video wall platform serving 165M+ requests.

MongoDB connections had climbed past 1,800 and were headed for the ceiling. The symptoms arrived before the diagnosis did: p95 latency creeping up, timeouts clustering under load, errors that would not reproduce in staging because staging never generated enough concurrency to matter.

That last part is what makes this class of bug expensive. It is invisible until you have traffic, and by the time you have traffic it is production.

Connections came down from 1,800 to 400, a 78% reduction, and p95 response time dropped 45%. The write-up of how is here.

The ways out

There are three, and picking between them is really a question about how well you know your traffic.

1. A connection proxy. RDS Proxy, PgBouncer, or the equivalent in front of your database. It holds the real pool and multiplexes your thousands of ephemeral clients onto a small, stable set of backend connections. This is the least invasive fix and usually the right first move.

The caveat is that multiplexing is not free or transparent. Transactions, session state, and prepared statements all pin a client to a backend connection for the duration, which cuts into the multiplexing you were buying.

2. Move to containers you control. Long-lived processes, real pools, known concurrency. This is what we ended up doing: re-architecting from serverless to a Dockerized system, which took availability to 99.9%. You give up the scale-to-zero story and get back predictability.

3. Cap the concurrency. Reserved concurrency on the function, so it can never open more connections than the database can take. This is less a fix than a choice about where to fail: bounded queueing instead of unbounded connection exhaustion. Sometimes that is the right call, and it is always better than discovering the limit at 3am.

The question underneath

Here is what kept nagging at me afterwards.

To size a connection proxy properly, or to set reserved concurrency correctly, you need to know your traffic patterns. Peak concurrency, burst shape, how long queries hold connections.

But if you know your traffic that well, what were you buying with serverless?

The pitch for serverless is that you do not have to think about capacity. The fix for its most common production failure is to think very carefully about capacity.

I do not think that makes serverless wrong. It makes it a poor default. It is excellent for genuinely spiky, unpredictable, event-driven work where the cost of idle capacity is real. It is a bad fit for steady request-response traffic against a relational database, which is most applications, and which is also most of what gets built on it.

Choose it because the traffic shape justifies it, not because it sounds like less operational work. It is not less. It is different work, and it arrives later, with less warning.

Just some thoughts from the trenches.

Written by Shahid Hameed Chaudhary, AI-First Software Engineer in Islamabad, Pakistan. If any of this is useful, or wrong, I would like to hear about it: shahidhameed.work@gmail.com.