Skip to content

fix(provider-utils): chunk array when converting to base64 to avoid excessive memory usage - #13581

Open
danielpoonwj wants to merge 3 commits into
vercel:mainfrom
danielpoonwj:fix-provider-utils-base64
Open

danielpoonwj wants to merge 3 commits into
vercel:mainfrom
danielpoonwj:fix-provider-utils-base64

Conversation

@danielpoonwj

@danielpoonwj danielpoonwj commented Mar 18, 2026 •

Copy link
Copy Markdown

Background

With a recent issue disabling passing files as base64 encoded data (#13103), we switched to passing a Buffer and let the library handle base64 encoding.

However we noticed very high memory usage with this method.

Summary

Problem

The previous implementation built the latin1 string character by character in a loop, creating a new string allocation on every iteration. For large arrays this causes O(n²) memory pressure due to repeated string concatenation.

Solution

Replace with a chunked approach (4096 bytes per chunk, matching js-base64) that converts each chunk in a single String.fromCodePoint call and collects results into an array, joining once at the end before passing to btoa. This avoids both per-byte allocations and the O(n²) string concatenation cost.

Manual Verification

Tests

Added uint8-utils.test.ts covering basic encoding/decoding. The suite passes against both the old and new implementations.

Checklist

  • Tests have been added / updated (for bug fixes / features)
  • Documentation has been added / updated (for bug fixes / features)
  • A patch changeset for relevant packages has been added (for bug fixes / features - run pnpm changeset in the project root)
  • I have reviewed this pull request (self-review)

Future Work

Related Issues

@tigent tigent Bot added ai/provider related to a provider package. Must be assigned together with at least one `provider/*` label bug Something isn't working as documented maintenance CI, internal documentation, automations, etc labels Mar 18, 2026
@aayush-kapoor

Copy link
Copy Markdown
Collaborator

will the upstream fix for #13103 fix your issue? meaning if that is patched, will you still need this fix?

@danielpoonwj

Copy link
Copy Markdown
Author

will the upstream fix for #13103 fix your issue? meaning if that is patched, will you still need this fix?

hi @aayush-kapoor, this is actually independent.

the patch now allows us to send in data pre-converted to base64 to avoid this issue, but if there are any other implementations that send in Buffers for the SDK to encode in base64, this fix is still relevant

@lgrammel

Copy link
Copy Markdown
Collaborator

This looks potentially interesting. Do you have any reproduction of the memory issues and proof that this reduces them? Why did you choose 4096 specifically?

}

return btoa(latin1string);
return btoa(chunks.join(''));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the max arg limit for String.fromCodePoint? ie why do we need the loop at all

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String.fromCodePoint(...array) spreads each byte as a separate argument, which blows the call stack well below attachment sizes. A few checks we did for our ~18MB PDF test file:

4096      ok
65536     ok
131072    RangeError: Maximum call stack size exceeded
whole file (18.7M)  RangeError

@janvennemann

Copy link
Copy Markdown

We hit this exact issue in production with a Node worker OOM'd encoding ~18 MB PDF attachments before handing them to the SDK. We solved it by using the native Buffer conversion and directly passing that into the AI SDK to bypass the loop conversion. Here are some benchmarks to see the impact.

Repro (Node v24.16, arm64, real 17.9 MB PDF), peak RSS via /usr/bin/time -l. All three produce byte-identical base64 (SHA-256 verified):

Strategy Peak RSS Encode time
old (per-byte concat, current) 787 MB 802 ms
chunked (this PR) 236 MB 464 ms
Buffer.from(a).toString("base64") (ref) 106 MB 5 ms

@safaiyeh

safaiyeh commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Filed #20149 with an independent production confirmation of this PR's numbers: one 14MB PDF file part OOM-killed a 2GB Fargate container in ~40s through the current per-byte encoder (+606MB / 392ms for the encode alone; 1,065MB peak through the full openai provider request path with two PDFs). It also carries the answers already posted here — the spread's ~131k-argument RangeError ceiling and the chunk-size rationale — so hopefully it helps this get a second look. Linking per CONTRIBUTING so the issue and fix credit connect.

wasim-builds pushed a commit to wasim-builds/ai that referenced this pull request Sep 2, 2026
Per-byte String.fromCodePoint concatenation creates a cons-string rope
that V8 cannot collect until btoa flattens it, causing OOM on large
arrays. Using chunked encoding with String.fromCodePoint.apply reduces
peak memory usage significantly.

Fixes vercel#20149, vercel#13581
wasim-builds pushed a commit to wasim-builds/ai that referenced this pull request Sep 5, 2026
Per-byte String.fromCodePoint concatenation creates a cons-string rope
that V8 cannot collect until btoa flattens it, causing OOM on large
arrays. Using chunked encoding with String.fromCodePoint.apply reduces
peak memory usage significantly.

Fixes vercel#20149, vercel#13581

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai/provider related to a provider package. Must be assigned together with at least one `provider/*` label bug Something isn't working as documented maintenance CI, internal documentation, automations, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants