nextjs - 💡(How to fix) Fix Memory leak in Next.js SSR under bun --bun next start [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#92778Fetched 2026-04-16 06:34:38
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
subscribed ×2issue_type_added ×1

Error Message

error: Failed to run "next" due to signal SIGTRAP error: script "start" was terminated by signal SIGTRAP (Trace or breakpoint trap)

Code Example

mkdir bun-nextjs-memleak && cd bun-nextjs-memleak
bun init -y
bun add next@16.2.3 react@19.2.5 react-dom@19.2.5

---

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactCompiler: true,
  images: { unoptimized: true },
};

export default nextConfig;

---

export const metadata = { title: "Memleak repro" };

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <html><body>{children}</body></html>;
}

---

// Every request fetches unique JSON (~5-50 KB) from the mock API.
// No React cache() wrapper — keeps the repro minimal.

interface Props {
  params: Promise<{ brandSlug: string; slug: string }>;
}

export default async function ProductPage({ params }: Props) {
  const { brandSlug, slug } = await params;

  const res = await fetch(
    `http://localhost:4000/products/${brandSlug}/${slug}`,
    { cache: "no-store" }
  );
  const product = await res.json(); // ~5-50 KB per product

  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
      <ul>
        {product.specs?.map((s: { label: string; value: string }, i: number) => (
          <li key={i}>{s.label}: {s.value}</li>
        ))}
      </ul>
    </div>
  );
}

---

// Run with: bun run mock-api.ts
// Returns deterministic but unique ~10-30 KB JSON per {brandSlug, slug} combo

const generateProduct = (brandSlug: string, slug: string) => {
  const id = `${brandSlug}-${slug}`;
  return {
    id,
    title: `Product ${slug} by ${brandSlug}`,
    description: "A".repeat(5000 + (slug.charCodeAt(0) % 20) * 1000), // 5-25 KB string
    price: Math.round(Math.random() * 100000),
    brand: { name: brandSlug, slug: brandSlug },
    images: Array.from({ length: 5 }, (_, i) => `https://example.com/${id}/img${i}.jpg`),
    specs: Array.from({ length: 15 }, (_, i) => ({
      label: `Spec ${i}`,
      value: `Value ${i} for ${id}`,
    })),
    variants: Array.from({ length: 3 }, (_, i) => ({
      id: `${id}-v${i}`,
      name: `Variant ${i}`,
      price: Math.round(Math.random() * 50000),
      isActive: true,
      properties: Array.from({ length: 5 }, (_, j) => ({
        name: `Prop ${j}`,
        value: `Val ${j}`,
      })),
    })),
  };
};

Bun.serve({
  port: 4000,
  fetch(req) {
    const url = new URL(req.url);
    const parts = url.pathname.split("/").filter(Boolean);
    // /products/:brandSlug/:slug
    if (parts[0] === "products" && parts.length === 3) {
      return Response.json(generateProduct(parts[1], parts[2]));
    }
    return new Response("Not Found", { status: 404 });
  },
});

console.log("Mock API running on http://localhost:4000");

---

// Run with: bun run load-test.ts
// Generates 25,000 unique {brandSlug, slug} pairs and hits the Next.js server

const TOTAL = 25_000;
const BATCH_SIZE = 100;
const BASE_URL = "http://localhost:3000";

// Generate unique slugs
const products = Array.from({ length: TOTAL }, (_, i) => ({
  brandSlug: `brand-${Math.floor(i / 100)}`,
  slug: `product-${i}`,
}));

const startTime = Date.now();
let totalFetched = 0;
let errors = 0;

for (let i = 0; i < products.length; i += BATCH_SIZE) {
  const batch = products.slice(i, i + BATCH_SIZE);
  const batchNum = Math.floor(i / BATCH_SIZE) + 1;

  const results = await Promise.allSettled(
    batch.map((p) =>
      fetch(`${BASE_URL}/products/${p.brandSlug}/${p.slug}`)
        .then((r) => {
          if (!r.ok) throw new Error(`HTTP ${r.status}`);
          return r.text();
        })
    )
  );

  const fulfilled = results.filter((r) => r.status === "fulfilled").length;
  const rejected = results.filter((r) => r.status === "rejected").length;
  totalFetched += fulfilled;
  errors += rejected;

  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
  console.log(
    `[batch ${batchNum}/${Math.ceil(TOTAL / BATCH_SIZE)}] ` +
    `OK: ${fulfilled}, FAIL: ${rejected}, ` +
    `Total: ${totalFetched}/${TOTAL}, Errors: ${errors}, ` +
    `Elapsed: ${elapsed}s`
  );

  // If too many failures in a row, the server is likely dead
  if (rejected === BATCH_SIZE) {
    console.error("Entire batch failed — server is likely unresponsive. Stopping.");
    break;
  }
}

console.log(`\nDone. ${totalFetched} OK, ${errors} errors out of ${TOTAL} total.`);

---

# Terminal 1 — mock API
bun run mock-api.ts

# Terminal 2 — build and start Next.js with Bun runtime
bun --bun next build
bun --bun next start -H 127.0.0.1 -p 3000

# Terminal 3 — fire the load test
bun run load-test.ts

---

# Watch the Next.js server process RSS in real-time
while true; do
  PID=$(pgrep -f "next start" | head -1)
  [ -z "$PID" ] && echo "Process dead" && break
  RSS=$(ps -o rss= -p "$PID" | awk '{printf "%.0f", $1/1024}')
  HEAP=$(cat /proc/$PID/status 2>/dev/null | grep VmRSS | awk '{print $2/1024 "MB"}')
  echo "$(date +%H:%M:%S) PID=$PID RSS=${RSS}MB"
  sleep 2
done

---

Possible memory leak — GC freed less than 10%
memUsageBefore: 786130199
memUsageAfter: 786130199
Possible memory leak — GC freed less than 10%
memUsageBefore: 786130199
memUsageAfter: 786130199
[repeats ~8 times]

---

error: Failed to run "next" due to signal SIGTRAP
error: script "start" was terminated by signal SIGTRAP (Trace or breakpoint trap)

---

PID       %CPU  %MEM  VSZ (KB)      RSS (KB)     ELAPSED  THREADS
1564416   20.8  24.1  95,522,648    7,774,100    26:28    35

---

VmPeak:   134,353,996 kB   (~128 GB peak virtual — likely mmap'd regions)
VmSize:    95,522,648 kB   (~91 GB current virtual)
VmRSS:     7,774,100 kB   (~7.4 GB resident)
RssAnon:   7,741,472 kB   (~7.4 GB anonymous — heap + stacks)
RssFile:      32,628 kB   (~32 MB file-backed pages)
VmData:   91,625,128 kB   (~87 GB data segment)
VmSwap:      71,228 kB   (~70 MB swapped out)
Threads:  35

---

Rss:             7,774,100 kB
Pss:             7,758,280 kB    (nearly all private — no sharing)
Pss_Dirty:       7,741,472 kB    (almost all dirty anonymous)
Pss_Anon:        7,741,472 kB
Pss_File:           16,808 kB
Private_Dirty:   7,741,472 kB
Shared_Clean:       27,564 kB
Swap:               71,228 kB

---

Linux 6.19.9-zen1-1-zen #1 ZEN SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
Arch Linux, 32 GB RAM
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

https://github.com/Playys228/nextjs-bun-mem-leaks

To Reproduce

Minimal reproduction (self-contained)

Note to maintainers: A standalone repo is provided below — no external backend needed. The mock API server is included.

1. Create the project

mkdir bun-nextjs-memleak && cd bun-nextjs-memleak
bun init -y
bun add [email protected] [email protected] [email protected]

2. next.config.ts

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactCompiler: true,
  images: { unoptimized: true },
};

export default nextConfig;

3. app/layout.tsx

export const metadata = { title: "Memleak repro" };

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <html><body>{children}</body></html>;
}

4. app/products/[brandSlug]/[slug]/page.tsx — Server Component with per-request fetch

// Every request fetches unique JSON (~5-50 KB) from the mock API.
// No React cache() wrapper — keeps the repro minimal.

interface Props {
  params: Promise<{ brandSlug: string; slug: string }>;
}

export default async function ProductPage({ params }: Props) {
  const { brandSlug, slug } = await params;

  const res = await fetch(
    `http://localhost:4000/products/${brandSlug}/${slug}`,
    { cache: "no-store" }
  );
  const product = await res.json(); // ~5-50 KB per product

  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
      <ul>
        {product.specs?.map((s: { label: string; value: string }, i: number) => (
          <li key={i}>{s.label}: {s.value}</li>
        ))}
      </ul>
    </div>
  );
}

5. mock-api.ts — Lightweight API server that returns unique JSON per URL

// Run with: bun run mock-api.ts
// Returns deterministic but unique ~10-30 KB JSON per {brandSlug, slug} combo

const generateProduct = (brandSlug: string, slug: string) => {
  const id = `${brandSlug}-${slug}`;
  return {
    id,
    title: `Product ${slug} by ${brandSlug}`,
    description: "A".repeat(5000 + (slug.charCodeAt(0) % 20) * 1000), // 5-25 KB string
    price: Math.round(Math.random() * 100000),
    brand: { name: brandSlug, slug: brandSlug },
    images: Array.from({ length: 5 }, (_, i) => `https://example.com/${id}/img${i}.jpg`),
    specs: Array.from({ length: 15 }, (_, i) => ({
      label: `Spec ${i}`,
      value: `Value ${i} for ${id}`,
    })),
    variants: Array.from({ length: 3 }, (_, i) => ({
      id: `${id}-v${i}`,
      name: `Variant ${i}`,
      price: Math.round(Math.random() * 50000),
      isActive: true,
      properties: Array.from({ length: 5 }, (_, j) => ({
        name: `Prop ${j}`,
        value: `Val ${j}`,
      })),
    })),
  };
};

Bun.serve({
  port: 4000,
  fetch(req) {
    const url = new URL(req.url);
    const parts = url.pathname.split("/").filter(Boolean);
    // /products/:brandSlug/:slug
    if (parts[0] === "products" && parts.length === 3) {
      return Response.json(generateProduct(parts[1], parts[2]));
    }
    return new Response("Not Found", { status: 404 });
  },
});

console.log("Mock API running on http://localhost:4000");

6. load-test.ts — Drives 25,000 unique-URL requests in batches of 100

// Run with: bun run load-test.ts
// Generates 25,000 unique {brandSlug, slug} pairs and hits the Next.js server

const TOTAL = 25_000;
const BATCH_SIZE = 100;
const BASE_URL = "http://localhost:3000";

// Generate unique slugs
const products = Array.from({ length: TOTAL }, (_, i) => ({
  brandSlug: `brand-${Math.floor(i / 100)}`,
  slug: `product-${i}`,
}));

const startTime = Date.now();
let totalFetched = 0;
let errors = 0;

for (let i = 0; i < products.length; i += BATCH_SIZE) {
  const batch = products.slice(i, i + BATCH_SIZE);
  const batchNum = Math.floor(i / BATCH_SIZE) + 1;

  const results = await Promise.allSettled(
    batch.map((p) =>
      fetch(`${BASE_URL}/products/${p.brandSlug}/${p.slug}`)
        .then((r) => {
          if (!r.ok) throw new Error(`HTTP ${r.status}`);
          return r.text();
        })
    )
  );

  const fulfilled = results.filter((r) => r.status === "fulfilled").length;
  const rejected = results.filter((r) => r.status === "rejected").length;
  totalFetched += fulfilled;
  errors += rejected;

  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
  console.log(
    `[batch ${batchNum}/${Math.ceil(TOTAL / BATCH_SIZE)}] ` +
    `OK: ${fulfilled}, FAIL: ${rejected}, ` +
    `Total: ${totalFetched}/${TOTAL}, Errors: ${errors}, ` +
    `Elapsed: ${elapsed}s`
  );

  // If too many failures in a row, the server is likely dead
  if (rejected === BATCH_SIZE) {
    console.error("Entire batch failed — server is likely unresponsive. Stopping.");
    break;
  }
}

console.log(`\nDone. ${totalFetched} OK, ${errors} errors out of ${TOTAL} total.`);

7. Build and run

# Terminal 1 — mock API
bun run mock-api.ts

# Terminal 2 — build and start Next.js with Bun runtime
bun --bun next build
bun --bun next start -H 127.0.0.1 -p 3000

# Terminal 3 — fire the load test
bun run load-test.ts

8. Monitor memory (optional, in a 4th terminal)

# Watch the Next.js server process RSS in real-time
while true; do
  PID=$(pgrep -f "next start" | head -1)
  [ -z "$PID" ] && echo "Process dead" && break
  RSS=$(ps -o rss= -p "$PID" | awk '{printf "%.0f", $1/1024}')
  HEAP=$(cat /proc/$PID/status 2>/dev/null | grep VmRSS | awk '{print $2/1024 "MB"}')
  echo "$(date +%H:%M:%S) PID=$PID RSS=${RSS}MB"
  sleep 2
done

Current vs. Expected behavior

fter ~100-700 requests (1-7 batches):

  1. RSS jumps to ~377 MB, heap to ~204 MB after the first batch
  2. Bun.gc(true) frees 0 bytes — heap stays frozen at exactly 786,130,199 bytes
  3. Repeated Bun.gc(true) calls continue to free 0 bytes:
Possible memory leak — GC freed less than 10%
memUsageBefore: 786130199
memUsageAfter: 786130199
Possible memory leak — GC freed less than 10%
memUsageBefore: 786130199
memUsageAfter: 786130199
[repeats ~8 times]
  1. Server stops accepting connections — load test gets ECONNRESET on all requests
  2. Process is killed with SIGTRAP:
error: Failed to run "next" due to signal SIGTRAP
error: script "start" was terminated by signal SIGTRAP (Trace or breakpoint trap)

Next.js's binary (node_modules/.bin/next) has #!/usr/bin/env node as its shebang, so even under bun run --bun, the actual server runs as a Node.js process. This means the SIGTRAP crash and GC failure reported above happen when Bun does successfully inject its runtime (on earlier versions / different configs), not in this particular Node fallback mode.

Node.js process snapshot (26 minutes uptime, after load test)

PID       %CPU  %MEM  VSZ (KB)      RSS (KB)     ELAPSED  THREADS
1564416   20.8  24.1  95,522,648    7,774,100    26:28    35

RSS: 7.6 GB (24.1% of 32 GB system RAM) — under Node.js, the process survives but still consumes enormous memory.

/proc/<pid>/status memory breakdown

VmPeak:   134,353,996 kB   (~128 GB peak virtual — likely mmap'd regions)
VmSize:    95,522,648 kB   (~91 GB current virtual)
VmRSS:     7,774,100 kB   (~7.4 GB resident)
RssAnon:   7,741,472 kB   (~7.4 GB anonymous — heap + stacks)
RssFile:      32,628 kB   (~32 MB file-backed pages)
VmData:   91,625,128 kB   (~87 GB data segment)
VmSwap:      71,228 kB   (~70 MB swapped out)
Threads:  35

/proc/<pid>/smaps_rollup (aggregated memory map)

Rss:             7,774,100 kB
Pss:             7,758,280 kB    (nearly all private — no sharing)
Pss_Dirty:       7,741,472 kB    (almost all dirty anonymous)
Pss_Anon:        7,741,472 kB
Pss_File:           16,808 kB
Private_Dirty:   7,741,472 kB
Shared_Clean:       27,564 kB
Swap:               71,228 kB

Provide environment information

Linux 6.19.9-zen1-1-zen #1 ZEN SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
Arch Linux, 32 GB RAM

Which area(s) are affected? (Select all that apply)

cacheComponents

Which stage(s) are affected? (Select all that apply)

next build (local), next start (local)

Additional context

  • The issue does not occur with node next start — only with bun --bun next start
  • Note: bun run --bun next start may silently fall back to Node.js due to Next.js's #!/usr/bin/env node shebang — verify with pstree or /proc/<pid>/exe which runtime is actually running
  • Adding cacheMaxMemorySize: 25 * 1024 * 1024 to next.config.ts does not help
  • The issue is triggered specifically by many unique URLs — a smaller number of URLs with repeated cache hits does not exhibit this behavior
  • The production app uses bun run --bun --max-old-space-size=2048 next start which also crashes
  • The crash is deterministic and happens within the first 1000 requests
  • Under Node.js fallback, the process survives but still reaches 7.6 GB RSS (OOM score 765/1000) — the load pattern is inherently memory-heavy, but V8 copes while JSC does not

extent analysis

TL;DR

The issue is likely caused by a memory leak in the Next.js application when using the Bun runtime, and a potential workaround is to adjust the max-old-space-size flag or optimize the application's memory usage.

Guidance

  • The memory leak appears to be triggered by the large number of unique URLs being requested, which may be causing the cache to grow indefinitely. Consider implementing a cache eviction policy or optimizing the cache to reduce its memory footprint.
  • The fact that the issue does not occur with node next start suggests that the problem is specific to the Bun runtime. Try adjusting the max-old-space-size flag to a larger value to see if it alleviates the issue.
  • The production app's use of bun run --bun --max-old-space-size=2048 next start may be contributing to the crash. Consider reducing the value of max-old-space-size or optimizing the application's memory usage to prevent the crash.
  • To mitigate the issue, consider adding error handling to the load-test.ts script to detect when the server is no longer responding and restart it as needed.

Example

No code example is provided as the issue is complex and requires a deeper understanding of the application's architecture and the Bun runtime.

Notes

The issue is likely specific to the combination of Next.js, Bun, and the unique load pattern. Further investigation is needed to determine the root cause of the memory leak and to develop a comprehensive solution.

Recommendation

Apply workaround: Adjust the max-old-space-size flag to a larger value or optimize the application's memory usage to prevent the crash. This may require significant changes to the application's architecture and caching strategy.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING