Benchmark JSON Libraries for Go on Realistic Payloads
Benchmark encoding/json, goccy/go-json and json-iterator on three payload sizes: decoding a 21 KB order took 239 µs, 27 µs and 54 µs

encoding/json is the library every Go service starts with, and it is also the first thing a profile blames when the service is a JSON gateway. Alternatives promise several times the speed, and a few of them deliver. They also bring their own costs: another dependency, behaviour that differs from the standard library in corners, and maintenance you do not control. This article runs encoding/json, goccy/go-json and json-iterator over the same order payload at three sizes, encoding and decoding, and shows the speed, the bytes allocated and the number of allocations, so the choice can be made with numbers and not reputation.
Tested with Go 1.25.6, goccy/go-json v0.11.2, json-iterator v1.1.12, Apple M1 Max (10 cores, 32 GB), macOS arm64, median of 5 runs of 150 ms
For techniques that speed up the standard library without a new dependency, read faster JSON in Go. The method is from benchmarking a Go function properly, and the wire format comparison with Protocol Buffers is in Protobuf or JSON speed. The cornerstone is the Go performance guide.
The payload and the libraries
An Order has scalar fields, a slice of line items (a SKU string, an integer, a float), a short slice of tags, and a map[string]string. The three sizes have 1, 20 and 500 items, which come to 266, 1,057 and 21,637 bytes of JSON. Each library encodes the struct and decodes the standard library's output back into a fresh Order. The same bytes column checks that the library's output is the same as the standard library's, or at least decodes to the same value. The libraries are used the way you would call them in a service: through Marshal and Unmarshal, with the standard library compatible configuration for json-iterator.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"sort"
"testing"
gojson "github.com/goccy/go-json"
jsoniter "github.com/json-iterator/go"
)
func init() {
testing.Init()
flag.Set("test.benchtime", "150ms")
}
type Tag struct {
Name string `json:"name"`
Score float64 `json:"score"`
}
type Order struct {
ID int64 `json:"id"`
Customer string `json:"customer"`
Email string `json:"email"`
Paid bool `json:"paid"`
Total float64 `json:"total"`
Items []Item `json:"items"`
Tags []Tag `json:"tags"`
Meta map[string]string `json:"meta"`
}
type Item struct {
SKU string `json:"sku"`
Qty int `json:"qty"`
Price float64 `json:"price"`
}
func sample(items int) Order {
o := Order{ID: 90210, Customer: "Ada Lovelace", Email: "[email protected]", Paid: true, Total: 1234.5,
Tags: []Tag{{"vip", 0.93}, {"early", 0.71}}, Meta: map[string]string{"channel": "web", "coupon": "SPRING", "region": "eu"}}
for i := 0; i < items; i++ {
o.Items = append(o.Items, Item{SKU: fmt.Sprintf("SKU-%05d", i), Qty: i%5 + 1, Price: float64(i)*1.25 + 0.99})
}
return o
}
type codec struct {
name string
enc func(any) ([]byte, error)
dec func([]byte, any) error
}
func median(f func(b *testing.B)) (ns float64, bytesOp, allocs int64) {
const runs = 5
xs := make([]float64, 0, runs)
var r testing.BenchmarkResult
for i := 0; i < runs; i++ {
r = testing.Benchmark(f)
xs = append(xs, float64(r.T.Nanoseconds())/float64(r.N))
}
sort.Float64s(xs)
return xs[runs/2], r.AllocedBytesPerOp(), r.AllocsPerOp()
}
var sink []byte
func main() {
codecs := []codec{
{"encoding/json", json.Marshal, json.Unmarshal},
{"goccy/go-json", gojson.Marshal, gojson.Unmarshal},
{"json-iterator", jsoniter.ConfigCompatibleWithStandardLibrary.Marshal, jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal},
}
for _, items := range []int{1, 20, 500} {
o := sample(items)
ref, _ := json.Marshal(o)
fmt.Printf("order with %d items, %d bytes of JSON\n", items, len(ref))
fmt.Printf(" %-14s %12s %8s %7s | %12s %8s %7s | %s\n", "library", "encode ns", "B/op", "allocs", "decode ns", "B/op", "allocs", "same bytes")
for _, c := range codecs {
out, _ := c.enc(o)
same := bytes.Equal(out, ref)
if !same {
var a, b any
json.Unmarshal(out, &a)
json.Unmarshal(ref, &b)
same = fmt.Sprint(a) == fmt.Sprint(b)
}
en, eb, ea := median(func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
sink, _ = c.enc(o)
}
})
dn, db, da := median(func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var x Order
if err := c.dec(ref, &x); err != nil {
b.Fatal(err)
}
}
})
fmt.Printf(" %-14s %12.0f %8d %7d | %12.0f %8d %7d | %v\n", c.name, en, eb, ea, dn, db, da, same)
}
}
}order with 1 items, 266 bytes of JSON
library encode ns B/op allocs | decode ns B/op allocs | same bytes
encoding/json 890 624 9 | 3282 1000 28 | true
goccy/go-json 487 400 2 | 566 746 5 | true
json-iterator 1176 952 12 | 1040 744 24 | true
order with 20 items, 1057 bytes of JSON
library encode ns B/op allocs | decode ns B/op allocs | same bytes
encoding/json 3334 1488 9 | 12540 3416 52 | true
goccy/go-json 1929 1264 2 | 1518 1494 5 | true
json-iterator 3683 1818 12 | 3386 3161 48 | true
order with 500 items, 21637 bytes of JSON
library encode ns B/op allocs | decode ns B/op allocs | same bytes
encoding/json 55852 22145 9 | 239048 46424 536 | true
goccy/go-json 34402 21896 2 | 26810 23909 6 | true
json-iterator 62443 22933 12 | 53592 45932 532 | trueResults
Encoding and decoding, in nanoseconds per operation, bytes allocated and allocations:
| Payload | Library | Encode | B / allocs | Decode | B / allocs |
|---|---|---|---|---|---|
| 266 B | encoding/json | 890 | 624 / 9 | 3,282 | 1,000 / 28 |
| 266 B | goccy/go-json | 487 | 400 / 2 | 566 | 746 / 5 |
| 266 B | json-iterator | 1,176 | 952 / 12 | 1,040 | 744 / 24 |
| 1,057 B | encoding/json | 3,334 | 1,488 / 9 | 12,540 | 3,416 / 52 |
| 1,057 B | goccy/go-json | 1,929 | 1,264 / 2 | 1,518 | 1,494 / 5 |
| 1,057 B | json-iterator | 3,683 | 1,818 / 12 | 3,386 | 3,161 / 48 |
| 21,637 B | encoding/json | 55,852 | 22,145 / 9 | 239,048 | 46,424 / 536 |
| 21,637 B | goccy/go-json | 34,402 | 21,896 / 2 | 26,810 | 23,909 / 6 |
| 21,637 B | json-iterator | 62,443 | 22,933 / 12 | 53,592 | 45,932 / 532 |
Decoding is where the libraries differ
On the largest payload goccy decoded in 26.8 µs against 239.0 µs for the standard library, 8.9 times faster, while allocating 6 times and not 536. json-iterator was 4.5 times faster than the standard library (53.6 µs) with nearly the same number of allocations as it. The standard library's decoder allocates for each string it produces, and the list of 500 items grows by appending, which is where most of its 536 allocations come from. goccy decodes into the destination with fewer intermediate objects. The time follows the allocations: the standard library spent most of its 239 µs on allocating and on the garbage collector's share of that.
The gap held on the smallest message: 3,282 ns against 566 ns, 5.8 times. The absolute difference is only 2.7 µs a call, which matters for a service handling 50,000 messages a second (a saving of 135 ms of CPU every second) and not at all for one handling 50.
Encoding gains less
goccy encoded the large payload in 34.4 µs against 55.9 µs, 1.6 times faster, and the one item payload 1.8 times faster. Encoding in the standard library is already reasonable: 9 allocations to produce 21 KB. json-iterator was slower than the standard library at encoding on all three sizes (62.4 µs against 55.9 µs on the large one), so replacing the encoder with it would have made things worse. Speed claims for a library are claims for the operation that benchmark chose, so check yours.
Bytes allocated
Encoding allocated about the output size in all three, since the result has to exist. Decoding of the large payload allocated 23.9 KB for goccy and 46.4 KB for the standard library, and the allocation count is the bigger factor for the collector.
What you give up
Compatibility is a property of the payload. All three printed true for equivalence here, with ordinary fields, a map and nested slices. They differ at the edges: how omitempty treats certain types, how embedded structs with conflicting tags resolve, how invalid UTF-8 is handled, what happens with json.Number, the order of map keys, escaping of HTML characters, and the exact text of error messages. A service that returns errors to clients, or signs a canonical encoding, needs round trip and golden file tests before a switch.
Another dependency. goccy and json-iterator are community libraries, and json-iterator in particular has had issues with new Go versions in the past because of its use of unsafe and runtime internals. Pin the version, run your tests against each Go upgrade, and check the project's recent activity before you depend on it. The standard library is maintained with the language.
Different failure modes. A reflection heavy library that touches runtime internals can crash a process where the standard library would have returned an error. This is rare and also the kind of failure that takes down a service at 3 a.m.
The standard library is changing. Go 1.25 ships an experimental encoding/json/v2 behind GOEXPERIMENT=jsonv2, with a faster decoder and different semantics in places. It was not measured here. If you are choosing a JSON path for a service you will run for years, watch it before binding to a third party library.
When it is worth switching
- Profile first. If JSON encoding and decoding are under about 10 percent of CPU in a CPU profile, an 8 times faster decoder saves less than 9 percent (CPU profiles).
- Reduce the work. Decode only what you need, stream large arrays, reuse buffers, and avoid
interface{}targets (faster JSON). - Try goccy behind an interface in one hot path, with tests that compare its output against the standard library's on production samples.
- Keep the standard library on boundaries where behaviour must match exactly, such as webhook signatures and public error bodies.
- Re-measure after Go upgrades. The standard library gets faster, and the gap in this table is a snapshot.
Checklist
- Decoding is where alternatives gain the most (8.9 times for goccy on 21 KB).
- Encoding gains are modest (1.6 times) and json-iterator lost.
- Allocation counts explain the speed: 536 against 6 on the large decode.
- Profile before switching, and test equivalence on real payloads.
- Pin versions, and watch
encoding/json/v2.
The libraries are at goccy/go-json and json-iterator/go, and the standard package is described in the encoding/json documentation.


