Back to blog
Backend
October 15, 2025
7 min read

Building Scalable Backends with FastAPI

Building Scalable Backends with FastAPI

When I first discovered FastAPI about two years ago, I was skeptical. Another Python web framework? Really? But after building several production systems with it, I can confidently say it's transformed how I approach backend development. Let me share what I've learned along the way.

The FastAPI Journey: Why I Made the Switch

Coming from a Flask background, I was comfortable with my workflow. But as our user base grew from hundreds to thousands to tens of thousands, the cracks started showing. Response times were creeping up, and our servers were struggling during peak hours. That's when I decided to give FastAPI a serious look.

The first thing that caught my attention was the automatic API documentation. Swagger UI out of the box? OpenAPI schema generation without writing a single line of extra code? I was intrigued. But what really sold me was the performance benchmarks—FastAPI is genuinely fast, rivaling Node.js and Go in many scenarios.

The Power of Async/Await: A Real-World Example

Here's something I learned the hard way: async/await isn't just about making your code look modern—it fundamentally changes how your application handles concurrent requests.

In one of my projects, we had an endpoint that needed to fetch user data, check their permissions, and retrieve their recent activity. Initially, I wrote it synchronously, and under load, our API would grind to a halt. After refactoring to use async/await properly, we saw a 60% improvement in throughput.

python
# Before: Synchronous approach
@app.get("/user/{user_id}/dashboard")
def get_dashboard(user_id: int):
    user = db.query(User).filter(User.id == user_id).first()
    permissions = db.query(Permission).filter(Permission.user_id == user_id).all()
    activity = db.query(Activity).filter(Activity.user_id == user_id).limit(10).all()
    return {"user": user, "permissions": permissions, "activity": activity}

# After: Async approach
@app.get("/user/{user_id}/dashboard")
async def get_dashboard(user_id: int):
    async with get_db_session() as db:
        user_task = db.get(User, user_id)
        permissions_task = db.execute(select(Permission).where(Permission.user_id == user_id))
        activity_task = db.execute(select(Activity).where(Activity.user_id == user_id).limit(10))

        user, permissions_result, activity_result = await asyncio.gather(
            user_task, permissions_task, activity_task
        )

        return {
            "user": user,
            "permissions": permissions_result.scalars().all(),
            "activity": activity_result.scalars().all()
        }

The key insight? With async, while we're waiting for one database query, we can start the others. It's like cooking a meal—you don't wait for the water to boil before chopping vegetables.

Database Connection Pooling: The Silent Performance Killer

One mistake I see often (and made myself early on) is not properly configuring database connection pools. In production, creating a new database connection for every request is expensive—really expensive.

With FastAPI and SQLAlchemy, proper connection pooling is crucial. Here's a configuration that's worked well for me across multiple high-traffic applications:

python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

engine = create_async_engine(
    DATABASE_URL,
    pool_size=20,
    max_overflow=10,
    pool_pre_ping=True,
    pool_recycle=3600,
    echo=False
)

AsyncSessionLocal = sessionmaker(
    engine, class_=AsyncSession, expire_on_commit=False
)

That

code
pool_pre_ping
parameter? It saved us from mysterious "connection closed" errors during deployments. The
code
pool_recycle
ensures connections don't go stale.

Dependency Injection: More Than Just a Fancy Pattern

FastAPI's dependency injection system is elegant. At first, I thought it was just syntactic sugar, but it's incredibly powerful for testing and code organization.

python
async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
) -> User:
    credentials_exception = HTTPException(
        status_code=401,
        detail="Could not validate credentials"
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    user = await db.execute(
        select(User).where(User.username == username)
    )
    user = user.scalar_one_or_none()
    if user is None:
        raise credentials_exception
    return user

@app.get("/protected-route")
async def protected_route(current_user: User = Depends(get_current_user)):
    return {"message": f"Hello {current_user.username}"}

The beauty? Testing becomes trivial—just override the dependency with a mock.

Background Tasks: Don't Block Your Users

Nothing frustrates users more than waiting for a slow API response when the actual work could happen later. FastAPI's background tasks are perfect for this.

I once built an analytics platform where we needed to log every user action. Initially, we were writing to the database synchronously, adding 50-100ms to every request. By moving it to a background task, responses became instant.

python
from fastapi import BackgroundTasks

async def log_analytics(user_id: int, action: str, metadata: dict):
    async with get_db_session() as db:
        log_entry = AnalyticsLog(
            user_id=user_id,
            action=action,
            metadata=metadata,
            timestamp=datetime.utcnow()
        )
        db.add(log_entry)
        await db.commit()

@app.post("/items/{item_id}/like")
async def like_item(
    item_id: int,
    background_tasks: BackgroundTasks,
    current_user: User = Depends(get_current_user)
):
    # Critical path: update the like count
    await update_like_count(item_id)

    # Non-critical: log the action in the background
    background_tasks.add_task(
        log_analytics,
        current_user.id,
        "like",
        {"item_id": item_id}
    )

    return {"status": "liked", "item_id": item_id}

Validation: Let Pydantic Do the Heavy Lifting

One of FastAPI's superpowers is Pydantic integration. Input validation that would take dozens of lines in Flask is automatic and type-safe.

python
from pydantic import BaseModel, EmailStr, validator
from datetime import datetime

class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str
    age: int

    @validator('username')
    def username_alphanumeric(cls, v):
        if not v.isalnum():
            raise ValueError('Username must be alphanumeric')
        return v

    @validator('age')
    def age_reasonable(cls, v):
        if v < 13 or v > 120:
            raise ValueError('Age must be between 13 and 120')
        return v

@app.post("/users")
async def create_user(user: UserCreate):
    # user is already validated, guaranteed to be correct
    hashed_password = hash_password(user.password)
    # ... rest of the logic

The request is validated before it even reaches your handler. Invalid data? Automatic 422 response with detailed error messages.

Lessons from Production

After running FastAPI in production for over a year now, here are some hard-earned lessons:

  1. Monitor your async tasks: Unhandled exceptions in background tasks can silently fail. Always add proper error handling and logging.

  2. Set timeouts: Even with async code, external API calls can hang. Use

    code
    httpx.AsyncClient
    with timeouts.

  3. Profile your code: FastAPI is fast, but your code might not be. Use tools like

    code
    py-spy
    to find bottlenecks.

  4. Use middleware wisely: Middleware runs on every request. Keep it light or your performance gains disappear.

Final Thoughts

FastAPI hasn't just made my code faster—it's made me a better engineer. The framework's design encourages good practices: type hints, async programming, proper error handling. Yes, there's a learning curve if you're new to async Python, but it's absolutely worth it.

If you're building a new API today, I'd reach for FastAPI without hesitation. And if you have an existing Flask or Django app struggling with performance, consider FastAPI for your new microservices. You might be surprised at how much simpler it makes things.