orjson: Fast JSON in Python (2026)
orjson: Fast JSON in Python (2026) shows how to replace stdlib json with a Rust-backed encoder that returns bytes, natively serializes datetime and UUID, and wins a real timing race on a 250-item nested payload — without starting a server.
Pair fast JSON with typed checks via Astral ty, stream line-delimited responses with FastAPI JSON Lines, or move columnar batches with PyArrow Tables + Parquet.
TL;DR
orjson.dumpsreturns UTF-8bytes(notstr); decode only when you need text.orjson.loadsacceptsbytesorstr— prefer bytes when you already have them.- datetime and UUID serialize natively; use
option=orjson.OPT_INDENT_2for pretty output. - Install with
pip install orjson(we tested3.12.0on Python 3.13.5). - Real run: 250 nested items → dumps 7.9× faster and loads 2.0× faster than stdlib
json.
Why orjson in 2026?
API handlers, job queues, and LLM tool payloads are still JSON-heavy. Stdlib json is fine for small blobs, but Rust-backed orjson cuts encode latency and avoids custom default= hooks for common types. If you already ship FastAPI or write NDJSON, dropping in orjson is one of the cheapest wins in the Efficient Code category.
| API | Returns / accepts | Use in 2026 |
|---|---|---|
orjson.dumps | bytes (UTF-8) | HTTP bodies, files, caches |
orjson.loads | dict / list / scalars | Parse request or file bytes |
OPT_INDENT_2 | pretty bytes | Debug dumps only |
Versions tested (2026-10-03)
- Python
3.13.5 orjson3.12.0- Timing: 2000 repeats on a 250-item nested payload (~30 KB compact)
python -m venv .venv && source .venv/bin/activate
pip install orjson==3.12.0
python orjson_demo.py
1. dumps, loads, and native types
Save this as orjson_demo.py. Build a payload with timezone-aware datetime and a fixed UUID, then dump and load without a default callback.
import json
import sys
import time
import uuid
from datetime import datetime, timezone
import orjson
print("Python", sys.version.split()[0])
print("orjson", orjson.__version__)
uid = uuid.UUID("886313e1-3b8a-5372-9b90-0c9aee199e5d")
created = datetime(2026, 10, 3, 8, 0, 0, tzinfo=timezone.utc)
items = [
{
"sku": f"SKU-{i:04d}",
"qty": i % 50 + 1,
"price": round(9.99 + (i % 17) * 1.25, 2),
"tags": ["fast", "json", f"batch-{i % 7}"],
"meta": {"ok": True, "idx": i, "note": f"row-{i}"},
}
for i in range(250)
]
payload = {
"event": "order.batch",
"created_at": created,
"request_id": uid,
"count": len(items),
"items": items,
"flags": {"indent": False, "source": "orjson_demo"},
}
raw = orjson.dumps(payload)
print("dumps_type", type(raw).__name__)
print("dumps_bytes", len(raw))
pretty = orjson.dumps(payload, option=orjson.OPT_INDENT_2)
print("indent2_bytes", len(pretty))
loaded = orjson.loads(raw)
print("loads_count", loaded["count"])
print("loads_created_at", loaded["created_at"])
print("loads_request_id", loaded["request_id"])
print("roundtrip_ok", loaded["count"] == 250)
snippet = orjson.dumps(
{"created_at": created, "request_id": uid, "count": 250}
)
print("native_types_json", snippet.decode())
dumps returns bytes, not str. After loads, datetime and UUID come back as RFC 3339 / UUID strings — rehydrate them in your schema layer if you need objects again.
2. Real timing vs stdlib json
Same payload, 2000 repeats. Stdlib needs a small default for datetime/UUID; orjson does not.
N = 2000
def _default(o):
if isinstance(o, datetime):
return o.isoformat()
if isinstance(o, uuid.UUID):
return str(o)
raise TypeError(type(o))
for _ in range(5):
orjson.dumps(payload)
json.dumps(payload, default=_default).encode()
t0 = time.perf_counter()
for _ in range(N):
orjson.dumps(payload)
t_orjson_dumps = (time.perf_counter() - t0) / N * 1000
t0 = time.perf_counter()
for _ in range(N):
json.dumps(payload, default=_default).encode("utf-8")
t_json_dumps = (time.perf_counter() - t0) / N * 1000
blob = orjson.dumps(payload)
blob_str = blob.decode()
t0 = time.perf_counter()
for _ in range(N):
orjson.loads(blob)
t_orjson_loads = (time.perf_counter() - t0) / N * 1000
t0 = time.perf_counter()
for _ in range(N):
json.loads(blob_str)
t_json_loads = (time.perf_counter() - t0) / N * 1000
print(f"orjson_dumps_ms {t_orjson_dumps:.4f}")
print(f"json_dumps_ms {t_json_dumps:.4f}")
print(f"dumps_speedup {t_json_dumps / t_orjson_dumps:.1f}x")
print(f"orjson_loads_ms {t_orjson_loads:.4f}")
print(f"json_loads_ms {t_json_loads:.4f}")
print(f"loads_speedup {t_json_loads / t_orjson_loads:.1f}x")
print("OK orjson dumps/loads + timing")
Real output (this machine)
Python 3.13.5
orjson 3.12.0
dumps_type bytes
dumps_bytes 30156
indent2_bytes 60449
indent2_head {
"event": "order.batch",
"created_at": "2026-10-03T08:00:00+00:00",
"requ
loads_count 250
loads_created_at 2026-10-03T08:00:00+00:00
loads_request_id 886313e1-3b8a-5372-9b90-0c9aee199e5d
roundtrip_ok True
native_types_json {"created_at":"2026-10-03T08:00:00+00:00","request_id":"886313e1-3b8a-5372-9b90-0c9aee199e5d","count":250}
payload_items 250
timing_repeats 2000
orjson_dumps_ms 0.0445
json_dumps_ms 0.3493
dumps_speedup 7.9x
orjson_loads_ms 0.1153
json_loads_ms 0.2276
loads_speedup 2.0x
OK orjson dumps/loads + timing
Compact dump is 30156 bytes; pretty OPT_INDENT_2 roughly doubles size. On this box, dumps is about 7.9× faster and loads about 2.0× faster than stdlib json for the same 250-item document.
Common upgrades
- HTTP: write
orjson.dumps(obj)straight to the response body; setContent-Type: application/json. - Pretty debug:
option=orjson.OPT_INDENT_2— slower and larger; keep it out of hot paths. - UTC Z suffix:
option=orjson.OPT_UTC_Zfor...Zinstead of+00:00. - NDJSON: encode each row with orjson, then append a newline (or use a JSONL helper).
When to use what
| Tool | Best for |
|---|---|
| orjson | Fast encode/decode, native datetime/UUID, API hot paths |
| stdlib json | Tiny scripts, teaching, when bytes vs str does not matter |
| msgspec / Pydantic | Schema validation on top of (or instead of) raw JSON |
FAQ
Why bytes instead of str? UTF-8 bytes are what sockets and files want; decoding to str only when needed avoids an extra copy.
Do I need OPT_SERIALIZE_UUID? Not in orjson 3.x — UUID (and datetime) serialize by default.
Is orjson a drop-in for json.dumps? Almost: change call sites that expect str to use .decode() or pass bytes through.
Next steps
Swap json.dumps in one hot endpoint, keep the timing harness above as a regression check, then stream JSON Lines from FastAPI when clients need incremental results.