Skip to content

docs build: next build holds ~7.6 GB in ONE process — the docs deploy's next memory ceiling, and no in-repo knob bounds it #12683

Closed as not planned

Description

@os-zhuang

Found while fixing #12677 (spec's DTS pass was declaring a 12 GB heap ceiling inside the build container). That fix lands separately and is not affected by this card; this records the next thing that will take the docs deploy down, because it is now the largest single consumer in the pipeline and nothing in this repo bounds it.

Measured

pnpm turbo run build --filter=@objectstack/docs is two sequential tasks — @objectstack/spec#build then @objectstack/docs#build (the docs app's only workspace dependency is spec). Measured as peak anonymous RSS of the whole process tree, inside a cgroup capped at the stated size and a mount namespace whose /proc/meminfo reports the same figure, so tools that size themselves from reported memory see a real container:

phase peak dominant process
@objectstack/spec#build, after the #12677 fix 5654 MB tsup DTS worker
@objectstack/docs#build ~7.6 GB a single next-build (v16.3.1) process at 7592 MB

The whole pipeline in an 8192 MB container: exit 137, oom_kill=1, peak 8143 MB, killed in the docs phase. In a 10240 MB container: completes, 2/2 tasks, 5m02s, peak 8135 MB.

The turbopack worker-pool children are not the problem — they measure 100–430 MB each. One process holds everything.

Why the existing controls do not bound it

Both knobs already in apps/docs/next.config.mjs and apps/docs/package.json were put there for earlier OOMs, and neither reaches this:

  • NODE_OPTIONS='--max-old-space-size=4096' bounds V8's old space only. Next 16 builds with Turbopack, which is Rust — its allocations sit outside V8 entirely. Measured: the process reaches 7592 MB while nominally capped at 4096.
  • experimental.cpus: 2 bounds the static-generation worker fan-out. Measured above: the workers are small; capping them cannot move a 7.6 GB main process.

And the demand is fixed, not adaptive — it does not shrink when given less. Under smaller containers the docs build grows until it is killed rather than sizing down:

container result
8192 MB exit 137, oom_kill=1, limit_failcnt=11073
6144 MB exit 137, oom_kill=1, killed at 75s
4096 MB exit 137, oom_kill=1, killed at 34s

Next 16.3.1 exposes no memory-limit option: its config schema carries no memoryLimit for turbopack (the only match anywhere in its dist/ is TURBO_TRACE_DEFAULT_MEMORY_LIMIT, which is tracing, not the build).

Why this is not already an outage

The docs phase has been building successfully on Vercel at roughly this size for weeks — the experimental.cpus: 2 comment records a previous OOM fought at this same seam. So the Vercel build container's real envelope is at or above ~8 GB usable. What broke in #12677 was the other phase crossing the line, not this one. This card is the standing risk, not a live failure.

Why it is a real risk anyway

The margin is thin and only one side of it is under this repo's control. The docs site prerenders 400+ MDX pages; that count grows with the documentation. When this phase crosses, it produces exactly the #12677 signature — exit 137, no diagnostic, every production deploy failing — and unlike #12677 there is no in-repo number to lower.

Options, none of them free

  1. Raise the Vercel build container (dashboard/maintainer action, not versioned in apps/docs/vercel.json). Buys headroom, changes no code, costs money.
  2. Build the docs app with webpack instead of Turbopack. Different memory profile, but it is a bundler change for a 400-page site — an architecture decision, and next.config.mjs documents that Vercel and CI both currently run Turbopack.
  3. Shard the prerender so no single process holds the whole page set. Largest change; also the only one that scales with the page count.

Not filed with a fix attached on purpose: (1) is a maintainer decision and (2)/(3) are architecture decisions, none of which belongs inside the #12677 memory fix.

Reproducing

Peak was read from a cgroup-v1 memory.stat total_rss sampler with per-process attribution at each new peak; the harness was validated against a positive control (600 MB allocation under an 8 GB cap completes) and a negative control (2 GB allocation under a 1 GB cap reproduces exit 137 with oom_kill=1). The container emulation is two halves — the cgroup cap and a faked /proc/meminfo in a private mount namespace; a cap alone is not a container, because it bounds what a process may use without changing what it believes it has.

Refs: #12677 · epic #12243

Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    ContributorAuthor

    ① 项目长远合理性:C(预渲染分片)是唯一随页面数增长而扩展的形状;A(调大构建机)只买间距不改曲线;B(换 webpack)换了打包器也没换增长曲线,还引入一次 400 页站点的打包器迁移。
    ② 实际业务拉动:零当前故障——该阶段数周来一直构建成功;但实测间距 ~0.5 GB(8192 容器内峰值 8135 MB),且每新增一页文档都在消耗它,一次大型文档落地即可能触线。⚠️ 检测延迟有实测:#12333 这次断流两天才被发现。
    ③ 防 AI 犯错:触线时的形态 = exit 137 无诊断、全部生产部署静默失败——与 #12677 同签名,但这次仓库里没有可降的数字;卡内已刻好指路(「再见 137 先查本卡」),响亮性只能靠记录补。
    ④ 创业阶段不扩散:A 零代码零新义务;B/C 都是架构级手术——默认不为尚未发生的故障动刀。
    推荐:A(维护者面板动作,分钟级)现在做,C 记录为页面数真正增长后的既定方向;B 排除。备选 D(记录后 defer,什么都不做):真实代价 = 某天 docs 生产部署再次全停,以实测两天的检测延迟计,停机窗口可能又是「悄悄的两天」,修复仍是去面板点 A。
    一行置信缺口:本分析看不见 Vercel 的真实容器规格与价格档(标准/增强构建机的内存与月费),也看不见未来文档页的增长速率。


    Generated by Claude Code

  2. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Aug 27, 2026
  3. os-litant commented on Aug 27, 2026

    @os-litant
    Contributor

    Deferred by ruling — not adjudicated this round. Director seat, batch #1, 2026-08-27 (session session_01MnijPVVDakqK2J335JoJtq). Maintainer's one-line ruling on the batch, verbatim: 「12683 本轮不处理,其他同意」.

    Card stays parked in the decision inbox (needs-user-decision, labels untouched) and rolls into the next Director batch. No option is chosen; the standing-risk analysis and the option set (A raise the Vercel build container / B webpack / C shard the prerender / D defer) remain as presented. Related but independent: #12698 (pm:awaiting-maintainer, P1 — dashboard Redeploy + Ignored Build Step fix) was NOT deferred by this ruling and remains on the 待您 ledger.


    Generated by Claude Code

  4. hotlong commented on Aug 28, 2026

    @hotlong
    Contributor

    Adjudicated by events: option A was taken, and it worked

    This card was deferred on 2026-08-27 («12683 本轮不处理») on the reading that there was «零当前故障». That premise expired the same day: production docs deploys began failing with exit 137 / errorCode: "out_of_memory", the site served a stale deployment for two days, and the team build queue reached 92 deployments with the oldest 34 hours old (#12711).

    The maintainer has since moved the docs project to an Enhanced build machine (8 vCPU / 16384 MB, project-scoped; the team's other six projects stay on standard). That is this card's option A.

    Standard 4 vCPU / 8 GB Enhanced 8 vCPU / 16 GB
    outcome out_of_memory, then BUILD_EXCEEDED_MAXIMUM_TIME READY
    wall time 46–50 min 1.6–3.7 min
    cost per build 184–200 CPU-min ≈ $0.64–0.70, wasted 30 CPU-min ≈ $0.105

    Note for ④ («A 零代码零新义务»): it also turned out cheaper per build, not more expensive. All three machine tiers bill the same $0.0035/CPU-minute, and doubling the cores cut wall time ~13x because the slowness was reclaim pressure near the ceiling, not work.

    What this card got right

    Measured and confirmed:

    • --max-old-space-size does not bound this peak — lowering it to 2048 moved the peak by 23 MB, i.e. noise.
    • experimental.cpus does not either — and more decisively than stated here: the kill lands in the compile phase, before static-generation workers are spawned at all.
    • Next 16 exposes no Turbopack memory limit.
    • «每新增一页文档都在消耗它» — exactly right, and now quantified below.

    What needs correcting

    The attribution. This card explains the 7.6 GB as «The docs site prerenders 400+ MDX pages». Measured on both failing production builds, the kill lands between Creating an optimized production build ... and Compiled successfully, with zero output in between — the prerender phase never executes. The consumer is Turbopack compile. This also clears the 403 build-time OG images and 403 markdown routes, which look like obvious suspects and are not: they are generateStaticParams work in a phase the process dies before.

    The growth curve this card wanted (② asked for it, could not state it)

    local:  peak = 2360 MB + 6.21 MB x pages
    Vercel: peak ≈ 3682 MB + 9.7 MB x pages     → 403 pages predicts 7591 MB
    

    This card independently measured 7592 MB. Two independent measurements, 1 MB apart.

    Consequences:

    • 403 pages sat at 92.7% of an 8192 MB container — past the safe water line, which is why it built fine for weeks and then did not.
    • On 16384 MB the 75% line is near 870 pages. The site is at 403, so option A bought room for the docs to more than double — not merely «间距».
    • Re-check when the page count approaches ~870.

    On option C (shard the prerender): the fixed cost is a floor. Even at 100 pages per shard the peak is ≈4652 MB on Vercel, barely better than 200/shard, for a rewrite of the build architecture. ①'s reasoning that C is «唯一随页面数增长而扩展的形状» holds in principle, but the constant term makes it a poor trade at this scale. B and C are not worth doing.

    Free knobs, for the record

    Only one existed, and it is merged (#12712): experimental.turbopackSourceMaps: false, −434 MB, plus 348 MB less build output. It now banks margin rather than spending it. Everything else measured as noise (±200 MB): turbopackScopeHoisting, --max-old-space-size, turbopackFileSystemCacheForBuild. Rejected: turbopackMinify: false (−972 MB but client JS 5.8 → 16 MB). Non-starter: splitting large MDX files — cost is per page, not per byte; truncating the 292.9 KB v17.mdx moved the peak +191 MB, i.e. noise. Also note Turbopack's build filesystem cache raises peak (4591 → 6488 MB) while cutting compile 20s → 6.1s — it trades away exactly the resource that was scarce, though it may be attractive now that headroom exists.

    Separately, #12743 / PR #12746 now skips docs builds that cannot change the output (80% of pushes in production so far), so the ceiling is approached far less often.

    Closing: option A adjudicated by events, executed, and verified. Refs #12711, #12743, #12698, #12677, #12333.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions