json.dumps on large response graphs quietly owns CPU on FastAPI fleets. Microbenchmarks lie; the unfair advantage is swapping in orjson or msgspec and measuring p99 + worker CPU under production-shaped payloads with the same Pydantic models you ship.
⚡ TL;DR: Install
orjsonresponse class ormsgspecstructs for the hottest routes; keep Pydantic for inbound validation; benchmark p99 under realistic nested payloads; watch unicode/datetime edge cases. Pair with Pydantic v2 Validators and Bytes vs str Boundaries.
FastAPI + orjson
# ✅ Drop-in ORJSONResponse
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from pydantic import BaseModel
app = FastAPI(default_response_class=ORJSONResponse)
class Item(BaseModel):
id: str
score: float
tags: list[str]
@app.get("/items/{item_id}", response_model=Item)
async def get_item(item_id: str) -> Item:
return Item(id=item_id, score=0.9, tags=["a", "b"])
# ✅ msgspec for the absolute hottest encoder path
import msgspec
class ItemStruct(msgspec.Struct):
id: str
score: float
tags: list[str]
encoder = msgspec.json.Encoder()
def encode_item(item: ItemStruct) -> bytes:
return encoder.encode(item) # returns bytes — perfect for Response
What to measure
| Metric | Why |
|---|---|
| p99 latency | Serialization sits on the request path |
| Worker CPU % | Often the real capacity unlock |
| RSS | Large dumps can spike temps |
| Error rate | datetime/UUID/NaN edge cases |
# ❌ Declaring victory from dumps of {"a": 1} in a notebook
# ✅ Replay production response fixtures (PII-redacted) in CI bench
import time
import orjson
import json
def bench(fn, payload, n=2000):
t0 = time.perf_counter()
for _ in range(n):
fn(payload)
return (time.perf_counter() - t0) / n * 1000
payload = {"items": [{"id": str(i), "tags": ["x"] * 20} for i in range(200)]}
print("stdlib", bench(lambda p: json.dumps(p).encode(), payload))
print("orjson", bench(orjson.dumps, payload))
Gotchas seniors hit
orjsonrejects naive non-UTC datetimes depending on options — setOPT_NAIVE_UTCdeliberately.msgspecstructs are not Pydantic models; convert at the boundary or dual-define.- Numpy / Decimals need explicit options or pre-normalization.
Closing checklist
✅ Dos
– ✅ Use ORJSONResponse (or msgspec) on hot read paths
– ✅ Bench with production-shaped fixtures
– ✅ Keep inbound validation on Pydantic v2
– ✅ Return bytes bodies to skip a final encode
– ✅ Document datetime/UUID encoding options in ADRs
❌ Don’ts
– ❌ Don’t trust microbenchmarks on tiny dicts
– ❌ Don’t mix three JSON libs in one service without boundaries
– ❌ Don’t forget custom types (Decimal, set)
– ❌ Don’t enable pretty-print in production responses
Related reading
- Pydantic v2 Validators
- Bytes vs str Boundaries
- Structured Clone vs JSON in Node
- CPython Specialize Stats
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
