FastAPI performance optimization

Logo
Table of contents
  • Verdict
  • FastAPI JSON response classes

    FastAPI supports custom response classes, among them there is support for multiple JSON response implementations. Default is JSONResponse but orjson and ujson are available as well. Both have their benchmark / performance testclaiminig they are the fastest, but worths checking for the given usecase.

    Note: FastAPI supports different response classes, but request parsing is done by Starlette where you don’t have control over which JSON implementation to be used

    JSON response classes test

    CI run 29770319196 — Python 3.14, Ubuntu latest.

    Test environment

    Baseline measurement

    By default, FastAPI uses the base JSON implementation, let’s see the results:

    Test attribute Test run 1 Test run 2 Test run 3 Average
    Requests per second 19.51 19.64 19.62 19.59
    Time per request [ms] 5125.68 5091.15 5096.26 5104.36

    Orjson

    Note: There are some specialities requre attention

    Test attribute Test run 1 Test run 2 Test run 3 Average Difference to baseline
    Requests per second 21.29 20.2 21.62 21.0367 +7.38 %
    Time per request [ms] 4696.16 4951.17 4624.58 4757.3 347.06 ms

    UltraJSON

    Note: Just like orjson this has its own speciality

    Test attribute Test run 1 Test run 2 Test run 3 Average Difference to baseline
    Requests per second 19.52 19.57 19.43 19.5067 -0.43 %
    Time per request [ms] 5122.47 5108.58 5145.63 5125.56 -21.2 ms

    Cross-runner JSON response class comparison

    CI run 29858316413 — Python 3.14, Ubuntu latest.

    The same JSON response class comparison was repeated across 3 key server runners (Gunicorn 2 workers 0 threads, Uvicorn single-process, FastAPI CLI 1 worker) to see whether the choice of response class interacts with the runner type.

    Sync endpoint (/sync/big_json_response/ — 1MB payload)

    Runner JSONResponse (baseline) ORJSONResponse UJSONResponse
    Gunicorn (2 workers, 0 threads) 17.65 RPS 18.44 RPS (+4.46%) 17.26 RPS (-2.19%)
    Uvicorn single 9.15 RPS 10.36 RPS (+13.26%) 9.40 RPS (+2.81%)
    FastAPI CLI (1 worker) 9.45 RPS 10.66 RPS (+12.81%) 9.84 RPS (+4.13%)

    Async endpoint (/async/big_json_response/ — 1MB payload)

    Runner JSONResponse (baseline) ORJSONResponse UJSONResponse
    Gunicorn (2 workers, 0 threads) 17.06 RPS 18.15 RPS (+6.39%) 17.30 RPS (+1.41%)
    Uvicorn single 9.16 RPS 10.38 RPS (+13.35%) 9.29 RPS (+1.42%)
    FastAPI CLI (1 worker) 9.48 RPS 10.58 RPS (+11.64%) 9.74 RPS (+2.74%)

    Observations

    Verdict

    Individual impact: +4-13% throughput by switching from JSONResponse to ORJSONResponse.

    Please note that you can have different JSON response class for each API endpoint as shown in the FastAPI docs:

    from fastapi import FastAPI
    from fastapi.responses import UJSONResponse
    
    app = FastAPI()
    
    
    @app.get("/items/", response_class=UJSONResponse)
    async def read_items():
        return [{"item_id": "Foo"}]