← Back to Blog
·9 min read

JSON Serialization Is Your API's Hidden Bottleneck

In high-throughput APIs, JSON serialization and deserialization takes meaningful CPU time — but most backend developers never measure it. They profile database queries, optimize network I/O, and reduce memory allocations, then leave the serialization layer untouched because it seems like infrastructure. It is not. Here is what the numbers actually show.

The baseline: how much does JSON serialization actually cost?

At low request rates, JSON serialization is invisible. Serialize a 10KB response in 0.5ms and it is lost in the noise of database queries and network latency. But as request volume increases, that 0.5ms per request becomes a throughput ceiling. At 2,000 req/sec, JSON serialization alone consumes a full CPU core — and most production services are running multiple cores doing nothing but encoding text.

Representative serialization benchmarks for a medium-complexity object (user profile with 15 fields, nested address, array of permissions):

Node.js serialization benchmarks (ops/second, higher = better):
---------------------------------------------------------------
JSON.stringify (built-in)         ~1,200,000 ops/sec
fast-json-stringify (schema)      ~4,500,000 ops/sec  (+275%)
@sinclair/typebox + fast-stringify ~5,200,000 ops/sec  (+333%)

Python serialization benchmarks:
---------------------------------------------------------------
json.dumps (stdlib)               ~200,000 ops/sec
ujson                             ~800,000 ops/sec   (+300%)
orjson                            ~1,400,000 ops/sec (+600%)

Go serialization benchmarks:
---------------------------------------------------------------
encoding/json (stdlib)            ~500,000 ops/sec
json-iterator                     ~1,800,000 ops/sec (+260%)
sonic                             ~3,200,000 ops/sec (+540%)

These are not minor optimizations. A 3-6x improvement in serialization throughput, for free, by swapping a library. The question is whether your service is bottlenecked there — and the only way to answer that is to measure.

How to profile serialization specifically

Most profiling tools show CPU time by function, but JSON serialization is often invisible because it happens inside built-in runtime functions that appear as a single "JSON.stringify" entry. The trick is to measure it explicitly in isolation:

// Node.js: measure serialization time separately
import { performance } from 'perf_hooks';

// In your request handler, time serialization explicitly:
app.get('/api/users', async (req, res) => {
  const users = await db.users.findAll();

  const serializeStart = performance.now();
  const body = JSON.stringify(users);
  const serializeMs = performance.now() - serializeStart;

  // Log to your metrics system
  metrics.histogram('json_serialize_ms', serializeMs, {
    endpoint: '/api/users',
    count: users.length,
  });

  res.setHeader('Content-Type', 'application/json');
  res.send(body);
});

// If serializeMs is > 5% of your total handler time, it's worth optimizing.
// For most APIs at < 500 req/sec, it won't be.

fast-json-stringify: schema-based serialization in Node.js

The key insight behind fast serializers is that dynamic serialization — inspecting an unknown object at runtime, deciding how to encode each field — is slow. Schema-based serialization compiles the schema into a specialized serialization function that knows in advance what types to expect and how to encode them, eliminating the runtime inspection overhead.

import fastJson from 'fast-json-stringify';

// Define the schema once — this is compile-time work
const stringifyUser = fastJson({
  type: 'object',
  properties: {
    id: { type: 'integer' },
    username: { type: 'string' },
    email: { type: 'string' },
    role: { type: 'string' },
    createdAt: { type: 'string' },
    permissions: {
      type: 'array',
      items: { type: 'string' },
    },
  },
});

// Use at request time — this is 3-4x faster than JSON.stringify
app.get('/api/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  res.setHeader('Content-Type', 'application/json');
  res.send(stringifyUser(user)); // compiled, type-aware serialization
});

Fastify, the high-performance Node.js web framework, uses fast-json-stringify internally for all route serialization. If you define response schemas in Fastify (which you should for documentation reasons anyway), you get the performance benefit automatically.

orjson and ujson: faster JSON in Python

Python's standard library json module is pure Python and correspondingly slow. For any Python API handling more than a few hundred requests per second, dropping in a faster serializer is one of the easiest performance wins available.

# orjson — fastest Python JSON library, with useful extras
import orjson

# Faster than json.dumps
data = {"users": [...], "total": 1000}
serialized = orjson.dumps(data)  # returns bytes, not str

# orjson handles types that stdlib json does not:
from datetime import datetime
from uuid import UUID

orjson.dumps({
  "created": datetime.now(),      # serialized as ISO 8601 string
  "id": UUID("12345678-..."),     # serialized as UUID string
  "value": float('nan'),          # raises ValueError — explicit, not silent null
})

# Drop-in replacement in FastAPI / Starlette:
from fastapi.responses import ORJSONResponse

@app.get("/api/users", response_class=ORJSONResponse)
async def get_users():
    return await db.users.find_all()  # serialized by orjson automatically

Go: encoding/json vs sonic

Go's standard library encoding/json is reasonably fast but uses reflection — inspecting struct fields at runtime — which has overhead. For Go services at high throughput, the reflection cost accumulates.

// Option 1: json-iterator — drop-in replacement for encoding/json
import jsoniter "github.com/json-iterator/go"

var json = jsoniter.ConfigCompatibleWithStandardLibrary

// Identical API to encoding/json:
bytes, err := json.Marshal(user)
err = json.Unmarshal(bytes, &user)

// Option 2: sonic — SIMD-accelerated, fastest Go JSON library
import "github.com/bytedance/sonic"

bytes, err := sonic.Marshal(user)
err = sonic.Unmarshal(bytes, &user)

// Option 3: easyjson — code generation, eliminates reflection entirely
// Run: easyjson -all models/user.go
// Generates user_easyjson.go with MarshalJSON/UnmarshalJSON methods
// No runtime reflection — uses generated type-aware code

// Benchmark results (typical struct with 10 fields):
// encoding/json:  500,000 ops/sec
// json-iterator:  1,800,000 ops/sec
// sonic:          3,200,000 ops/sec
// easyjson:       2,400,000 ops/sec

When to actually optimize: the 1,000 req/sec threshold

The argument that serialization performance matters is true, but it requires context. For the vast majority of APIs — those handling fewer than 1,000 req/sec — JSON serialization is not the bottleneck. Database queries, external API calls, and business logic are the bottlenecks, and optimizing serialization before those is premature.

The pragmatic rule: add serialization performance measurement to your baseline profiling setup. Run it against realistic load. If JSON serialization accounts for more than 5% of handler time at your target throughput, switch to a faster library. If it does not, optimize something else.

The services where serialization is genuinely the bottleneck are: high-frequency financial data APIs, real-time gaming backends, telemetry ingestion pipelines, and any service that aggregates and re-serializes large amounts of data from upstream sources. For these, the library switch is a free performance multiplier. For a typical CRUD API serving thousands of daily active users, the correct optimization is a better database index.

Published June 2, 2026 · By the utili.dev Team