Skip to content

Conversation

@dolfinus
Copy link
Contributor

@dolfinus dolfinus commented Nov 4, 2025

#14262 implemented caching of inspect module calls within Dependant class, but it left one clause outside, and it is called on every request.

Reusing inspect.iscoroutinefunction result reduces time spent in get_route_handler from 2.23 µs to 1.33 µs:

perf.py
import asyncio
import time
from fastapi import FastAPI, Depends, Header, Query
from fastapi.routing import APIRoute
from typing import Optional


# -----------------------
# Dependencies
# -----------------------

# Sync dependency
def get_token_header(x_token: str = Header(...)) -> str:
    if x_token != "secret-token":
        raise ValueError("Invalid token")
    return x_token


# Async dependency
async def get_query_param(q: Optional[str] = Query(None)) -> str:
    await asyncio.sleep(0.001)  # simulate small async delay
    return q or "default"


# Nested dependency (sync)
def get_user(token: str = Depends(get_token_header)) -> dict:
    return {"username": "test_user", "token": token}


# Nested dependency (async)
async def get_full_context(
    user: dict = Depends(get_user),
    query: str = Depends(get_query_param),
) -> dict:
    await asyncio.sleep(0.001)
    return {"user": user, "query": query}


# -----------------------
# App & route definition
# -----------------------

app = FastAPI()


@app.get("/items/{item_id}", dependencies=[Depends(get_token_header)])
async def read_item(
    item_id: int,
    context: dict = Depends(get_full_context),
) -> dict:
    return {"item_id": item_id, "context": context}


# -----------------------
# Performance test
# -----------------------

def test_get_route_handler_performance(loop_count: int = 50_000_000):
    route: APIRoute = app.routes[-1]
    assert route.path == '/items/{item_id}'

    start_time = time.perf_counter()

    for _ in range(loop_count):
        route.get_route_handler()

    duration = time.perf_counter() - start_time

    print(f"Called get_route_handler() {loop_count:,} times in {duration:.4f}s")
    print(f"Average per call: {duration / loop_count * 1e6:.2f} µs")


if __name__ == "__main__":
    test_get_route_handler_performance()

Before:
callable_cache_fastapi_0 121

After:
callable_cache_fastapi_0 121_half_patched

Copy link
Member

@YuriiMotov YuriiMotov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@dolfinus, thank you!

Before:

$ python perf.py 
Called get_route_handler() 50,000,000 times in 49.6951s
Average per call: 0.99 µs

After:

$ python perf.py 
Called get_route_handler() 50,000,000 times in 31.5395s
Average per call: 0.63 µs

@YuriiMotov YuriiMotov removed their assignment Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants