Engineering notes
A 2MB fetch response can take down your whole Next.js page
Why "Failed to set Next.js data cache" is followed by "RangeError: Maximum call stack size exceeded" and a 500, what actually causes it, and how to fix it without giving up caching.
If you are seeing
Failed to set Next.js data cache for <url>, items over 2MB can not be cachedRangeError: Maximum call stack size exceededError: failed to pipe response
Short version: if you pass next: { revalidate } to a fetch() whose response body is larger than 2MB, Next.js tries to serialize that body into its data cache, and on a large enough body the write path throws a RangeError. If that fetch happens inside generateMetadata, the exception escapes into the render stream and the request ends as a 500 — not as a page with missing metadata.
The log line about the 2MB limit reads like a warning you can ignore. It isn't, when it's followed by a stack overflow.
What the logs look like
Failed to set fetch cache https://cdn.example.com/large.png
[Error: Failed to set Next.js data cache for https://cdn.example.com/large.png,
items over 2MB can not be cached (4688978 bytes)]
⨯ [RangeError: Maximum call stack size exceeded] { digest: '1146001719' }
⨯ [Error: failed to pipe response] {
page: '/share/xxx',
[cause]: [RangeError: Maximum call stack size exceeded]
}
GET /share/xxx 500The code that causes it
This is the shape to look for — a cached fetch of a whole binary file, usually to read something small out of it:
// generateMetadata needs the image's width/height for the OG tags
const res = await fetch(imageUrl, { next: { revalidate: 3600 } });
const buffer = Buffer.from(await res.arrayBuffer());
const { width, height } = await sharp(buffer).metadata();It works in development and it works in tests, because test fixtures are small. It fails on exactly the files you care most about — high-resolution output, user uploads from modern phones, anything a real user actually produced.
Why the failure correlates with quality
This is what makes it hard to spot. Small images stay under the cache limit and behave perfectly. Large ones 500. So the symptom is not "the page is broken" — it's "the page is broken for our best content," which looks random until you correlate it with file size.
The fix
Two changes. The first removes the cause, the second stops the same class of bug from taking the page down again.
- Don't download a whole file to read its header. Image dimensions for PNG, JPEG and WebP live in the first few KB, so a Range request gets them without ever approaching the cache limit.
- Never let generateMetadata throw. Anything doing I/O there should degrade to sensible defaults — missing OG dimensions cost you very little, a 500 costs you the page.
const HEADER_BYTES = 96 * 1024;
async function probeDimensions(url: string) {
try {
const res = await fetch(url, {
headers: { Range: `bytes=0-${HEADER_BYTES - 1}` },
// No `next: { revalidate }` — that is what blows up on large bodies.
cache: "no-store",
});
if (!res.ok) return null;
const meta = await sharp(Buffer.from(await res.arrayBuffer())).metadata();
return meta.width && meta.height ? meta : null;
} catch {
return null; // degrade, never throw out of generateMetadata
}
}If the origin ignores the Range header and returns the whole file, this still works — dropping the cache option is what removes the crash, and the Range request just makes it cheap. 4.7MB became 96KB in our case.
How to check whether you have this
Request the affected page several times in a row and check two things: whether the response actually terminates, and whether the OG tags are present. A truncated body with zero og: tags is the signature — the shell renders, the stream dies before metadata is flushed.
curl -s http://localhost:3000/your-page | grep -c 'og:'Zero on a page that should have Open Graph tags, while other pages return a healthy count, means metadata generation is failing rather than misconfigured.