DEV Community

Akarshan Gandotra
Akarshan Gandotra

Posted on

FastAPI: request.state vs Context Variables - When to Use What? 🚀

When building FastAPI applications, you'll often need to share data across different parts of your request lifecycle i.e. middleware, dependencies, and route handlers. FastAPI provides two primary mechanisms for this: request.state and context variables (contextvars). But when should you use each one?

Understanding request.state 📦

request.state is a simple namespace object attached to each FastAPI request. It's designed to store arbitrary data that needs to be shared during the processing of a single request.

Basic Usage 🔧

from fastapi import FastAPI, Request, Depends

app = FastAPI()

@app.middleware("http")
async def add_user_info(request: Request, call_next):
    # Store data in request.state
    request.state.user_id = "user_123"
    request.state.request_time = time.time()

    response = await call_next(request)
    return response

@app.get("/profile")
async def get_profile(request: Request):
    # Access data from request.state
    user_id = request.state.user_id
    return {"user_id": user_id, "message": "User profile"}
Enter fullscreen mode Exit fullscreen mode

Pros of request.state ✅

  • ✅ Simple and straightforward
  • ✅ Explicitly tied to the request object
  • ✅ Easy to understand and debug
  • ✅ No additional imports needed
  • ✅ Works well with dependency injection

Cons of request.state ❌

  • ❌ Requires passing the Request object around
  • ❌ Not accessible in deeply nested functions without explicit passing
  • ❌ Can become verbose in complex applications

Understanding Context Variables 🌍

Context variables (contextvars) provide a way to store data that's automatically available throughout the execution context of a request, without explicitly passing it around.

Basic Usage 🚀

from contextvars import ContextVar
from fastapi import FastAPI, Request
import time

# Define context variables
user_id_var: ContextVar[str] = ContextVar('user_id')
request_time_var: ContextVar[float] = ContextVar('request_time')

app = FastAPI()

@app.middleware("http")
async def add_user_info(request: Request, call_next):
    # Set context variables
    user_id_var.set("user_123")
    request_time_var.set(time.time())

    response = await call_next(request)
    return response

@app.get("/profile")
async def get_profile():
    # Access context variables from anywhere
    user_id = user_id_var.get()
    request_time = request_time_var.get()
    return {
        "user_id": user_id, 
        "request_time": request_time,
        "message": "User profile"
    }
Enter fullscreen mode Exit fullscreen mode

Pros of Context Variables ✅

  • ✅ Available anywhere in the execution context
  • ✅ No need to pass request object around
  • ✅ Cleaner function signatures
  • ✅ Automatic cleanup after request completion
  • ✅ Thread-safe and async-safe

Cons of Context Variables ❌

  • ❌ Less explicit than request.state
  • ❌ Harder to debug and trace
  • ❌ Requires additional setup and imports
  • ❌ Can make testing more complex

Advanced Patterns 🎨

Using Context Variables with Dependencies 🔗

from contextvars import ContextVar
from fastapi import FastAPI, Depends, HTTPException
from typing import Optional

current_user_var: ContextVar[Optional[dict]] = ContextVar('current_user', default=None)

app = FastAPI()

async def get_current_user() -> dict:
    user = current_user_var.get()
    if not user:
        raise HTTPException(status_code=401, detail="User not authenticated")
    return user

@app.middleware("http")
async def auth_middleware(request: Request, call_next):
    # Simulate user authentication
    token = request.headers.get("authorization")
    if token:
        user = {"id": "123", "name": "John Doe"}  # Simulate user lookup
        current_user_var.set(user)

    response = await call_next(request)
    return response

@app.get("/protected")
async def protected_route(user: dict = Depends(get_current_user)):
    return {"message": f"Hello {user['name']}!"}
Enter fullscreen mode Exit fullscreen mode

Combining Both Approaches 🤝

from contextvars import ContextVar
from fastapi import FastAPI, Request
import uuid

# Context var for request ID (global access)
request_id_var: ContextVar[str] = ContextVar('request_id')

app = FastAPI()

@app.middleware("http")
async def request_middleware(request: Request, call_next):
    # Generate request ID and store in both places
    request_id = str(uuid.uuid4())

    # Store in context var for global access
    request_id_var.set(request_id)

    # Store in request.state for explicit access
    request.state.request_id = request_id
    request.state.start_time = time.time()

    response = await call_next(request)
    return response
Enter fullscreen mode Exit fullscreen mode

Performance Considerations ⚡

Memory Usage

  • request.state: Minimal overhead, data is garbage collected with the request 🗑️
  • contextvars: Slightly higher overhead due to context management, but still very efficient 💪

When to Use What? 🤔

Use request.state when:

  • 🎯 You want explicit, traceable data flow
  • 🎯 Building simple applications
  • 🎯 You're already passing Request objects around
  • 🎯 You need to store request-specific metadata
  • 🎯 Debugging and testing simplicity is important

Use context variables when:

  • 🎯 You have deeply nested function calls
  • 🎯 You want cleaner function signatures
  • 🎯 Building complex applications with many layers
  • 🎯 You need global access to request-scoped data
  • 🎯 Working with third-party libraries that need access to request data

Real-World Example: Logging System 📝

Here's how you might implement a request logging system with both approaches:

With Context Variables 🌐

from contextvars import ContextVar
import logging
import uuid

request_id_var: ContextVar[str] = ContextVar('request_id')
logger = logging.getLogger(__name__)

class RequestLogger:
    @staticmethod
    def info(message: str):
        request_id = request_id_var.get("unknown")
        logger.info(f"[{request_id}] {message}")

@app.middleware("http")
async def logging_middleware(request: Request, call_next):
    request_id_var.set(str(uuid.uuid4()))
    RequestLogger.info(f"Request started: {request.method} {request.url}")

    response = await call_next(request)

    RequestLogger.info(f"Request completed: {response.status_code}")
    return response

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    RequestLogger.info(f"Fetching user {user_id}")
    # Your business logic here
    return {"user_id": user_id}
Enter fullscreen mode Exit fullscreen mode

With request.state 📋

import logging
import uuid

logger = logging.getLogger(__name__)

def log_with_request_id(request: Request, message: str):
    request_id = getattr(request.state, 'request_id', 'unknown')
    logger.info(f"[{request_id}] {message}")

@app.get("/users/{user_id}")
async def get_user(user_id: int, request: Request):
    log_with_request_id(request, f"Fetching user {user_id}")
    # Your business logic here
    return {"user_id": user_id}
Enter fullscreen mode Exit fullscreen mode

Have you used both approaches in your FastAPI projects? Share your experiences in the comments below! 👇

Top comments (0)