Hedge Your JavaScript Runtime: A CTO’s Playbook After Bun’s Retreat

By Diogo Hudson Dias
A CTO and senior engineer review a runtime-agnostic architecture diagram on a whiteboard in a São Paulo office.

Bun support is being limited and deprecated in places. Deno just shipped another big release. Edge providers keep tightening and loosening resource limits. If your backend assumes a single JavaScript runtime will stay stable for years, you’re doing vendor lock-in the hard way. You need a runtime hedge — not because you want to switch, but because you want the right to switch without a quarter of rework.

Why this matters now

Three headlines in the last few weeks should recalibrate your risk model:

  • Runtime churn is real. Deno 2.8 is out; that’s great velocity and also a reminder that non-Node ecosystems still move fast and change defaults.
  • Support can vanish. Bun support is now limited or deprecated in several tools, which means your CI or host may simply stop promising compatibility.
  • Vendors change course. Microsoft canceling certain AI tool licenses is a different domain, but the same lesson: when you rent capabilities, your roadmap inherits external policy risk.

You can keep the speed of modern JavaScript while insulating your core business from runtime volatility. The trick is to adopt a portable subset + adapter strategy, measure the tax (it exists), and buy an option: the ability to pivot in weeks, not quarters.

A CTO’s Runtime Risk Ledger

Put numbers next to these failure modes before you choose a runtime (or double down on one):

  • API drift: ESM vs. CommonJS, standard Web APIs vs. Node-only modules, subtle differences in streams, crypto, and timers.
  • Execution limits: Edge isolates with 128–256 MB memory caps vs. Node containers with 512 MB–1 GB; CPU time quotas and per-request wall clocks change by provider.
  • Native addons: Anything requiring Node-API or specific libc variants (glibc vs. musl) is an anchor. Moving between AWS Lambda, containers, and isolates becomes a rebuild exercise.
  • Toolchain lock-in: A runtime-specific test runner, bundler, or package manager can turn a “small” migration into a full pipeline rewrite.
  • License landmines: Pulling in AGPL transitive deps into server components can compel source disclosure — a separate, but adjacent, surface area you must watch as you experiment with new runtimes and forks.

Score the probability and blast radius of each for your top workloads. If the blast radius is “payments API down” or “can’t deploy security fix,” hedge it.

The Decision Framework: Pick by Workload, Not Hype

One runtime will not fit all. Map your workload types to the right execution model, then apply a portability discipline where it matters.

1) Latency-critical, stateless request/response

  • Typical examples: edge rendering, A/B routing, personalization, auth token minting, simple adapters.
  • Target: Edge isolates or lightweight serverless that implement the WinterCG Web API set (fetch, Request/Response, Web Crypto, URL, Web Streams).
  • Discipline: ESM-only, no Node built-ins (http, net, tls), avoid fs, no native addons; keep code pure and dependency-light.
  • Why: Cold starts are often single-digit milliseconds on isolates; lower latency to the user; higher scale efficiency. You’ll sacrifice sockets and file I/O — accept it.

2) Stateful or batch jobs with I/O

  • Typical examples: ETL, media processing, PDF generation, queue workers, long-running webhooks, AI model brokering.
  • Target: Node.js LTS in containers or serverless with generous compute (and ideally GPU sidecars where needed).
  • Discipline: Keep all nonportable pieces (fs, native addons, headless Chrome) behind a narrow interface so they can be spun out as a service later.
  • Why: You need stable file/network semantics, predictable memory, and mature debugging. Throughput beats tail latency here.

3) Developer tools and CLIs

  • Typical examples: project generators, internal scaffolding, migration scripts.
  • Target: Deno or Bun can be excellent, especially for ease of distribution and speed.
  • Discipline: Don’t let tool runtime choices leak into your production libraries. Keep prod libs runtime-agnostic.
  • Why: You can swap a CLI runtime with low customer impact; it’s a safe place to exploit bleeding-edge ergonomics.

The Portable Subset: Build on Web Standards First

Most of the hedge comes from leaning into the APIs that every modern runtime is converging on. Prioritize these:

  • fetch / Request / Response: Universal HTTP client/server primitives. In Node 18+, fetch is built-in; elsewhere it’s first-class. Avoid Node’s http/https modules.
  • Web Crypto (SubtleCrypto): Use standard digest, sign, verify, and key generation; avoid crypto polyfills tied to Node-only semantics.
  • URL and URLSearchParams: Don’t use custom parsers.
  • TextEncoder / TextDecoder and Web Streams: Prefer transform streams over Node-only stream types; interop is improving across runtimes.
  • ESM-only: No CommonJS. Conditional exports where needed, but do not introduce dynamic require.

Each time you’re tempted to import a Node builtin, stop and ask: Is there a WinterCG-compliant way? If not, can this live behind a boundary?

Anti-Patterns That Kill Portability

  • Leaning on fs for temp storage in code intended for edge/serverless. Use object storage via fetch instead; or a memory-only cache with bounded size.
  • Native addons for compression, image ops, or crypto when high-quality WASM or pure-JS alternatives exist. If you must use native, isolate it.
  • Node Streams everywhere. Migrate to Web Streams for request/response pipelines.
  • Globally polyfilling Buffer or other Node globals. Prefer explicit imports and portable utilities.
  • SSR that assumes Node APIs for file reads, path resolution, or network sockets. Keep rendering pure; feed it data via fetch.

Your Adapter Layer: The Smallest Box with the Biggest Payoff

Create a single internal package that expresses your narrow runtime-specific needs. Typical surface:

  • kv.get/set/delete: backed by Redis in Node, provider KV at the edge, or a durable object store.
  • secrets.get(name): maps to process.env in Node, environment bindings at the edge, and encrypted local store in dev.
  • scheduler.delay(fn, ms): uses setTimeout in Node, provider alarms in edge environments, and a no-op fallback in local tests.
  • storage.put/get: S3-compatible fetch client; never raw fs for portable paths.

Then write two to four concrete implementations: Node, Edge, and any special-case host you rely on. Your application code imports the interface, not the host.

Conformance Tests: Make Drift Visible

Portability without a test matrix is theater. Add a job that runs your unit and integration tests across these targets:

  • Node.js: current and previous LTS (for 2026, think 20.x and 22.x).
  • Deno: current stable (2.x). Use its Node-compat mode only in adapter tests, not app code.
  • Bun: pin a known-good version for dev tooling and adapters; fail fast if semantics differ.
  • Edge emulator: local isolate emulator (e.g., Miniflare-like) to validate Web API assumptions.

Gate merges on this matrix for libraries that must remain portable. Expect a 10–15% build time tax and occasional refactors. It’s cheaper than a forced migration under outage pressure.

Performance Reality Check

Numbers vary by provider and app, but the shape of the trade-off is reliable:

  • Cold start: isolate-based edge often wakes in single-digit milliseconds; generic serverless Node functions are typically tens to hundreds of milliseconds depending on runtime and memory size.
  • Memory: edge isolates are commonly limited to ~128–256 MB per request; Node containers/functions offer 512 MB and up.
  • CPU time: edge enforces tight per-request compute ceilings; containers give you steady CPU and are friendlier to streaming transforms and CPU-bound tasks.

Don’t pick a runtime to “win benchmarks.” Pick it to meet a clear SLO: P95 latency, throughput, and cost per 1K requests. Then use your adapter to place each function in the right execution tier.

Security and Compliance Don’t Pause for Runtimes

Runtime hedging is also a security play:

  • SSA/CSA-ready SBOMs: generate and store SBOMs per build per runtime. If a CVE lands in a Node-only transitive, you can cut over to an edge-safe variant faster.
  • License scanning: add a stage that flags AGPL, SSPL, or unvetted licenses in server-executed code. The recent public blowups around AGPL violations are warnings, not footnotes.
  • Secrets discipline: no dotenv-only hacks; your adapter should centralize secret access and make local vs. prod explicit.

People and Process: Keep Velocity While You Hedge

Brazil has 750K+ developers and a deep bench of Node/TypeScript talent. Most haven’t shipped production Deno or Bun yet — and that’s fine. The portability skills you need are teachable in weeks:

  • Week 1–2: Train on Web APIs (fetch, Web Crypto, Web Streams) and ESM-only practices; eliminate CommonJS in new code.
  • Week 3–4: Introduce the adapter pattern; migrate one noncritical service to use it.
  • Ongoing: Keep a “runtime-agnostic” lint profile and enforce it in PRs for portable packages.

The cost: expect a 5–10% hit to raw feature velocity for teams owning portable modules. The benefit: when you do need to pivot runtimes, you typically save 4–6 weeks of one-time migration work and avoid a risky freeze.

Your 90‑Day Runtime Hedge Plan

Days 0–30: Inventory and Boundaries

  • Inventory all runtime-specific calls (Node built-ins, native addons, global polyfills). Tag by criticality and replaceability.
  • Define your portable subset and codify it in lint rules. ESM-only for all new code.
  • Stand up a tiny adapter package and implement Node and Edge versions for kv, secrets, scheduler, storage.
  • Add a conformance test job to CI across Node LTS and one alternate runtime. Fail green-field modules that drift.

Days 31–60: First Port and Observability

  • Migrate one latency-sensitive endpoint to the portable subset and deploy it to an edge runtime side-by-side with your Node path. Route 1–5% of traffic and measure.
  • Migrate one batch worker to isolate native deps behind the adapter. If impossible, log that as deliberate technical debt with an exit plan.
  • Instrument runtime/version/region tags into every log line and trace. Build per-runtime SLO dashboards.

Days 61–90: Expand and Enforce

  • Port 30–50% of your request/response endpoints that fit edge constraints; leave the rest on Node.
  • Harden the adapter (chaos tests across implementations, timeouts, backoffs). Document it like a public API.
  • Make the conformance matrix a merge gate for any library intended to be reused across services.

Trade-offs and When to Say No

Hedging isn’t free. Say no to portability when:

  • You truly need Node-only or native features and have no realistic alternative (e.g., high-fidelity PDF rendering with headless Chrome). Keep it in a bounded service.
  • The code is throwaway (a one-off migration script). Prefer speed and delete it after use.
  • Your team is under acute delivery pressure. Timebox hedging to the adapter and linting; schedule the rest next cycle.

Otherwise, buy the option. Today it’s Bun support turning wobbly; tomorrow it could be a billing change or a platform capability you suddenly need that your current runtime can’t give you.

What Good Looks Like in Six Months

  • You can run critical libraries on Node LTS and at least one alternative runtime without code changes beyond the adapter binding.
  • Your SLOs are per runtime, and you have graphs proving where edge wins and where Node wins.
  • You have zero undifferentiated runtime glue code outside the adapter; everything else is application logic.
  • Developers default to Web APIs first. Code review catches Node-only imports instantly.

That posture gives you leverage with vendors, fewer 2 a.m. surprises, and a hiring funnel that taps the huge TypeScript market (including 6–8 hours of US/Brazil overlap) without training engineers into a corner.

Key Takeaways

  • Don’t bet your backend on a single JavaScript runtime’s goodwill. Buy an option to switch with a portable subset and an adapter layer.
  • Pick runtimes by workload: edge for latency-pure stateless paths, Node LTS for I/O heavy and long-running jobs, bleeding-edge runtimes for tools/CLIs.
  • Lean on Web standards (fetch, Web Crypto, Web Streams, URL) and ESM-only; avoid Node-only modules in portable code.
  • Run a conformance matrix across Node LTS and at least one alternate runtime; gate merges for shared libraries.
  • Expect a 5–10% velocity tax for portability; it pays back 4–6 weeks of migration time when a pivot is necessary.
  • Centralize secrets, storage, kv, and scheduling in a tiny adapter; treat it like a product with tests and docs.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation