Skip to main content

Your PaaS Bumped the Default Runtime Again: Render's Bun 1.4.3 and the Case for Pinning Build Images You Control

11 min readDora NodaDora Noda
Share
On this page

Two days ago, on October 12, 2026, Render changed the Bun version that new services get. If you created a service on October 11 and another on October 13, and neither pins a version, those two services — same repo, same branch, same build command — now run different JavaScript runtimes. Nothing in your code changed. Only the calendar did.

This is not a bug or a one-off. It is how platform-chosen defaults work: Render documents a sliding window of defaults, and your service's runtime is a function of its creation date. The Bun default has moved three times in three months — each move announced, each reasonable, each quietly forking every unpinned fleet a little further.

This post shows the fork, why even a minor runtime bump can break a build that never changed, and the four places to pin so the runtime is your decision — not your creation date's.

Your runtime is a function of your creation date​

Render's Bun version docs publish the full history. If you don't set a version, the default depends on when you originally created the service:

Service creation dateDefault Bun version
Oct 12, 2026 and later1.4.3
Sep 9 – Oct 12, 20261.4.2
Jul 16 – Sep 9, 20261.3.14
Dec 8, 2025 – Jul 16, 20261.3.4
Aug 18 – Dec 8, 20251.2.20
Before Aug 18, 20251.1.0

Six possible runtimes, selected by birthday. A team that created its API in June, a worker in August, and a preview service this week runs 1.3.4, 1.3.14, and 1.4.3 side by side — spanning a major-minor boundary with real behavioral deltas — while every dashboard still says the same word: "default."

Render is explicit and consistent about the policy. Every default-bump changelog entry carries the same line: newly created services get the new default, while existing services keep their original default version to prevent breaking changes. That sentence is doing a lot of quiet work. It means the platform will never upgrade you — and will also never converge you.

And Bun is not special here: Node.js has the same creation-date table (current default 24.21.0), settable via NODE_VERSION or a .node-version file. The pattern is platform-wide, on every PaaS that installs runtimes for you.

Note the asymmetry this creates. The platform bumps defaults to stay current and secure, which is good. But it applies the new default only to services that don't exist yet, which means the longer your fleet lives, the further it drifts from the default — and the further apart its members drift from each other. Stasis is the price of stability, fragmentation the price of stasis.

A minor bump is never "just" a minor bump​

"But it's a patch release" is the usual reassurance, and for Bun 1.3.x it was mostly true: one team's upgrade notes for the 1.3.1 → 1.3.14 span found no breaking changes affecting their codebase. The trouble is the boundary the defaults crossed twice this quarter: 1.3 → 1.4. Bun 1.4.0 shipped August 20, 2026 headlined by bun test --parallel, and behavior changed underneath it in ways that bite unpinned builds:

  • The bundler started tree-shaking a module it used to keep. One project ran CI against setup-bun with latest, and Bun 1.4.2's bundler dropped a source file from the build that 1.3 had always included. Their words: the release build "died in 'Build kernel' on a tree that had not changed," green on the same baseline a week earlier — only Bun moved. They pinned afterward. Everyone pins afterward.
  • spawnSync learned to wait longer. Bun 1.4 changed spawnSync to wait for every holder of the child's output pipes, not just the child. When some process outlives its parent, a call that used to return now hangs — the kind of intermittent CI timeout that scales with spawn volume and host load, passes on retry, and fails on the release branch.
  • Redirect cleanup regressed, then got fixed. A Bun 1.4 redirect-cleanup failure forced at least one project to feature-detect body.dump() and fall back on older behavior — exactly the version-sniffing tax that appears whenever a fleet runs both sides of a boundary at once.
  • The type surface moved too. bun-types 1.4.x added a memoryPressure event declaration that interacts with @types/node 25's generics changes — a reminder that "the runtime" includes its types, and types are a dependency like any other.

None of these are scandals. Bun documents its changes, fixes regressions fast, and 1.4's headline features are genuinely good. The point is narrower: a runtime is a compiler, a test runner, a bundler, and a standard library wearing one version number.

Any of those moving under you can move your build output. Inheriting that number from your service's birthday means accepting all of those moves sight unseen, on whatever date each service happened to be born.

The fleet fork is worse than one bad deploy​

A single broken deploy is loud: the build fails, someone investigates, someone pins. The creation-date fork is quiet, and quiet is worse. Consider what a mixed fleet actually does to you:

The debugging matrix multiplies. "Works on staging, fails in production" gains a new axis when staging was created in August (1.3.14) and production in June (1.3.4). Every incident review now starts with "which runtime was this service born with?" — a question no dashboard answers at a glance, because every service just says "default."

New services behave differently from old ones. Preview environments and new microservices get the newest default. When the new preview passes and the old service fails — or vice versa — the team debugs application code for an hour before discovering the runtimes differ. The platform's stability guarantee ("we won't change your existing services") becomes a divergence guarantee between old and new.

Security fixes reach you only by rebirth. Bun 1.4.3, released October 10, hardens TLS certificate name matching (CVE-2026-48618), verifies the server during the TLS handshake instead of after it, and rate-limits HTTP/2 stream resets. Services created before October 12 don't get any of that from the platform. The default protects you from breaking changes by also protecting you from security fixes. That tradeoff is only acceptable if you've made it deliberately — which requires knowing your versions, which requires pinning them.

