Why I Stopped Using Lambda as an API
Serverless looks like less infrastructure. For HTTP APIs under real load, I found the opposite — and went back to containers.
What you will read
This is a field note from running HTTP APIs two ways: one process in a container (ECS / EKS / plain Docker), and a surface of Lambdas behind API Gateway.
You will see the friction that showed up once traffic and shared code were real — cost under load, cold starts, limits, bundle size, deployment and rollback pain, awkward local feedback, and the RDS connection tax — why a single encapsulated service usually wins for request/response work, and when I still reach for Lambda without apology.
The through-line is simple: put the API in one place; use Lambda for work that is already asynchronous.
Two shapes of “the API”
When people say “we run on Lambda,” they often mean two different products:
- HTTP API — a client waits for a response. Latency, auth, validation, and DB access sit on the hot path.
- Async worker — something already happened (event, queue message, schedule). The caller does not wait on your process.
I have used both. This note is mostly about shape (1). Shape (2) is where Lambda still earns its keep for me.
A containerized API is boring in a good way: one (or a few) long-lived processes, a normal framework, a reverse proxy or load balancer in front, and infrastructure that does not rewrite itself every time you rename a handler.
A Lambda-as-API stack is a constellation. At minimum you provision API Gateway + Lambda. In practice you also grow shared layers, IAM policies, stage variables, alarms, and often more AWS products than the feature needed. The marketing story is “no servers.” The day-to-day story is still infrastructure — just a different kind.
What went wrong for me with Lambda as the API
Nothing is “broken” about Lambda. Plenty of teams ship on it. The problem is the mismatch: treating a short-lived function runtime as if it were a full application server for synchronous HTTP.
Here is what kept showing up.
Cost climbs when the app is actually busy
Idle traffic is cheap. Sustained request volume is not. Once the API is under real load, you pay for concurrent executions, duration, and often the gateway in front. A small fleet of containers with predictable capacity became easier to forecast — and usually cheaper for the workloads I cared about.
You spend more time on the platform than on the product
Provisioning “just” an API is not one resource. Gateway routes, authorizers, memory settings, timeouts, concurrency caps, VPC attachments, and permissions become the weekly work. I found myself optimizing AWS shapes instead of domain behavior.
Service limits show up earlier than you expect
Concurrency, payload size, timeout ceilings, connection behavior inside a VPC — these are not edge cases forever. They become product constraints. With a container you still have limits, but they feel like your ops knobs, not a ceiling you discover mid-incident.
Cold start is a product problem unless you buy it away
Warm invocations are fine. Cold ones are slow enough that users notice. Provisioned concurrency and bigger memory help — that is money and more config. I stopped pretending cold start was a theoretical footnote.
There is no real application framework on the hot path
You can glue libraries together. You do not get the boring, complete stack you would take for granted in Nest, FastAPI, Rails, or Spring: middleware, DI, request lifecycle, migrations, a local server that behaves like prod. For an HTTP API, that absence is expensive.
Bundle size becomes a feature tax
Every shared utility, logger, and SDK shows up in deploy artifacts and cold-start time. Teams start arguing about tree-shaking and layer layout while the backlog waits. I would rather ship the feature than shave another megabyte off a zip.
End-to-end testing is harder than the demos suggest
Local stubs, SAM, containers-that-pretend-to-be-Lambda, and “just deploy to a sandbox” all work — until you need confidence across auth, gateway, and downstream deps. A container API with docker compose and a real HTTP client was simply faster to trust.
Zero trust everywhere is powerful — and often more than you need
Fine-grained IAM per function is a strength of the model. For a single product API owned by one team, I often wanted a simpler trust boundary: one service identity, clear network rules, less policy surface to review on every change.
Shared code turns into deployment hell
Change a shared package and you redeploy every Lambda that imports it. That is fine at three functions. At thirty, deploy time and blast radius dominate the week. A monolith or modular service updates in one ship.
Rollbacks with IaC are not “git checkout” friendly
CDK, CloudFormation, Terraform — great for intent, awkward when you want “give me the stack from ten commits ago” under pressure. Blue/green on a container service was a mental model I could operate without ceremony.
Local feedback loops stay thin
You can invoke a handler in isolation. Feeling the full request path — gateway quirks, headers, auth, timeouts — still leans on cloud or heavy emulation. Containers gave me a loop I could live in all day.
RDS needs special care on every cold path
Each concurrent execution can want a connection. Without RDS Proxy or Data API (and the design that goes with them), you will exhaust the database. That is solvable — and it is another permanent tax on “simple serverless.”
None of these alone made me leave. Together, for synchronous APIs, they meant I was fighting the runtime instead of using it.
Why a containerized API usually wins for me
| Pressure | What I want | What the container shape gives |
|---|---|---|
| Cost under load | Predictable spend | Capacity you can size and forecast |
| Latency | Fast enough without buying warm pools | Process already running |
| Complexity | Room for awkward domain flows | Full frameworks and long-lived state patterns |
| Shape of the app | One encapsulated service when that fits | Monolith or modular service without splitting every verb into a deployable |
| Scale | Grow when demand is real | Scale the service you already have |
| Stack choice | Use the framework the team knows | Anything that speaks HTTP |
| Feedback | Local end-to-end | Compose + real ports + real clients |
| Ownership | Infra next to, not inside, every feature | Codebase and platform concerns stay separable |
The important win is not “Docker good.” It is encapsulation: one place to read the request, apply the domain, talk to the database, and return a response. Infra stays around that unit instead of becoming a graph of tiny deployables for every route.
When I still use Lambda
I still reach for Lambda when the work is already off the request path:
- Event-driven processors — something landed on a bus or stream; handle it asynchronously.
- Background and long-running jobs — exports, fan-out, cleanup, scheduled maintenance (within timeout reality, or stepped via queues).
- Spiky, rare work — a weekly report or a webhook that fires ten times a day, where an always-on box would sit idle.
- Glue at the edge of the platform — thin adapters: S3 event → notify, queue → enrich, cron → kick a workflow.
In those cases the “no servers” story is closer to true. Cold start hurts less when nobody is staring at a spinner. Deploying many small workers can even be a feature if each owns a clear event type.
What I avoid: modeling every GET /orders/:id as its own function estate unless there is a strong isolation or scale reason I can name out loud.
The pattern I prefer
I call it:
One encapsulated HTTP service; Lambda only for async edges.
In practice:
- Put the request/response API in a containerized service (ECS, EKS, or equivalent) with a normal framework.
- Keep domain logic, shared libraries, and DB access inside that service boundary.
- Expose async work as explicit jobs or events — then use Lambda (or workers) for those processors.
- Optimize for local end-to-end: one command that boots API + deps and lets you hit real HTTP.
- Treat infrastructure as the shell around the service, not as a new AWS product per handler.
- Measure cost and latency on your traffic shape before declaring serverless cheaper or faster.
What this is not: a ban on Lambda, a claim that containers have no ops cost, or nostalgia for pets over cattle. It is a default for HTTP APIs after living both sides.
Conclusion
Serverless did not remove infrastructure from my life. For APIs, it moved the work into gateways, limits, bundles, deploys-per-function, and connection strategies — while the product still needed a coherent application.
I prefer a tightly encapsulated service: one place that owns the HTTP surface, not a cloud of Lambdas plus SQS, SNS, and a table per concern that only makes sense after you already chose the runtime. Containers gave me predictable cost under load, speed without paying for warmth, frameworks I already trusted, and a local loop that felt like production.
Use Lambda where the work is already asynchronous. Keep the API as a service. That split has been the version of “cloud native” I can operate without apology.
Resources
Official references for the systems and trade-offs in this note:
Lambda and the HTTP edge
- AWS Lambda Developer Guide — product overview and how functions run
- Using AWS Lambda with Amazon API Gateway — the usual Lambda-as-API pairing
- API Gateway Developer Guide — routes, stages, and the gateway in front of the function
- Lambda quotas — timeouts, payload size, concurrency ceilings, and other hard limits
Cold starts and concurrency
- Lambda execution environment lifecycle — cold starts, init, and warm reuse
- Lambda function concurrency — reserved vs on-demand concurrency
- Configuring provisioned concurrency — paying to keep environments warm
- Lambda SnapStart — another path to faster starts on supported runtimes
Databases from Lambda
- Amazon RDS Proxy — connection pooling between many short-lived clients and RDS
- Using the Amazon RDS Data API — HTTP SQL without managing a connection pool in the function
- Using AWS Lambda with Amazon RDS — Lambda + relational DB patterns (direct connect and Proxy)
Containers
- Amazon ECS Developer Guide — running containerized services on AWS
- Amazon EKS User Guide — managed Kubernetes when you want that control plane
- Service load balancing on Amazon ECS — putting a long-lived service behind a load balancer
- AWS decision guide: Fargate or Lambda — AWS’s own framing of containers vs functions
Pricing changes; when you compare cost shapes, start from the current Lambda, API Gateway, ECS, and RDS Proxy pricing pages rather than anything quoted in a blog post — including this one.