Or: How I Built a Serverless REST API That Actually Works (Despite Management’s Best Efforts)


It’s 2 AM. A patient’s heart rate spikes. Our Remote Patient Monitoring system needs to alert the on-call doctor. The Lambda cold starts.

Three seconds later, the alert goes through.

Three seconds doesn’t sound like much until you realize that in those three seconds, a patient could have already collapsed, a doctor could have checked their phone twice and assumed the system was down, and your CTO could have walked by your desk asking “Is the system slow again?”

This is the story of how we built a serverless REST API using AWS Lambda that actually performed well, the architecture decisions that made it work, the management pressure that almost destroyed it, and why FastAPI was the smartest choice we made—even if we didn’t know it at the time.

The Innocent Beginning: Pure Python Lambda

In the beginning, there was a simple Lambda function. Just pure Python. No frameworks, no ORMs, no fancy observability tools. Just a function that received an event, queried a database, and returned some JSON.

It was beautiful. It was simple. It cold-started in about 800ms.

And then, naturally, we had to ruin it.

The FastAPI Seduction

“We need a proper REST framework,” someone said in a meeting. (Spoiler: it was me.)

So we added:

  • FastAPI for the REST framework (because writing raw Lambda handlers is for masochists)
  • SQLModel for the ORM (because writing raw SQL is also for masochists, apparently)
  • Mangum to bridge FastAPI to Lambda (the adapter that makes it all work)

The result? A beautiful, type-safe, auto-documented REST API that cold-started in… 2.1 seconds.

“That’s fine,” we said. “Cold starts are rare, right?”

Guess what: Cold starts were not rare.

The AWS Powertools Phase (Drinking the Kool-Aid)

Then we discovered AWS Lambda Powertools. Structured logging! Distributed tracing! Metrics! Custom routing!

But here’s the thing about Powertools: it has its own routing system (Event Handler) that competes with FastAPI’s routing. So we had a choice to make.

The Benchmark:

  • Powertools Event Handler routing: ~18ms response time
  • FastAPI routing: ~18ms response time

Identical performance. So which one to choose?

The Decision: FastAPI. Because vendor lock-in is for suckers.

Powertools routing locks you into Lambda. FastAPI works anywhere—Lambda, ECS, your laptop, a Raspberry Pi in your closet.

So we used:

  • Powertools Logger (goodbye, print statements)
  • Powertools Tracer (hello, X-Ray costs)
  • Powertools Metrics (hello, CloudWatch costs)
  • FastAPI for all routing (hello, portability)
  • Mangum to bridge FastAPI to Lambda (the adapter that makes it all work)

At this point, we had a Lambda that:

  • Used FastAPI for routing (Powertools benchmarked identically, but FastAPI = portability)
  • Logged everything in structured JSON with Powertools
  • Traced every request through X-Ray
  • Emitted custom metrics
  • Cold-started in 3.2 seconds
  • Used 256MB of RAM (because 128MB caused OOM errors)

The warm response time? A beautiful sub-20ms. Absolutely gorgeous.

The cold start? A soul-crushing 3+ seconds.

The Real Culprit: It Was the Secrets, Stupid

Here’s what nobody tells you about Lambda cold starts: it’s not always the code.

Our 3-second cold start breakdown:

  • Loading Python + packages: ~1.2 seconds
  • Fetching DB credentials from AWS Secrets Manager: ~1.5 seconds
  • Initializing DB connection: ~0.3 seconds

That’s right. Half our cold start time was waiting for AWS to tell us the password to AWS’s own database service.

The irony was not lost on us.

The EventBridge Band-Aid

“Just keep it warm,” the AWS documentation whispered seductively.

So we set up an EventBridge rule to ping our Lambda every 5 minutes. A serverless keep-alive. A cloud-native heartbeat. A expensive cron job.

Results:

  • Cold starts reduced from ~40% of requests to <10%
  • Under heavy load (when AWS spun up multiple instances): ~50% cold starts
  • Our AWS bill: slightly higher
  • Our pride: slightly lower

But it worked. Mostly.

The Architecture That Emerged (Or: How We Over-Engineered Session Auth)

Then the security audit happened.

“You can’t store JWT tokens in localStorage,” they said. “XSS attacks,” they said.

Fair point. So we pivoted to session authentication with HTTP-only cookies.

But here’s the thing: we had built our system as multiple domain-specific Lambda functions. One for patient data, one for device readings, one for alerts, one for user management. You know, “microservices” (I’ll rant about this later).

This meant we needed a Backend-for-Frontend (BFF) layer to:

  1. Handle session authentication
  2. Aggregate data from multiple services
  3. Propagate user claims to internal APIs for RBAC/ABAC
  4. Give management something to put in PowerPoint slides

