/user/kayd @ devops :~$ cat lambda-million-users-per-hour.md

Handling a Million Users an Hour with AWS Lambda (Think Like a System Designer) Handling a Million Users an Hour with AWS Lambda (Think Like a System Designer)

QR Code linking to: Handling a Million Users an Hour with AWS Lambda (Think Like a System Designer)
Karandeep Singh
Karandeep Singh
• 9 minutes

Summary

A system-designer’s walkthrough of running a million users an hour on AWS Lambda, the capacity math, concurrency and cold starts, which language to pick, the database bottleneck that actually breaks first, and how to handle logs and metrics without the bill or the noise getting out of hand.

“Can it handle a million users an hour?” is a system-design question, not a Lambda question. Lambda will happily scale, the interesting part is everything around it: how much concurrency you actually need, what breaks first (spoiler: not Lambda), which language keeps you fast and cheap, and how to see what’s happening through logs and metrics without drowning in noise or cost.

This is how I’d reason through it, step by step, the way you’d whiteboard it in a design review.

Step 1: Do the capacity math first

Never start with services. Start with numbers.

1,000,000 requests ÷ 3,600 seconds ≈ 278 requests/second on average.

But traffic is never flat. A launch, a campaign, or a 9 a.m. login rush peaks 3-5x the average, so design for ~1,000-1,500 RPS peak, not 278.

Now the number that actually matters on Lambda: concurrency. By Little’s Law:

Concurrency ≈ requests/second × average duration (seconds)

Avg durationConcurrency at 1,500 RPSVerdict
100 ms~150Comfortable
500 ms~750Near the default 1,000 limit
1 s~1,500Exceeds the default limit
3 s~4,500Needs a big limit increase + rethink

Two lessons fall out immediately:

  1. Duration is your most powerful lever. Halving execution time halves the concurrency you need, which lowers cost and keeps you under limits. Fast code and fast downstreams aren’t nice-to-haves; they’re the design.
  2. You must know your limits before launch. Lambda’s default account concurrency is 1,000 (a soft limit) with a regional burst ceiling. If your math says you’ll cross it, request an increase days ahead — not during the incident.

Step 2: Is Lambda even the right tool here?

Think about the shape of the load, not just the size.

  • Spiky, event-driven, or unpredictable (launches, webhooks, bursty APIs) → Lambda shines. You pay only for what runs and it scales to the burst automatically.
  • Sustained, high, predictable throughput 24/7 → do the math against Fargate/ECS or EKS. Past a certain steady volume, always-on containers get cheaper than per-invocation billing.

“A million users an hour” for one hour a day is a perfect Lambda story. “A million an hour, every hour, forever” is where you at least compare containers. Decide this on purpose.

Step 3: Which language? (it changes cold starts and cost)

Language choice affects cold-start latency, memory footprint, and therefore cost. Here’s the honest comparison for a high-throughput API:

LanguageCold startRuntime speed / memoryBest when
Node.js / TypeScriptFastGreat for I/O-bound APIsDefault for most web APIs; huge ecosystem, quick to ship
PythonFastGreat for I/O, glue, dataFast to build; watch heavy imports (they slow cold start)
GoVery fastLow memory, fast, compiledHigh concurrency, low latency, cost-sensitive at scale
RustFastestLowest memory, fastestSqueezing max performance / min cost; steeper curve
Java / .NETSlow (JVM/CLR init)Fast once warmEnterprise code; use SnapStart / provisioned concurrency

My rule of thumb: Node.js or Python when developer speed wins and the work is I/O-bound (which most APIs are), Go or Rust when you need the lowest latency and cost at scale. Java/.NET are fine if you pair them with SnapStart or provisioned concurrency so cold starts don’t hurt the tail. If you want a worked example, see my build and deploy a Go Lambda function walkthrough.

Step 4: Cold starts, and how to make them a non-issue

A cold start is the one-time cost of spinning up a new execution environment (download code, init runtime, run your top-level setup). It only hits the first request on each new environment, but at a burst that’s a lot of firsts.

What to do:

  • Keep the package small. Fewer/lighter dependencies = faster init. Lazy-load what you don’t always need.
  • Do heavy setup once, outside the handler (reuse DB clients/connections across invocations on the same warm environment).
  • Provisioned concurrency for latency-critical paths, pre-warmed environments, no cold start, at a fixed cost.
  • SnapStart for Java, restores from a snapshot instead of a full JVM boot.
  • Pick a fast runtime (Step 3) for anything on the critical path.

Step 5: The thing that actually breaks first, the database

Here’s the senior insight that separates a working design from an outage: Lambda scales to thousands of concurrent executions, but your relational database does not scale its connection count to match. A thousand warm Lambdas each opening a Postgres connection will exhaust the database long before Lambda hits any limit.

Your options, in order of how well they fit serverless:

  • DynamoDB — scales with Lambda, no connection pool to exhaust. If the access pattern fits key-value/document, this is the path of least resistance at this scale.
  • RDS Proxy — if you need PostgreSQL/MySQL, put RDS Proxy in front to pool and share connections, so 1,000 Lambdas share a small, sane pool. Non-negotiable for relational + Lambda at scale.
  • Cache aggressively — ElastiCache (Redis/Valkey), DAX for DynamoDB, or CloudFront in front of read-heavy endpoints. The cheapest query is the one you never run.

