Back to Insights
AI & Systems7 min read

Why I Ditched Flask and Never Looked Back

We moved from a Flask monolith to FastAPI microservices and cut response time by 40%. Here's exactly what changed.

AP

Anshuman Parmar

August 2025

Why I Ditched Flask and Never Looked Back

FastAPI is my go-to for Python backends now. But it wasn't always. We started on Flask, and moved for real reasons, not just because FastAPI was newer.

Here's what actually changed, and the numbers that came with it.

Why we left Flask

Flask's async support always felt bolted on afterward. FastAPI has it built in from the start, plus automatic API docs and real type checking through Pydantic.

In our load tests, FastAPI handled roughly 3000 requests per second where Flask managed about 1000 on the same hardware. That gap alone justified the migration.

From one big app to several small ones

We had a single Flask app doing auth, users, tasks, and data, all tangled together. One bug in any part could take down everything, and we couldn't scale just the busy part.

text
1┌────────────────────────────────────────┐
2│ Flask Monolith │
3│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
4│ │ Auth │ │ User │ │ Task │ │ Data │ │
5│ └──────┘ └──────┘ └──────┘ └──────┘ │
6└────────────────────────────────────────┘

We split it into separate services behind an API gateway, each with its own database, each deployable on its own.

text
1┌─────────────┐
2│ API Gateway │
3└──────┬──────┘
4
5┌──────┴──────┬──────────────┬──────────────┐
6▼ ▼ ▼ ▼
7┌─────┐ ┌──────┐ ┌──────┐ ┌──────┐
8│Auth │ │ User │ │ Task │ │ Data │
9└─────┘ └──────┘ └──────┘ └──────┘

Going async, properly

The real win in FastAPI only shows up if you actually go async everywhere, not just in the framework.

python
1# Blocking, holds up the whole event loop
2@app.get("/users/{user_id}")
3def get_user(user_id: int, db: Session = Depends(get_db)):
4 return db.query(User).filter(User.id == user_id).first()
5
6# Async, doesn't block anything else
7@app.get("/users/{user_id}")
8async def get_user(user_id: int, db: AsyncSession = Depends(get_async_db)):
9 result = await db.execute(select(User).where(User.id == user_id))
10 return result.scalar_one_or_none()

Database connections are expensive to open, so we pool them instead of creating new ones per request.

python
1engine = create_async_engine(
2 DATABASE_URL,
3 pool_size=20,
4 max_overflow=30,
5 pool_timeout=30,
6 pool_recycle=1800,
7)

And a lot of reads don't need to hit the database every single time.

python
1@app.get("/products/{product_id}")
2@cache(expire=300)
3async def get_product(product_id: int):
4 return await fetch_product(product_id)

What happened at 10K concurrent users

We load tested with Locust before and after these changes.

MetricBeforeAfter
RPS2,5004,200
P50 Latency180ms95ms
P95 Latency850ms280ms
Error Rate2.3%0.1%
-----------------------
P50 Latency180ms95ms
P95 Latency850ms280ms
Error Rate2.3%0.1%
RPS2,5004,200
P95 Latency850ms280ms
Error Rate2.3%0.1%
P50 Latency180ms95ms
Error Rate2.3%0.1%
P95 Latency850ms280ms

The biggest wins came from switching to an async database driver, connection pooling, Redis caching, and just adding pagination to list endpoints instead of returning everything at once.

Designing for things to fail well

Systems fail. The goal is making sure they fail in a way you can predict and recover from.

We return structured errors instead of raw stack traces, wrap external calls in circuit breakers, expose a real health check endpoint, and fall back gracefully instead of crashing.

python
1@app.get("/recommendations/{user_id}")
2async def get_recommendations(user_id: int):
3 try:
4 return await ml_service.get_personalized(user_id)
5 except ServiceUnavailable:
6 return await get_popular_items()
7 except Exception:
8 return {"recommendations": [], "fallback": True}

We also added structured logging with a request ID on every log line, so tracing one request across the system actually works, plus Prometheus metrics for request counts, latency, and status codes.

Where we ended up

40% faster average response time, reliably handling 10K+ concurrent requests, 99.5% deployment success with CI/CD, and zero-downtime rolling deployments.

None of it was exotic. Go async everywhere, pool your connections, cache what you can, and plan for failure instead of hoping it won't happen.

FAQ

Is FastAPI actually faster than Flask?

In our tests, yes, roughly 3000 RPS versus 1000 RPS, mostly because async is native instead of added on top.

When should I split a monolith into microservices?

When a specific part needs to scale, fail, or deploy independently. Splitting earlier than that just adds overhead.

What actually helped us handle 10K concurrent requests?

Going async everywhere, proper connection pooling, and Redis caching on read-heavy endpoints.


Questions about FastAPI or microservices? Connect with me on LinkedIn or GitHub.

AP

WRITTEN BY

Anshuman Parmar

Senior Full Stack Developer specializing in AI systems, browser automation, and scalable web applications. Building production-grade solutions that deliver measurable business impact.

Enjoyed this article?

Explore more insights on AI, automation, and system design.

View All Insights