sequenceDiagram
participant C1 as Table 1 (Slow Request)
participant C2 as Table 2 (Quick Request)
participant S as Sync Server (One Waiter)
C1->>S: POST /slow-query
Note over S: Waiting on external I/O (5s)
C2->>S: GET /health
Note over C2: Waiting in line...
S->>C1: Response
S->>C2: Response (after C1)
3 How FastAPI Handles Many Requests at Once
3.1 Why One Slow Call Delays Everyone
Imagine a restaurant with one waiter handling every table. One table asks for a custom order that takes five minutes to confirm with the kitchen. If the waiter stands there waiting, no other table gets service. That is a blocking server (one server worker/thread handling requests sequentially).
In FastAPI terms, this happens during I/O-bound work: waiting on a database, file system, or external API. While that wait is happening, the server thread can’t move on, so other requests queue up.
With async, the waiter places Table 1’s request, then serves other tables while waiting. Same waiter, better flow.
In everyday language, “synchronous” describes events that are “in sync”, i.e. they take the same steps together. It DOES NOT tell us if events happen sequentially or simultaneously, although it is often interpreted as “events happening simultaneously”.
And this is where the confusion arises. In programming, “synchronous” refers to “an event and the code that handles it happen simultaneously”. In other words, the code is “in sync” with the event and cannot handle other events until it finishes. This means that the code can only handle events sequentially, one at a time. So “synchronous” in programming actually means “sequential”, which is the opposite of what the term suggests in everyday language.
3.2 async and await: Pause One Request, Serve Another
Python uses two keywords to juggle multiple requests without blocking: async and await. - When you declare a function with async def, you are telling Python: “This function might pause itself to let others work.” - When you write await in front of a slow operation (like an API call), you are saying: “I am stepping aside; let the next customer in line while I wait for this to finish.”
Asynchronous operations are handled by an event loop (managed by Uvicorn), which keeps track of all the paused functions and wakes them up when their awaited operations complete.
Here is a simple async endpoint. We will use asyncio.sleep to simulate a slow operation:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/slow")
async def slow_endpoint():
# This simulates a 5-second database query or LLM call
await asyncio.sleep(5)
return {"message": "Sorry for the wait, here is your data"}If you hit this endpoint in a browser, it takes five seconds. If your friend hits the /health endpoint on the same server during those five seconds, they get an instant response. The server was not blocked.
Just remember the rule: if a function might take time, make it async def and await the slow parts.
3.4 Keeping API Keys Safe
When you create a client object for an external service, you often need to provide an API key for authentication in addition to the service URL. This is a secret credential that should never be hardcoded in your source code. Instead, we store it in a .env file as an environment variable and use Pydantic to validate them.
Your .env file (never commit this to Git!) looks like:
OPENAI_API_KEY=... # If you are using OpenAI, for example
MODEL_NAME=...
Install the helper:
pip install pydantic-settingsCreate a settings object that reads from your .env file automatically:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
openai_api_key: str
model_name: str = "gpt-4" # Default value if not in env
class Config:
env_file = ".env"
settings = Settings()
# Now use it in your dependency
def get_settings():
return settingsYou can now access settings via dependency injection: settings: Settings = Depends(get_settings).
3.5 Hands-On: Upgrade the Quote API to Async + Depends
Let us bring it all together by upgrading your Quote of the Day API from Part 2. We will convert it to async, add a fake “slow” quote lookup to prove non-blocking behavior, and inject a shared “database connection” (simulated with a simple class).
First, the dependency. We will create a fake database handler that takes time to “connect”:
import asyncio
from fastapi import FastAPI, Depends, HTTPException
app = FastAPI()
# Simulating an expensive-to-create resource
class QuoteDatabase:
def __init__(self):
# Imagine this opens a real Postgres connection pool
self.quotes = {
1: {"text": "Patience is a virtue", "author": "Unknown"},
2: {"text": "FastAPI is fun", "author": "You"}
}
async def get_by_id(self, quote_id: int):
# Simulate network latency to a real database
await asyncio.sleep(2)
if quote_id not in self.quotes:
raise HTTPException(status_code=404, detail="Quote not found")
return self.quotes[quote_id]
# The dependency provider
async def get_db():
db = QuoteDatabase() # In production, you'd cache this properly
try:
yield db
finally:
# Cleanup would happen here (close connections)
passNow, two endpoints to test concurrency. One is slow (uses the database), one is instant:
@app.get("/quotes/{quote_id}")
async def get_quote(quote_id: int, db: QuoteDatabase = Depends(get_db)):
# This takes 2 seconds due to the simulated latency inside db.get_by_id
quote = await db.get_by_id(quote_id)
return quote
@app.get("/health")
async def health():
# This responds instantly, even if /quotes/1 is grinding away
return {"status": "ok", "server": "async and ready"}Testing workflow: 1. Start the server: uvicorn main:app --reload 2. In one terminal (or browser tab), request the slow quote: curl http://localhost:8000/quotes/1 (This will hang for 2 seconds) 3. Immediately, in a second terminal, hit the health check: curl http://localhost:8000/health
You will see the health check return instantly, while the quote request takes its full two seconds. If you had written this synchronously (using time.sleep instead of await asyncio.sleep), the health check would have waited in line behind the slow quote, taking over two seconds itself.
3.6 Recap: Terms You Learned
| Web Term | What it actually means in Python |
|---|---|
Async (async def) |
A marker telling Python this function can pause itself to let other work happen |
Await (await) |
The keyword that triggers the pause, specifically for I/O operations like network calls |
| Blocking I/O | Waiting for external resources (network, disk) in a way that freezes the entire thread |
| Event Loop | The invisible manager (run by Uvicorn) that juggles all your paused async functions |
| Dependency Injection | The pattern of providing pre-built tools (like database clients) to your functions via Depends() |