# Zero-Allocation JSON Parsing
Optimizing ingestion pipelines by avoiding GC pressure with custom unmarshalers.
Context
Our event ingestion pipeline was processing 100k events/second, but Go's standard encoding/json was causing significant GC pressure. The heap was growing uncontrollably during peak hours.
Performance tip: Always profile memory allocations before optimizing. We used
go tool pprofto identify the bottleneck.
Background
The ingestion pipeline runs on a Kubernetes cluster with auto-scaling enabled. Under normal load, things were fine. But during traffic spikes:
- GC cycles increased from 2ms to 45ms
- Goroutine pauses caused downstream latency
- Heap grew to 8GB within minutes
Why Standard Library Falls Short
Go's encoding/json is excellent for general use, but it allocates heavily for:
- Intermediate
map[string]interface{}structures - Reflection-based type resolution
- Temporary buffers during marshaling
Approach
We switched to a zero-allocation JSON parser using json-iterator with custom unmarshalers. By reusing buffers and avoiding intermediate allocations, we reduced memory churn significantly.
The Core Idea
Reuse, don't reallocate. Every allocation is a tax on your garbage collector.
We implemented three strategies:
- Buffer pooling with
sync.Pool - Custom struct tags for direct streaming
- Pre-allocated slices sized to expected payload shapes
package main
import (
"fmt"
"github.com/json-iterator/go"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type Event struct {
ID string `json:"id"`
Data []byte `json:"data"`
}
func main() {
event := Event{ID: "evt_123", Data: []byte("payload")}
b, _ := json.Marshal(event)
fmt.Println(string(b))
}
Streaming Parser Example
For even larger payloads, we used a streaming approach:
use serde_json::StreamDeserializer;
use serde::Deserialize;
#[derive(Deserialize)]
struct LogEntry {
timestamp: u64,
level: String,
message: String,
}
fn main() {
let data = br#"{"timestamp":1705920000,"level":"INFO","message":"ok"}"#;
let stream = StreamDeserializer::from_slice(data);
for entry in stream {
println!("{:?}", entry.unwrap());
}
}
Benchmarks
Throughput Comparison
| Parser | Speed | GC Time | Allocations/sec |
|---|---|---|---|
| Standard library | 850 MB/s | 45% | 2.4M |
| Custom parser | 1.2 GB/s | 5% | 120K |
| Streaming (Rust) | 2.1 GB/s | N/A | 80K |
Key Improvements
- Standard library: 850 MB/s, 45% GC time
- Custom parser: 1.2 GB/s, 5% GC time
- Memory usage: Reduced by 70%
- Zero-allocation goal: Not fully reached yet
Memory Profile
- Before optimization
- Heap: 8GB
- GC pauses: 45ms
- Goroutines: 12,000
- After optimization
- Heap: 2.4GB
- GC pauses: 3ms
- Goroutines: 12,000
Trade-offs
The code is more verbose and harder to maintain. We only applied this optimization to the hot path (ingestion workers), keeping the standard library for less critical paths.
When to Optimize
Premature optimization is the root of all evil. But measured optimization is the root of all performance.
Consider zero-allocation parsing when:
- Your throughput exceeds 100k events/second
- GC pauses exceed your p99 latency budget
- Memory is constrained (e.g., edge devices, IoT gateways)
Don't optimize when:
- Your pipeline is I/O bound
- Your payloads are small (< 1KB)
- Team velocity is more important than marginal gains
Design Philosophy
┌─────────────────────────────────────────┐
│ Optimize for readability first │
│ Measure with profilers second │
│ Optimize hot paths third │
│ Document trade-offs always │
└─────────────────────────────────────────┘
Advanced Examples
Inline Elements
- Bold text for emphasis
- Italic text for terminology
Strikethroughfor deprecated APIsinline codefor variable names likejsonConfigorbufferSize- Combined:
bold codeanditalic code
Blockquotes
Single-level blockquote.
Nested blockquotes:
This is a nested quote. It helps show hierarchy of thoughts.
Deep nesting for dramatic effect.
Autolinks
Visit https://github.com/json-iterator/go directly — no brackets needed with GFM.
Links
- json-iterator on GitHub
- See also our internal architecture decisions
- Footnotes provide extra context without cluttering the main text1.
Summary
Key takeaway: Zero-allocation JSON parsing is powerful but not free. Use it where it matters, measure everything, and keep your code maintainable. The custom parser reduced GC pauses by 90% in high-throughput scenarios — but only after we proved the bottleneck with
pprof.
Footnotes
-
This is a footnote. GFM supports footnotes natively, and they appear at the bottom of the rendered document. ↩
The custom parser reduced GC pauses by 90% in high-throughput scenarios.