This is the core argument: the default is not a version policy, it's the absence of one. A policy says "we run X, we upgrade on this cadence, here's how we roll it out." A default says "whatever was newest on the day someone clicked Create." One of those survives an audit. The other survives until the first incident.

Pin it in four places​

The fix is unglamorous, which is why it keeps not happening. There are four layers, and each takes about a minute. Do all four — each covers a surface the others don't.

1. The platform layer: BUN_VERSION or .bun-version. Render reads the version from the BUN_VERSION environment variable first, then a .bun-version file in the repo root containing a single line like 1.3.14. Prefer the file: it lives in version control, survives dashboard reconfiguration, and shows up in code review. The env var wins on precedence, so use it for temporary overrides, not as the system of record. And never set either to latest — that converts every deploy into a potential runtime upgrade, which is precisely the failure mode you're eliminating.

One more Render sharp edge worth knowing: if you set no version and have no bun.lock/bun.lockb, your service gets no Bun at all.

text
# .bun-version — one line, in git, reviewed like code
1.4.3

2. The repo layer: packageManager. The packageManager field in package.json names the toolchain the code was written against, and oven-sh/setup-bun now reads it as the default version. One team pins Bun 1.3.10 in three coordinated places — a tool-versions manifest, .bun-version, and packageManager: "bun@1.3.10" — so local shells, the platform, and CI can't disagree. That looks redundant until the first time it catches a drift.

json
{
  "packageManager": "bun@1.4.3"
}

3. The CI layer: an explicit version, not latest. The kernl postmortem above is the canonical warning: both workflows asked setup-bun for latest, and the release bundler changed under an unchanged tree. Pin the action's version input (or a bun-version-file), and run installs with bun install --frozen-lockfile so the lockfile is a contract, not a suggestion. CI is where "only the toolchain moved" failures are cheapest to catch — but only if the toolchain is part of what CI declares.

yaml
- uses: oven-sh/setup-bun@v2
  with:
    bun-version: "1.4.3"

4. The image layer: own the bytes. If you deploy from a Dockerfile instead of a native runtime, the version is an ARG near the top and the base image is digest-pinned. This is the strongest pin of the four: not "version 1.4.3" but "these exact bytes." The Dockerfile is also where local dev, CI, and production finally converge on one artifact — the platform's default table stops applying the moment you ship your own image.

dockerfile
ARG BUN_VERSION=1.4.3
RUN curl -fsSL -o /tmp/bun.zip \
      "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-x64.zip" \
    && unzip -j /tmp/bun.zip 'bun-linux-x64/bun' -d /usr/local/bin

Four pins sounds like four chores. It reads better as one rule: every environment names its runtime in a file that goes through code review. Dashboards get clicked; files get reviewed, diffed, and reverted.

Pinned is not frozen​

Pinning has a failure mode of its own, and honesty requires naming it: a pin from 2025 is how you end up running Bun 1.1.0 in production while CVE fixes pile up behind a boundary you never crossed. The creation-date table's oldest row — "before August 2025: 1.1.0" — is full of services whose owners never chose 1.1.0 and never chose to stay on it either. An unpinned fleet drifts by accident; a never-bumped pin rots by neglect. Same outcome, more paperwork.

So the pin needs a companion discipline: a bump cadence. What works for small teams:

  • Watch the platform's default table, not just your dashboard. When Render moves the default — three times last quarter — treat it as a prompt to evaluate, not background noise. The default is the platform telling you what it considers current.
  • Bump one service first, in a preview environment. Roll the new version to a non-production service, run the full suite (including the integration tests that exercise spawnSync, redirects, and TLS paths — the exact surface that moved in 1.4), then promote the pin across the fleet in one reviewed commit.
  • Read the minor-release notes, skim the patches. The 1.3 → 1.4 boundary carried the behavioral changes; the 1.4.2 → 1.4.3 step carried security hardening. Different bumps deserve different scrutiny, and the Bun blog writes unusually thorough notes.
  • Automate the nudge. A scheduled CI job that builds against the newest Bun and reports — without deploying — turns "we should upgrade sometime" into a green/red signal. Dependabot-style automation for the BUN_VERSION file itself is even better: the bump arrives as a pull request with a diff and a test run, which is exactly the ceremony a runtime change deserves.

The goal is not to run the newest runtime. The goal is for the runtime you run to be a decision with a date, an author, and a test run attached — the same standard you'd apply to any other dependency. 1.3.14 in a file you chose beats 1.4.3 by birthday, every time. And when you're ready for 1.4.3, you'll take it deliberately, with notes read and tests green, instead of inheriting it from a click.


The deeper lesson generalizes past Bun and past Render. Every managed platform makes choices on your behalf — runtimes, base images, buildpacks, default flags — and every one of those choices has a version, a changelog, and a date it changed. Mature teams inventory those choices and pin the ones that can move their output. The platform's default is a fine starting point and a terrible version policy.

Own what you run. Pin the runtime, review the bump, and let "default" be a word that appears in your fleet nowhere.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Your build image is yours to pin, audit, and reproduce. 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