I go deeper on this exact failure mode in how to scale a database in microservices. If you take one thing from this article: size the datastore for the concurrency, not the average.

Step 6: Absorb the spikes, go asynchronous where you can

Not every request needs a synchronous answer. If the client only needs an acknowledgement (a signup, an event, an upload notification), put a queue in the middle:

API Gateway / Function URL  →  SQS  →  Lambda  →  DynamoDB

SQS in front of Lambda turns a spike into a smooth, buffered stream. It gives you:

  • Back-pressure — the queue absorbs the burst so downstreams aren’t hammered.
  • Retries + Dead Letter Queues — failures don’t vanish; they park in a DLQ for reprocessing.
  • Cost control — batch records per invocation instead of one-invocation-per-event.

For the front door, choose deliberately: HTTP API (cheaper, simpler) vs REST API (more features, pricier) vs ALB (cost-effective at very high volume) vs Lambda Function URLs (simplest, no API layer). At a million/hour, the API Gateway line item is real, run the numbers.

And make handlers idempotent — retries will happen, so processing the same message twice must not double-charge a customer or double-write a row.

Step 7: Logging at scale, without the bill or the noise

Lambda ships stdout/stderr to CloudWatch Logs automatically. At a million invocations an hour that convenience becomes a cost and a needle-in-haystack problem.

Do this:

  • Log structured JSON, not strings. {"level":"error","userId":123,"latency_ms":812} is queryable; "something failed for user 123" is not. Use Lambda Powertools (Python/Node/Java/.NET) to get structured logging, correlation IDs, and sampling for free.
  • Sample, don’t firehose. At 1M/hour you cannot afford INFO on every call. Log errors and a sample of successes; raise verbosity only when debugging.
  • Set retention. Log groups default to never expire. Set 7-14 days for hot logs; archive the rest.
  • Ship cheaply for long-term/search. A CloudWatch Logs subscription filter → Firehose → S3 (or OpenSearch) is far cheaper to retain and search than leaving everything in CloudWatch.

Step 8: Metrics, see trouble before your users do

CloudWatch gives you Lambda metrics out of the box. The ones a system designer watches:

MetricWhy it mattersAlarm when
ThrottlesYou’re hitting a concurrency limit; requests are being rejected> 0 (any throttle is a design signal)
ErrorsFunction failuresError rate above your SLO (e.g., > 1%)
Duration (p99)Tail latency and your concurrency driverp99 above your latency budget
ConcurrentExecutionsHow close you are to the ceilingApproaching your account/reserved limit
IteratorAge (streams/SQS)You’re falling behind on the queueRising steadily = consumers too slow

Beyond the built-ins:

  • Custom business metrics via EMF (Embedded Metric Format). Emit metrics through logs (Powertools Metrics) and CloudWatch turns them into real metrics, no expensive PutMetricData calls per invocation. This is how you track “checkouts/sec” cheaply at scale.
  • Distributed tracing with X-Ray (or OpenTelemetry/ADOT). When p99 spikes, tracing tells you whether it’s cold start, the DB, or a downstream API, across API Gateway → Lambda → data store. Without it you’re guessing.
  • Dashboard the RED method: Rate (invocations), Errors, Duration. Three graphs answer “is it healthy?” at a glance.

Step 9: What does it actually cost?

Rough numbers for 1M invocations in an hour, each 200 ms at 512 MB (confirm against current pricing):

  • Compute: 1,000,000 × 0.2 s × 0.5 GB = 100,000 GB-seconds → ~$1.67
  • Requests: 1,000,000 × $0.20/1M → $0.20
  • Lambda total ≈ $1.87 for the hour (~$0.0000019/request)
  • CloudWatch Logs: 1 GB ingested → **$0.50** (see why logging discipline matters)

The compute is almost a rounding error; the surrounding services (API Gateway, NAT, logs, the database) dominate the bill. Optimize those, not the Lambda.

The launch checklist

Pulling it together, what “handle a million an hour” actually means in practice:

  1. Capacity math done — peak RPS and concurrency computed via Little’s Law.
  2. Concurrency limit increase requested ahead of time; provisioned concurrency on the hot path.
  3. Datastore sized for concurrency — DynamoDB, or RDS Proxy for relational; caching in front.
  4. Spikes buffered with SQS where responses can be async; idempotent handlers.
  5. Right front door chosen (HTTP API / ALB / Function URL) on cost.
  6. Structured, sampled logging with retention set and long-term shipped to S3.
  7. Metrics + alarms on Throttles, Errors, p99 Duration, ConcurrentExecutions; EMF for business metrics; X-Ray tracing on.
  8. Cost modelled end-to-end, not just Lambda.

Do these and a million users an hour is a quiet afternoon. Skip the database and observability steps and it’s an outage with no dashboards to explain it.

References and Further Reading

Question

For a million-an-hour API, do you reach for Lambda or containers first, and what makes you switch?

Similar Articles

More from cloud

No related topic suggestions found.

Knowledge Quiz

Test your general knowledge with this quick quiz!

A set of multiple-choice questions to test your knowledge.

Take as much time as you need.

Your score will be shown at the end.