The Final Architecture:

Browser → Public API Gateway → BFF Lambda (Session Auth)
                                      ↓
                          Private API Gateway
                                      ↓
                          Internal REST Lambdas (RBAC/ABAC)
                                      ↓
                                  RDS Proxy
                                      ↓
                                  PostgreSQL

The Latency Tax:

  • Single Lambda: 20ms response time
  • BFF → Internal Lambda: 40ms response time

We literally doubled our latency for the privilege of:

  • Proper session management
  • Data aggregation
  • Authorization enforcement
  • Architectural purity (lol)

Was it worth it? Actually… yes. The 20ms penalty was negligible for our use case, and it gave us a clean separation between public-facing endpoints and internal services.

But let me be clear: we did NOT do this because it was “best practice.” We did it because it solved specific problems we had.

The Sync Revelation

Here’s something nobody tells you: Lambda functions are single-threaded.

You know what else is single-threaded? Synchronous Python code.

So we made everything sync:

  • Sync database queries (no async/await overhead)
  • Sync FastAPI endpoints (yes, FastAPI works great in sync mode)
  • Sync HTTP calls (requests library, not httpx)

The result? Simpler code, easier debugging, and identical performance.

Because here’s the truth: in a Lambda function handling one request at a time, async gives you exactly zero benefits. It’s just ceremony.

The Database Connection Pool We Didn’t Need

Classic Lambda advice: “Don’t use connection pooling! Lambdas scale horizontally!”

Cool story. Our database was getting hammered with connection requests.

Enter: RDS Proxy

We pointed all our Lambdas at RDS Proxy instead of directly at PostgreSQL. RDS Proxy handles connection pooling, so each Lambda just opens a single connection and RDS Proxy manages the pool.

Cost: ~€50/month
Benefit: Database stopped falling over under load
Engineers’ sleep quality: Improved

No connection pooling in Lambda code. Just a single SQLModel engine per function instance. Let RDS Proxy do its job.

The Management Circus: “We Need Microservices!”

I need to tell you about the meeting.

VP of Engineering: “We need to split the RPM system into microservices.”

Me: “Why?”

VP: “Because that’s the best practice. Netflix does it.”

Me: “We’re not Netflix. We have 200 concurrent users, not 200 million.”

VP: “But what about scalability?”

Me: “We’re on Lambda. It scales automatically.”

VP: “But what about team autonomy?”

Me: “We have four engineers.”

VP: “What if we had separate databases for each service?”

Me: “Then we’d have five services making queries to five different databases to render a single patient dashboard, turning a 20ms response into a 200ms nightmare.”

VP: “Let me send you this blog post about how Uber does it…”

I wrote an entire article about this. The short version: we kept our “microservices” lightweight, shared the same PostgreSQL database, and avoided the distributed systems hellscape that management desperately wanted to inflict on us.

The RPM system has three Lambda functions:

  1. BFF (public-facing, session auth, data aggregation)
  2. Core API (patient data, device readings, core business logic)
  3. Analytics (reporting, metrics, read-only queries)

That’s it. Three “microservices.” Not because Netflix does it. Because it made sense for our bounded contexts.

When Lambda Actually Makes Sense

Let’s be real. After all this, would I use Lambda again?

Absolutely yes, if:

  1. Your traffic is spiky or low-volume
  • We had ~5,000 requests/day during business hours
  • Lambda costs: ~€80/month (including RDS Proxy, API Gateway, etc.)
  • Equivalent ECS Fargate (24/7): ~€150/month minimum
  1. You don’t need sub-second cold starts
  • Our 3-second cold starts affected <10% of requests with warming
  • For an RPM system, that’s acceptable
  • For a real-time trading platform? Run screaming to ECS
  1. You’re not doing websockets or long-running tasks
  • Lambda has a 15-minute timeout
  • API Gateway has a 30-second timeout
  • If your requests are <10 seconds, you’re fine
  1. You value operational simplicity
  • No servers to patch
  • No scaling configuration
  • No container orchestration
  • Just deploy and forget

Run to ECS/Fargate when:

  1. Your traffic is constant (24/7)
  • At ~50,000 requests/day with even distribution, ECS becomes cheaper
  • At 100,000+ requests/day, Lambda pricing becomes insulting
  1. You need predictable latency
  • Cold starts are a statistical inevitability
  • If p99 latency matters, use containers
  1. You need long-running connections
  • WebSockets, streaming responses, file uploads >6MB
  • Lambda is not your friend here
  1. Your team is already comfortable with containers
  • If you’re already running ECS/EKS, adding another service is trivial
  • If you’re Lambda-first, the infrastructure overhead might not be worth it

The FastAPI Masterstroke (By Accident… or Was It?)

Here’s the beautiful part: our entire Lambda codebase is just FastAPI.

Remember when we benchmarked Powertools Event Handler vs FastAPI routing and found identical performance? That wasn’t luck—that was us thinking ahead.

We chose portability over vendor lock-in. And it paid off.

Want to migrate to ECS? Change three lines:

# Lambda version (with Mangum)
from mangum import Mangum
from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "healthy"}

handler = Mangum(app)  # Lambda handler

# ECS version (plain uvicorn)
from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "healthy"}

# Run with: uvicorn main:app --host 0.0.0.0 --port 8000

That’s it. The entire application code is identical.

Our Dockerfile for ECS:

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Same code. Same dependencies. Same business logic.

Lambda or ECS? Just a deployment choice, not an architecture rewrite.

This is why we chose FastAPI over Powertools routing. Portability. We weren’t locked into Lambda’s programming model.

The Go/Rust Temptation

“What if we rewrote it in Go?” someone asked.

“Or Rust,” another engineer chimed in. “I hear cold starts are under 100ms.”

So we’re building a POC to benchmark:

  • Python + FastAPI (our current stack)
  • Go + Gin/Fiber
  • Rust + Axum

Preliminary research suggests:

  • Go cold starts: ~400ms (vs our 3s Python)
  • Rust cold starts: ~200ms (vs our 3s Python)
  • Go/Rust warm latency: ~5ms (vs our 18ms Python)

But here’s the question: Is a 2.6-second cold start improvement worth:

  • Rewriting the entire application
  • Losing Python’s ecosystem (SQLModel, Pydantic, etc.)
  • Training the team on a new language
  • Maintaining two codebases during migration

For a system where <10% of requests hit cold starts? Probably not.

For a new project starting from scratch with hard latency requirements? Maybe.

I’ll update this article when we have real benchmark numbers. But I suspect the answer will be: “It’s faster, but not worth the migration cost.”

The Real Lessons

After building a production serverless REST API with AWS Lambda, here’s what I learned:

1. Cold Starts Are Mostly Solvable

  • EventBridge warming handles 90% of cases
  • If that’s not enough, you probably shouldn’t be using Lambda

2. The Secrets Are the Slowest Part

  • Caching DB credentials cuts cold starts in half
  • AWS Secrets Manager is slow; consider alternatives for hot paths

3. RDS Proxy Is Worth It

  • Let it handle connection pooling
  • Your Lambda code stays simple
  • Your database stays healthy

4. Sync > Async in Lambda

  • Single-threaded execution makes async pointless
  • Simpler code, easier debugging, same performance

5. FastAPI = Portability

  • Benchmarked against Powertools routing: identical performance
  • Chose FastAPI anyway for vendor independence
  • Write once, deploy to Lambda or ECS
  • Mangum adapter is excellent
  • No vendor lock-in

6. Microservices Are Not a Best Practice

  • They’re a tool for specific problems
  • “Netflix does it” is not a valid architecture decision
  • Shared databases are fine; distributed systems are hard

7. Management Will Pressure You to Over-Engineer

  • Resist cargo cult architecture
  • Demand specific problems that need solving
  • “Best practice” without context is just theater

8. Know When to Leave Lambda

  • Traffic patterns dictate economics
  • Latency requirements dictate feasibility
  • Team expertise dictates maintenance cost

Conclusion: The Cloud Tax Is Optional

Lambda cold starts are real. They’re annoying. They add latency.

But they’re not a fundamental limitation—they’re a cost-benefit tradeoff.

For our RPM system:

  • 3-second cold starts on <10% of requests? Acceptable.
  • €80/month for a system handling 5,000 requests/day? Excellent value.
  • Zero operational overhead? Priceless.

If your traffic is spiky, your latency requirements are reasonable, and you value simplicity over absolute performance, Lambda is still a great choice.

But if you’re running 24/7 at scale, paying for cold starts you’ll never eliminate, and your engineering manager keeps sending you blog posts about how Uber does microservices…

Maybe it’s time to move to ECS.

Or, you know, Hetzner. But that’s another article. 😉


P.S. My 4-year-old poodle thinks serverless is a terrible name. “There are servers,” he says. “They’re just someone else’s servers.” He’s not wrong.


Want to argue about Lambda vs ECS? Find me on LinkedIn or check out policy.hermesc.gr where I built an authorization platform that runs on… well, you’ll have to read the next article.

Still using AWS and watching your bill grow? Read about how I escaped the AWS cost trap.