Next.js Build Heap Out of Memory di CI: Fix Exit 137
Next.js build heap out of memory di CI, terus Killed exit 137? Log job asli dari runner 4 GiB, 10 kali kena kill, dan hasil pindahin type check ke job terpisah.

Saran paling umum buat error heap out of memory di Next.js CI itu naikin --max-old-space-size. Gue lakuin persis itu, dan empat hari berikutnya runner-nya malah ngebunuh sepuluh build dengan exit code 137, sembilan di antaranya di step yang sama.
Tersangka utamanya fase type check di dalam next build, yang nyorong seluruh container job lewat dari limit 4 GiB begitu heap dibolehin membesar. Fix yang gue pasang: jalanin tsc --noEmit sebagai job sendiri, skip type check di dalam build, dan pertahanin job terpisah itu sebagai gate wajib sebelum deploy. Di bawah ada dua signature log buat bedain dua kegagalan ini, config yang gue pakai, plus apa yang bisa dan gak bisa dibuktiin dari angka gue.
Heap Out of Memory dan Exit 137 Itu Dua Kegagalan Beda
Abort karena batas heap V8 dan OOM kill dari container sama-sama bikin next build merah, tapi yang mutusin beda dan fix-nya juga beda. Bedainnya cuma butuh sepuluh detik kalau lo udah tau tampang masing-masing log.
Setup gue: app Next.js 16.2.1 yang isinya banyak dashboard, di-build pakai next build --webpack sebagai static export 62 halaman. Runner-nya GitLab docker executor yang jalanin satu job per waktu, di host 24 vCPU dan RAM 16 GB yang dipakai bareng banyak container lain. Tiap job jalan di container node:20-alpine dengan limit 4 GiB, pakai pnpm 10.22.
Kegagalan pertama muncul waktu NODE_OPTIONS belum di-set sama sekali.
✓ Compiled successfully in 4.4min
Running TypeScript ...
[...] Mark-Compact (reduce) 1532.6 (1562.3) -> 1530.6 (1562.3) MB, 3740.44 / 0.00 ms
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
Next.js build worker exited with code: null and signal: SIGABRT
ELIFECYCLE Command failed with exit code 1.
ERROR: Job failed: exit code 1Di sini Node bunuh dirinya sendiri. Heap mentok di sekitar 1,5 GB terus V8 nyerah. Perhatiin exit code-nya 1, bukan 134. Child process-nya mati karena SIGABRT, tapi Next.js dan pnpm ngebungkusnya, jadi job cuma ngeliat failure biasa. Yang dicari di log itu baris FATAL ERROR dan SIGABRT.
Kegagalan kedua tampangnya begini.
✓ Compiled successfully in 3.2min
Running TypeScript ...
Killed
ERROR: Job failed: exit code 137Gak ada stack trace, gak ada pesan V8. Exit code 137 itu 128 ditambah 9, alias SIGKILL, dan pengirimnya kernel Linux. Di host runner, log kernelnya ngomong jelas.
memory: usage 4194304kB, limit 4194304kB, failcnt 71638
oom-kill:constraint=CONSTRAINT_MEMCG,task=node
Memory cgroup out of memory: Killed process (node) total-vm:68433732kB, anon-rss:2929496kBLimit-nya pas 4 GiB. Angka total-vm itu cuma V8 yang nge-reserve address space, jadi abaikan aja. Yang penting anon-rss, memori yang bener-bener dipegang proses node, dan nilainya hampir 2,9 GB.
| Batas heap V8 | OOM kill container | |
|---|---|---|
| Signature log | FATAL ERROR dan JavaScript heap out of memory | Cuma satu baris Killed |
| Signal | SIGABRT | SIGKILL |
| Exit code job | 1, lewat pnpm | 137 |
| Yang mutusin | Node | Cgroup kernel |
| Naikin max-old-space-size | Ngebantu | Bisa makin parah |
Apa yang Terjadi Waktu Heap Gue Naikin
Naikin heap nyelesaiin kegagalan pertama dan ngelahirin kegagalan kedua. Gue set NODE_OPTIONS ke --max-old-space-size=2560 di job build, dan commit yang sama langsung kena kill sekali dan lolos sekali, masing-masing di 314 detik dan 500 detik.
Itu jebakan exit 137. Retry yang hijau bikin keliatan udah beres, padahal belum.
Empat hari setelahnya, sepuluh job build kena kill dengan exit 137. Sembilan dari sepuluh log berhenti tepat setelah Running TypeScript ..., habis compile webpack yang sukses selama 2,7 sampai 3,6 menit. Memory cgroup kill pertama di log kernel host hari itu muncul sekitar enam menit setelah perubahan heap. Tiap kill ke proses node utama nunjukin dia megang 2,6 sampai 2,9 GB, kira-kira heap 2560 MB ditambah overhead.
Dugaan gue, heap yang lebih gede bikin V8 nahan lebih banyak sampah sebelum dibersihin. Fase TypeScript lalu nambah ukuran proses sampai mendekati batas heap, sementara sisa container masih dipakai build worker, dan limit 4 GiB kena duluan. Gue gak jalanin tes terkontrol, jadi anggap ini penjelasan paling masuk akal, bukan bukti. Tapi urutan waktunya susah diabaikan.
Baca Juga: Ngatasin Next.js Docker Build OOM Kill di CI
Next.js nge-print Collecting page data using 23 workers di host ini, kemungkinan besar karena dia ngeliat 24 CPU host, bukan limit container. Artikel di atas ngebahas cara ngebatasinnya pakai experimental.cpus.
Pindahin Type Check ke Job Sendiri
Lonjakan memorinya ada di fase type check, jadi fase itu gue cabut dari next build. Di build yang hijau, Finished TypeScript makan 2,9 sampai 3,6 menit, dan sembilan dari sepuluh kill jatuh di jendela itu.
Panduan memory usage Next.js versi 16 ngebahas opsi yang sama. Di sana disebut type checking bisa makan memori banyak di project besar, dan lo bisa set typescript.ignoreBuildErrors. Mereka juga ngingetin ini bisa ngeloloskan build yang rusak, dan nyaranin build baru dipromosikan setelah static analysis lolos di tempat lain. Peringatan itu justru jadi batasan desain utamanya, jadi job terpisahnya harus jadi gate yang wajib lolos.
Pertama, bikin build skip type check hanya kalau CI yang minta, supaya build lokal tetep ngecek tipe.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: process.env.SKIP_BUILD_TYPECHECK === "1",
},
};
export default nextConfig;Terus tambahin job-nya dan bikin deploy bergantung ke keduanya. Kalau lo set variable kayak SKIP_BUILD_TYPECHECK per environment, tulisan soal GitLab CI/CD Dynamic Variables per Environment ngebahas caranya.
build:
image: node:20-alpine
stage: build
variables:
NODE_OPTIONS: "--max-old-space-size=2560"
SKIP_BUILD_TYPECHECK: "1"
script:
- corepack enable
- pnpm install --frozen-lockfile
- pnpm build
interruptible: true
typecheck:
image: node:20-alpine
stage: build
variables:
NODE_OPTIONS: "--max-old-space-size=2560"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
interruptible: true
script:
- corepack enable
- pnpm install --frozen-lockfile
- pnpm exec tsc --noEmit
deploy:
stage: deploy
needs:
- build
- typecheckLog build sekarang nge-print Skipping validation of types, dan type error bakal bikin job typecheck yang gagal. Karena deploy nyebut dua job itu di needs, gak ada yang ke-deploy kalau salah satunya gagal. Ini juga bikin type error asli tetep keliatan. Kegagalan awal di runner shared lain, yang juga berhenti setelah compile, ternyata type error beneran dan bukan masalah memori, jadi gak semua build merah di step TypeScript itu OOM.
Angka Gue: Apa yang Terbukti dan Apa yang Enggak
Job build jadi lebih cepat, tapi pipeline-nya enggak. Ini run hijau di runner yang sama, dalam detik.
| Setup | Job build | Job type check |
|---|---|---|
| Type check di dalam build, 10 run | 488 sampai 592 | tidak ada |
| Type check dipisah, 7 run | 308 sampai 349 | 222 sampai 278 |
Runner-nya jalanin satu job per waktu, jadi dua job itu jalan berurutan. Satu pipeline makan 315 detik buat build dan 246 buat type check, sekitar 561 detik total, dibanding kira-kira 545 sebelumnya. Pemisahan ini gak ngehemat waktu. Dia cuma mindahin puncak memori ke proses sendiri.
Gue juga harus jujur soal satu confound. Sore 23 Oktober, gue naikin limit memori job di atas 4 GiB. Kill terakhir di log kernel jam 15:07, dan config runner berubah jam 15:14. Pemisahan ini sempat jalan sekali di branch jam 14:35 di bawah limit lama dan lolos, dengan build 381 detik. Tujuh build hijau setelahnya semua jalan sesudah limit dinaikin. Jadi gue gak bisa ngasih kredit buat pemisahan itu sendirian atas streak bersih ini.
Yang bisa gue bilang lebih sempit. Sembilan dari sepuluh kill terjadi di satu fase, fase itu sekarang gak jalan di build, dan build jadi lebih pendek kira-kira sepanjang fase itu. Limit memori yang lebih besar dan type check yang dipisah kemungkinan dua-duanya ngebantu.
Eksperimen Cache yang Gak Worth It
Sambil ngoprek file pipeline, gue coba cache, dan dua-duanya malah nambah waktu. Ini run tunggal, cache disimpan lokal di runner tanpa shared cache server, jadi anggap sebagai satu data point, bukan benchmark.
| Setup | Waktu job | Catatan |
|---|---|---|
| Tanpa cache | 500 detik | pnpm install 13,7 detik |
| .next/cache dan pnpm store | 1218 detik | Bikin cache makan 653 detik |
| .next/cache saja | 717 detik | Restore 56 detik, save 90 detik, compile 3,3 menit lawan 3,1 menit |
Arsip pnpm store isinya sekitar 58 ribu file dan nyimpennya lebih lama daripada install ulang, yang selesai dalam 10 sampai 33 detik di runner ini. Hasil .next/cache malah lebih ngagetin. Next.js nge-print peringatan bahwa build cache gak ketemu dan nyaranin dikonfigurasi, tapi di run gue, cache yang di-restore gak mempercepat compile. Dua-duanya gue hapus. Ukur dulu waktu save dan restore dibanding step yang mau lo skip sebelum mutusin pertahanin cache.
Baca Juga: Fix Beneran Buildx CI yang Flaky, Bukan Cuma Retry
Catatan soal Turbopack
Project ini di-build pakai --webpack. Panduan upgrade Next.js 16 bilang Turbopack sekarang bundler default dan next build --webpack buat opt-out. Batas heap dan type check terpisah gak bergantung ke bundler. Opsi kayak experimental.webpackMemoryOptimizations itu setting webpack, jadi cuma relevan kalau lo tetep di webpack. Gue gak ngukur memori di Turbopack, jadi gue gak mau klaim apa-apa soal itu.
Fix Mana yang Lo Butuhin?
Baca log-nya dulu sebelum ngubah apa pun.
- Kalau lo liat
FATAL ERRORdenganSIGABRT, lo kena batas heap V8. Naikin--max-old-space-size, tapi tetep jauh di bawah limit container. Heap 2560 MB di container 4 GiB kegedean begitu type check jalan. - Kalau lo liat
Killeddoang dan exit code 137, kernel yang ngakhirin build-nya. Kasih job lebih banyak memori atau kurangin yang jalan barengan di dalamnya, dan jangan naikin heap lagi. - Kalau log berhenti di step TypeScript dengan pesan
Type error, itu bukan memori. Benerin tipenya.
Ini satu project di satu runner dengan sampel kecil, jadi cek log job lo sendiri sebelum nyalin angka gue. Aturan keputusan di atas gak bergantung ke angka itu.
Referensi resmi di balik tulisan ini adalah panduan memory usage Next.js dan panduan upgrade Next.js 16.


