Mohammad Hamrah

Notes

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:

  1. HTTP API — a client waits for a response. Latency, auth, validation, and DB access sit on the hot path.
  2. 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:

  1. Put the request/response API in a containerized service (ECS, EKS, or equivalent) with a normal framework.
  2. Keep domain logic, shared libraries, and DB access inside that service boundary.
  3. Expose async work as explicit jobs or events — then use Lambda (or workers) for those processors.
  4. Optimize for local end-to-end: one command that boots API + deps and lets you hit real HTTP.
  5. Treat infrastructure as the shell around the service, not as a new AWS product per handler.
  6. 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

Cold starts and concurrency

Databases from Lambda

Containers

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.