orjson and msgspec: Serialization Wins for FastAPI Worker Fleets

orjson and msgspec: Serialization Wins for FastAPI Worker Fleets

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 orjson response class or msgspec structs 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

  • orjson rejects naive non-UTC datetimes depending on options — set OPT_NAIVE_UTC deliberately.
  • msgspec structs 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

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply