TLDR: use orjson when you need performance when dump(Serialize) json data, where every millisecond counts.

Introduction

In our current project at work, I noticed that my team uses orjson instead of Python's standard json module. At first, I was like: "Why do we need to pip install an external library when Python already has built-in json support?". I'm a simple guy who likes to keep things simple xD

My boss told me that orjson is much faster than standard json, and I was like: "Ohh, this shit is definitely something worth digging into!"

To satisfy my curiosity, I wrote use AI to generate a small benchmark to test both on 500,000 records. And the result is good as I expected for: orjson is faster (about ~9.3x) on serialization (dumps)!!!


Section 1: Quick Comparison

Criteria Python standard json orjson
Output of dumps str (common string) bytes (b'...' type)
Speed of dumps (Serialize) Baseline (common) Faster ~9x – 10x
Speed of loads (Deserialize) Baseline (common) Faster ~1.2x – 1.3x
Dict keys type Easy (Force int, bool becomes str) Strict (must be str)

Section 2: Benchmark Results

I benchmarked 3 different scenarios with 500,000 records on my local machine:

  1. bench_orjson.py: orjson with raw datetime & UUID.
  2. bench_stdlib_json_no_str.py: Standard json (no pre-str), with raw datetime & UUID + default=str.
  3. bench_stdlib_json.py : Standard json, pre-wrapped str() during data creation.

The Numbers

Environment:

  • Python: 3.10
  • orjson: 3.10.0
  • CPU: AMD Ryzen 5900X
((venv) ) kienlt@kienlt-pc:/data/work$ python bench_orjson.py 
Generating 500,000 records with raw datetime & UUID...
[ORJSON] Dumps time : 0.1439 seconds
[ORJSON] Payload size: 71045.02 KB
[ORJSON] Loads time : 0.2917 seconds

((venv) ) kienlt@kienlt-pc:/data/work$ python bench_stdlib_json_no_str.py 
Generating 500,000 records with raw datetime & UUID...
[STANDARD JSON] Dumps time : 1.3381 seconds
[STANDARD JSON] Payload size: 75927.84 KB
[STANDARD JSON] Loads time : 0.3811 seconds

((venv) ) kienlt@kienlt-pc:/data/work$ python bench_stdlib_json.py 
Generating 500,000 records...
[STANDARD JSON] Dumps time : 0.3866 seconds
[STANDARD JSON] Payload size: 75927.84 KB
[STANDARD JSON] Loads time : 0.3766 seconds

Better view

Engine & Scenario Dumps Time (Serialize) Loads Time (Deserialize) Payload Size Comparison (Dumps)
orjson (Raw Datetime & UUID) 0.1439s 0.2917s 71,045.02 KB (~69.3 MB) Baseline (Fastest)
Standard json (Pre-converted str()) 0.3866s 0.3766s 75,927.84 KB (~74.1 MB) ~2.68x slower
Standard json (Raw + default=str) 1.3381s 0.3811s 75,927.84 KB (~74.1 MB) ~9.30x slower

Section 3: Why orjson is faster?

The "Python Callback" Bottleneck

In real-world applications (FastAPI, ORMs, Data pipelines), data usually contains real objects like datetime.datetime and uuid.UUID.

  • Standard json: Doesn't know what datetime is. For every record, it has to jump back to Python to execute default=str. With 500k records (1 UUID + 1 datetime each), Python runs 1M callbacks. This kills serialization performance.
  • orjson: Written in Rust. It reads the CPython object memory pointers directly and formats them into ISO-8601 strings in machine code without touching Python runtime.

Outputting bytes directly

Here is a quick example:

import json
import orjson

data = {"username": "kienlt", "role": "admin"}

res_json = json.dumps(data) # <--- json.dumps() returns str, not bytes
print(type(res_json))  # <class 'str'>
print(res_json)

res_orjson = orjson.dumps(data)  # <--- orjson.dumps() returns bytes, not str
print(type(res_orjson))  # <class 'bytes'>
print(res_orjson)

Output:

<class 'str'>
{"username": "kienlt", "role": "admin"}
<class 'bytes'>
b'{"username":"kienlt","role":"admin"}'

Why is the payload ~4.77 MB smaller

  • Standard json: Uses separators=(', ', ': '), adding a space after every comma and colon.

json

  • orjson: Uses compact format separators=(',', ':') by default, eliminating all unnecessary whitespace. Every eliminated whitespace adds up to saved bandwidth and memory! Take another look at the output above:
<class 'str'>
{"username": "kienlt", "role": "admin"} ==> spaces
<class 'bytes'>
b'{"username":"kienlt","role":"admin"}' ==> No fuckin' space

You can see there is no fuckin' space when we print!


Section 4: When should you use which?

Use Standard json when

  • Writing simple automation scripts or CLI tools where you want zero external dependencies (pip install).
  • Performance is not a bottleneck (who cares if an automation script runs 1-2ms faster?).

Use orjson when

  • Building high-throughput Web APIs where performance is a must (e.g. FastAPI handling thousands of requests or IoT devices!!!)
  • Processing large JSON files or stream data (Kafka consumers, log pipelines).

Trade-offs of orjson (things to know before switching)

  • Indent is limited to 2 spacesoption=orjson.OPT_INDENT_2 is the only indentation level supported, unlike stdlib's indent=N which accepts any integer. (orjson README) Since orjson is designed with performance as the top priority, hardcoding the indent logic lets the Rust compiler optimize it better at compile time, instead of handling an arbitrary N at runtime.
  • sort_keys isn't a kwarg — it's replaced by option=orjson.OPT_SORT_KEYS, so any code doing json.dumps(data, sort_keys=True) needs a small rewrite. (orjson README) Standard json parses sort_keys as a keyword argument and checks it on every call. orjson replaces this with a single bitwise flag, so the check becomes one cheap bit operation instead of parsing/validating a Python kwarg each time — saving a bit of CPU overhead.
  • And more, but I don't fully understand them yet, so I'll leave them out of this article xD

References


Published

Category

Knowledge Base

Tags

Contact