Skip to content

O(n²) memory allocation when marshaling large slices via Marshal() #4315

@Tonyqu123

Description

@Tonyqu123

Summary

internal/apijson.Marshal allocates O(n²) memory and runs in O(n²) time when given a large slice. For a 30,000-element []string, a single Marshal call allocates ~6.2 GB and takes ~4 seconds.

Root cause

newArrayTypeEncoder builds the array by repeatedly calling sjson.SetRawBytes(json, "-1", item) inside a loop:

https://github.com/cloudflare/cloudflare-go/blob/main/internal/apijson/encoder.go#L182-L202

sjson is designed for sparse field updates on existing JSON; appending element-by-element forces it to re-parse and reallocate the entire buffer per call.

Reproduction

Benchmark on Marshal([]string{...}) with varying N (Apple M4 Pro, go1.26):

N ns/op B/op allocs
100 137,896 92 KB 436
1,000 7,061,292 6.9 MB 4,071
5,000 118,919,562 173 MB 20,315
10,000 437,616,980 692 MB 40,996
30,000 4,090,896,583 6,178 MB 128,698

1k → 30k (30x): ns/op grows 580x, B/op grows 893x — clear O(n²).

Real-world impact

We hit this calling Rules.Lists.Items.Update with ~30,000 IPs to sync an internal allow-list to a Cloudflare account-level IP list. A single Update call allocated ~6.2 GB and OOM-killed our controller (1 GiB limit), forcing us to bump the limit to 2 GiB.

Fix

Replace the sjson loop with a single-pass bytes.Buffer concatenation, restoring O(n). After the fix, 30,000 elements goes from 4,091 ms / 6.2 GB to 3.1 ms / 2.0 MB (1320x faster, 3,070x less memory). Output is byte-identical to the current implementation.

PR: #4316

Notes

The same sjson.SetRawBytes pattern is used by encodeMapEntries and newStructTypeEncoder — also technically O(n²), but typically don't hit the pathological case because struct field counts and map sizes stay small. The PR scopes the fix to the array encoder.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions