Skip to content

Conversation

@dolfinus
Copy link
Contributor

@dolfinus dolfinus commented Dec 3, 2025

Motivation

Currently if app.depedendency_overrides is not empty then each request generates new Dependant, instead of reusing one build while declaring a route. This effects performance of all requests, even if specific dependency is not actually overridden. I'm personally use dependency overrides on production apps (see #13913), so all my applications are affected.

Change description

If dependency is not overridden, reuse existing Dependant with cached inspect results. This avoids performance hit, in my case 460RPS -> 630RPS.

Also simplified check of dependency_overrides makes resolution of complex dependency trees faster, even when no dependency overrides used in the first place.

Microbenckmark:

Details
import asyncio
import time
from typing import Annotated

from pydantic import BaseModel

from fastapi import FastAPI, Depends, Query, Response
from httpx import AsyncClient, ASGITransport


async def test_no_dependency_override_performance(loop_count: int = 100_000):
    app = FastAPI()

    async def sub_sub_dependency(more_nested: Annotated[int, Query()]) -> int:
        return more_nested

    class SubDependency(BaseModel):
        value: Annotated[int, Query()]
        other: Annotated[int, Depends(sub_sub_dependency)]

    async def some_dependency(nested: Annotated[SubDependency, Depends()]) -> int:
        return nested.value + nested.other

    @app.get("/data")
    async def get(
        one: int = Depends(some_dependency),
    ):
        return Response(content=f'{{"value": {one}}}'.encode(), status_code=200)

    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://localhost",
    ) as client:
        start_time = time.perf_counter()

        for _ in range(loop_count):
            response = await client.get("/data", params={"value": 1, "more_nested": 2})
            assert response.status_code == 200, response.text
            assert response.text == '{"value": 3}'

        duration = time.perf_counter() - start_time

    print(f"Performed GET /data requests without dependency override: {loop_count:,} times in {duration:.4f}s")
    print(f"Average per call: {duration / loop_count * 1e6:.2f} µs")


async def test_dependency_override_nonempty_performance(loop_count: int = 100_000):
    app = FastAPI()

    async def sub_sub_dependency(more_nested: Annotated[int, Query()]) -> int:
        return more_nested

    class SubDependency(BaseModel):
        value: Annotated[int, Query()]
        other: Annotated[int, Depends(sub_sub_dependency)]

    async def some_dependency(nested: Annotated[SubDependency, Depends()]) -> int:
        return nested.value + nested.other

    async def unused_dependency(nested: Annotated[SubDependency, Depends()]) -> int:
        return nested.value + nested.other + 1

    @app.get("/data")
    async def get(
        one: int = Depends(some_dependency),
    ):
        return Response(content=f'{{"value": {one}}}'.encode(), status_code=200)

    async def override_unused_dependency() -> int:
        return 22

    app.dependency_overrides[unused_dependency] = override_unused_dependency

    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://localhost",
    ) as client:
        start_time = time.perf_counter()

        for _ in range(loop_count):
            response = await client.get("/data", params={"value": 1, "more_nested": 2})
            assert response.status_code == 200, response.text
            assert response.text == '{"value": 3}'

        duration = time.perf_counter() - start_time

    print(f"Performed GET /data requests with non-empty dependency overrides: {loop_count:,} times in {duration:.4f}s")
    print(f"Average per call: {duration / loop_count * 1e6:.2f} µs")


async def test_dependency_override_performance(loop_count: int = 100_000):
    app = FastAPI()

    async def sub_sub_dependency(more_nested: Annotated[int, Query()]) -> int:
        return more_nested

    class SubDependency(BaseModel):
        value: Annotated[int, Query()]
        other: Annotated[int, Depends(sub_sub_dependency)]

    async def some_dependency(nested: Annotated[SubDependency, Depends()]) -> int:
        return nested.value + nested.other

    @app.get("/data")
    async def get(
        one: int = Depends(some_dependency),
    ):
        return Response(content=f'{{"value": {one}}}'.encode(), status_code=200)

    async def sub_sub_dependency_override(more_nested: Annotated[int, Query()]) -> int:
        return 11

    app.dependency_overrides[sub_sub_dependency] = sub_sub_dependency_override

    async with AsyncClient(
        transport=ASGITransport(app=app),
        base_url="http://localhost",
    ) as client:
        start_time = time.perf_counter()

        for _ in range(loop_count):
            response = await client.get("/data", params={"value": 1, "more_nested": 2})
            assert response.status_code == 200, response.text
            assert response.text == '{"value": 12}', response.text

        duration = time.perf_counter() - start_time

    print(f"Performed GET /data requests with actual dependency override: {loop_count:,} times in {duration:.4f}s")
    print(f"Average per call: {duration / loop_count * 1e6:.2f} µs")

if __name__ == "__main__":
    asyncio.run(test_no_dependency_override_performance())
    print("==" * 20)
    asyncio.run(test_dependency_override_nonempty_performance())
    print("==" * 20)
    asyncio.run(test_dependency_override_performance())
    print("==" * 20)

Before

Performed GET /data requests without dependency override: 100,000 times in 81.2075s
Average per call: 812.08 µs

Flamegraph

before_no_dependency_override

Performed GET /data requests wit dependency override of unrelated dependency: 100,000 times in 220.0742s
Average per call: 2200.74 µs

Flamegraph

before_dependency_override_nonempty

Performed GET /data requests wit dependency override: 100,000 times in 236.5470s
Average per call: 2365.47 µs

Flamegraph

before_dependency_override

After:

Performed GET /data requests without dependency override: 100,000 times in 66.0966s
Average per call: 660.97 µs

Flamegraph

after_no_dependency_override

Performed GET /data requests with non-empty dependency overrides: 100,000 times in 67.9596s
Average per call: 679.60 µs

Flamegraph

after_dependency_override_nonempty

Performed GET /data requests with actual dependency override: 100,000 times in 106.0456s
Average per call: 1060.46 µs

Flamegraph

after_dependency_override

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.

I still don't think it's good idea to use depedendency_overrides in production for dependency injection.
But suggested changes look reasonable to me - no need to re-create dependant on every call.

So.. LGTM!

@dolfinus, thank you!

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