Skip to main content

Your Build Takes 4 Minutes and Nobody Knows Why: What Cloud Native Buildpacks' RFC 0131 Would Itemize

8 min readDora NodaDora Noda
Share
On this page

Your deploy-from-git build just took 4 minutes and 12 seconds. Was it the Node engine download? A cold dependency cache? The minifier? Today the honest answer is a shrug followed by scrollback archaeology: thousands of interleaved log lines, a total duration, and no per-component accounting. The build is a black box that reports success or failure and nothing else.

Here is what that same build would look like itemized, under Cloud Native Buildpacks' RFC 0131 — the build-observability proposal Heroku says is coming in Q3 2026. Every lifecycle phase and every buildpack emits opt-in OpenTelemetry traces as append-only .jsonl files under a single directory, and a platform can turn them into a receipt like this one (illustrative numbers):

text
<layers>/tracing/
├── lifecycle/
│   ├── detect.jsonl     # 3s — detection order resolved across 8 buildpacks
│   ├── analyze.jsonl    # 2s
│   ├── restore.jsonl    # 6s — dependencies layer: cache HIT (0.4s)
│   ├── build.jsonl      # 3m31s total, itemized per buildpack below
│   └── export.jsonl     # 18s
└── buildpacks/
    ├── node-engine@2.4.1-build.jsonl    # 2m58s — download 1m52s + install 1m06s
    ├── node-dependencies@1.2.0-build.jsonl  # 24s — npm ci, cache MISS
    └── node-start@0.9.3-build.jsonl     # <1s

Three things jump out that no scrollback log gives you today. First, the culprit has a name and a number: node-engine ate 2:58 of a 4:12 build, and inside that span, 1:52 was pure network download time — the kind of line that turns "builds feel slow" into "mirror the Node tarball or warm the download cache." Second, the cache story is explicit: the restore phase shows the dependencies layer hitting cache in 0.4 seconds while the engine layer missed and paid full price — "which layer never hit cache" answered structurally, not by guessing from timestamps. Third, when a build fails instead of dragging, the error span carries the failing buildpack's id, the error type, and the message — failure attribution to the exact buildpack in the detection order, instead of whichever log line happened to flush last.

None of this exists as a shipped default yet. But the spec is approved, the file layout is fixed, and the design is unusually honest about what a platform needs. The rest of this post is about what the RFC actually specifies, why today's introspection can't do the job, and what a self-hosted PaaS would build on top of it.

What RFC 0131 actually specifies

The RFC — "Buildpack Observability," opened in October 2022 by Josh Lewis and now approved — proposes leveraging OpenTelemetry to answer the questions every build operator eventually asks. It lists them explicitly: which buildpacks commonly fail to compile, how often a particular error occurs, how long each buildpack's compile phase takes, how often a buildpack is used, which language versions get installed, and how long dependency downloads take. If you run a build platform, that list reads like your own incident-review notes.

The mechanism has two trace types. Lifecycle tracing is buildpack-agnostic: which buildpacks were available, which were detected, how long each phase — detect, restore, build, export — took. Buildpack tracing is buildpack-specific: how long it took to download a language binary, which version was selected, where time went inside one buildpack's work. Both are emitted in OpenTelemetry's File Exporter format as .jsonl, one file per phase per emitter, appended frequently so a crashed or killed build still leaves its partial trace behind.

Three design choices matter more than the format. First, everything is opt-in: lifecycle and pack emit nothing unless a --telemetry flag is passed, and the data lands only on the build filesystem — no collector to run, no phone-home. The RFC explicitly rejected direct OTLP export because local builds have no collector and platforms have firewalls; files are the lowest-common-denominator transport. Second, traces correlate with platform traces via a CNB_OTEL_TRACEPARENT variable following the W3C trace-context spec, so a platform can parent a whole build trace under its own deploy span. Third, the privacy rules are load-bearing: no personally identifiable information, no business-sensitive data (passwords, keys, image names, repository names), and the files must never become an inter-buildpack communication channel. That restraint is what makes the tenant-facing uses in the next section shippable rather than creepy.

Why scrollback logs cannot do this today

It is worth stating plainly what introspection exists now, because "nothing" is not quite true — and the gap is precise. pack build --report-output-file already emits a JSON report with buildpack ids, versions, the run image, and the build BOM. That tells you what was built. It says nothing about how long each piece took or why a step failed. Duration and causality are exactly the two dimensions a slow-build complaint or a red build needs.

The raw lifecycle stdout fills the gap the way a firehose fills a glass. Buildpack output interleaves non-deterministically — there is a standing issue against pack about incorrect, non-deterministic ordering of buildpack stdout and stderr — so attributing a failure to the correct buildpack in the detection order means reconstructing event order from text that never promised one. And layer-cache behavior, the single biggest lever on rebuild speed, is invisible: nothing in today's output distinguishes a 0.4-second cache restore from a 96-second cold reinstall except wall-clock vibes. Operators learn to read the tea leaves; tenants never do.

This is the sense in which observability was the CNB spec's most obvious gap. The zero-config build layer knew everything about your build and told you almost none of it in a form a machine could use.

What a self-hosted PaaS builds on top

The RFC deliberately leaves consumption to the platform operator — read the files during or after the build, enrich them with your own build_id, export to an OpenTelemetry backend like Honeycomb or Prometheus. That division of labor implies two concrete products, one facing the tenant and one facing the fleet.

The tenant-facing build receipt. Instead of streaming raw lifecycle stdout, the platform renders the trace: per-buildpack durations, cache hit or miss per layer, the failing buildpack and its error type on red builds. "node-engine added 40 seconds" and "dependencies layer never hit cache" stop being support-ticket mysteries and become lines in the deploy log the developer already reads. Because the traces carry no PII, no secrets, and no repository names by spec, showing a tenant their own build's spans needs no scrubbing pipeline — the privacy design is doing real work here.

The fleet-wide build dashboard. The same spans, aggregated across every tenant build, answer the operator's questions: per-buildpack failure rates after a builder update, version drift in installed toolchains, download-time regressions when a registry mirror degrades, restore-phase durations trending up as caches go cold. These are the queries that turn "the platform feels slow this week" into a diff — a builder image bump, a mirror change, a cache-eviction policy — with the trace timestamps to prove it.

Neither requires a new agent, sidecar, or collector inside the build. The files are group-readable on the build filesystem; the platform reads them where the build already ran. That is the quiet elegance of the files-over-network choice: the smallest possible integration surface for the platform that has to operate it.

The competitive frame: why this gap mattered most against faster rivals

Cloud Native Buildpacks graduated to the CNCF's highest maturity level in July 2026, with Heroku maintaining dedicated upstream teams — the governance story is settled. But on raw build speed and image size, the challengers have been louder. Railway's Railpack, the Go-and-BuildKit successor to Nixpacks, produces images Railway reports as 38 percent smaller for Node and up to 77 percent smaller for Python versus Nixpacks, emitting BuildKit LLB directly rather than generating a Dockerfile. Meanwhile Nixpacks itself sits in maintenance while Coolify and Dokploy still default to it.

Speed is real, but it is not introspectable. A faster black box is still a black box: Railpack is single-vendor and young, and neither it nor Nixpacks offers anything resembling per-component, machine-readable build accounting. That is the trade the RFC reframes. CNB already had the structural advantages for a fleet — reproducible layers, rebase patching a base image across tenants without rebuilding application layers — and observability was the missing piece that let faster rivals define "better" as "faster" alone. With per-buildpack timing, derived cache effectiveness, and failure attribution as platform-consumable data, the zero-config build layer that tells you the most about your build is also the open standard one. Two honest caveats ship with that claim: consumption is genuinely left to the platform (there is no default dashboard, only files), and the File Exporter format the RFC builds on is still officially experimental, matching OTLP nearly exactly but not yet stable.

What it does not solve

Three limits, stated plainly. First, opt-in cuts both ways: no fleet-wide data exists until the platform wires --telemetry through its build path and stands up the read-and-export loop. The RFC gives you the instrument, not the observatory. Second, the timeline is a vendor statement, not a release: Heroku describes RFC 0131 as coming in Q3 2026, and until lifecycle and pack releases actually emit these files, everything above is a blueprint with fixed dimensions, not a feature you can enable. Third, telemetry files are excluded from the build result by design — they would bloat image size and harm reproducibility — so debugging a months-old image still requires the stored trace, not the image. Keep your traces as long as you keep your build logs.


None of those limits changes the shape of the answer. The zero-config build layer knew everything about your build; RFC 0131 is the proposal that it finally start telling you, one span at a time — which buildpack, how long, hit or miss, and exactly where it broke. For a platform that deploys other people's code for a living, that receipt is not a nice-to-have. It is the difference between operating builds and merely hosting them.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex