Server-side JavaScript runtimes in 2026: revisit Node.js, Deno and Bun before you move another service off Node.js
Every few months someone in a JavaScript team asks the same question: should we move this service to Bun, or to Deno? The question usually arrives with a benchmark chart, a complaint about slow installs or slow tests, or a wish to run TypeScript without a build step. Sometimes it arrives after a security scare, when somebody reads about Deno’s permission flags and wonders why the Node.js service can read every file on the machine.
This post compares the three server-side JavaScript runtimes that teams actually consider in 2026: Node.js, Deno and Bun. The angle is not speed. It is the less exciting work of moving an existing production service: whether your npm packages and Node APIs still behave, what the permission models really protect, how TypeScript is handled, what replaces your test runner and package manager, how long each version is supported, how often security fixes arrive, and what your monitoring sees after the switch. We look at where each runtime stands on 27 October 2026, and at a plan for one invented team that was about to move a service for the wrong reasons.
Two earlier posts sit close to this one. In server-side WebAssembly with Wasmtime, WasmEdge and Wasmer we saw that Node’s vm module is not a security boundary. The Node.js permission model, discussed below, has a similar warning in its own documentation. In dependency updates with Renovate, Dependabot and Updatecli we looked at keeping libraries current. The runtime itself is the dependency most teams forget to update.
A scenario to keep us honest
Take an invented Hubballi company that builds dealer management and service billing software for tractor and farm equipment dealers in north Karnataka and nearby districts of Maharashtra. About 25 engineers. The main product is a TypeScript API on Node.js with Express, PostgreSQL, a React front end, and a few background workers that generate GST invoices, warranty claim PDFs and photo thumbnails for service jobs.
Their runtime setup looks like this:
- The API and workers run on Node.js 20 in containers built from an image that was last changed in early 2025. Node.js 20 reached end of life on 30 April 2026. Nobody noticed until a vulnerability scanner started flagging the base image.
- TypeScript is compiled with
tscin CI. Tests run on Jest. Packages are installed with npm and a committedpackage-lock.json. - Dependencies include
bcrypt(a native addon) for password hashing,sharpfor thumbnails, an OpenTelemetry SDK with automatic instrumentation for HTTP and PostgreSQL, andpinofor logs. - The code base uses TypeScript
enums in many places, and a few classes with constructor parameter properties. - Configuration comes from environment variables. Secrets are mounted as files.
Two proposals came up in the same week. One developer wanted to move the API to Bun, because tests and installs felt faster on his laptop and TypeScript would run without a build step. Another, after the team’s secret scanning review, wanted Deno, because a service that cannot read files or open connections without explicit permission sounded safer.
The question this post tries to answer: is a runtime switch the right fix for this team, and if not, what is?
The short version
- Node.js (MIT, under the OpenJS Foundation) is the default, and in 2026 it has quietly absorbed many of the reasons people left it: it runs erasable TypeScript directly, has a stable built-in test runner, a stable permission model and a long, published support window. Node.js 26 becomes LTS on 28 October 2026 and is supported until 30 April 2029. From Node.js 27, there will be one major release a year and every release becomes LTS.
- Deno (MIT) is secure by default, runs TypeScript with type checking, ships formatter, linter and test runner, and has good npm and Node API compatibility. But on 9 October 2026 the Deno team announced it is joining Cloudflare and will support the Deno runtime for one more year with monthly bug fix and security releases, after which it will end its own development of the runtime. That changes the calculation for any long-lived service.
- Bun (MIT, with statically linked LGPL-2 JavaScriptCore and WebKit code) is an all-in-one runtime, package manager, bundler and test runner built for speed and Node compatibility. It was acquired by Anthropic in December 2025 and rewritten from Zig to Rust for Bun 1.4 in August 2026. It has no permission model and no LTS line; only the latest 1.x release gets fixes.
- For the Hubballi team, the plan in this post is: get off the end-of-life Node.js 20 now, move to Node.js 26 LTS with Node.js 24 as the fallback, use Node’s permission flags as a seat belt in staging first, and keep Bun for a bounded pilot on an internal tool. Deno is not chosen, mainly because of its new support horizon.
- Whatever you choose, the runtime is a security dependency. All three published security fixes in 2026, several of them in the very permission features that make each runtime look safer.
Where things stand on 27 October 2026
All times below are converted to IST.
Node.js
- Current line: Node.js 26. Latest release 26.12.1, dated 21 October 2026 in the Node.js release index. Node.js 26.0.0 came out on 5 May 2026 with V8 14.6 and npm 11.12. Node.js 26.12.1 bundles npm 11.20.0.
- LTS lines: Node.js 24 “Krypton” (latest 24.22.1, 20 October 2026) moved from Active LTS to Maintenance LTS on 20 October 2026 and reaches end of life on 30 April 2028. Node.js 22 “Jod” (latest 22.23.4, 20 October 2026) is in Maintenance until 30 April 2027. Node.js 20 reached end of life on 30 April 2026, and Node.js 25 on 1 June 2026.
- According to the project’s
schedule.json, Node.js 26 enters LTS on 28 October 2026, moves to Maintenance on 20 October 2027 and reaches end of life on 30 April 2029. - Release model change: in March 2026 the Node.js releasers announced that from Node.js 27 there will be one major release a year, in April, with LTS promotion in October, and every release will become LTS. An Alpha channel, starting with Node.js 27 in October 2026, replaces the old odd-numbered releases for early testing. Total support is 36 months from the first Current release. Node.js 26 is the last line under the old model.
- TypeScript: type stripping is stable since Node.js 25.2.0 and 24.12.0. Node runs files with erasable TypeScript syntax directly. It does not type-check, and syntax that needs code generation, such as
enumand namespaces with runtime code, fails in strip-only mode. Node.js 26.0.0 removed the--experimental-transform-typesflag. - Permission model: stable since 23.5.0 and 22.13.0.
--permissionrestricts file system, network (--allow-net, added in 25.0.0 and still marked as in active development), child processes, worker threads, native addons, WASI and FFI. An audit mode,--permission-audit, was added in 25.8.0. The documentation calls the model a “seat belt” for trusted code and says it does not provide security guarantees in the presence of malicious code. - Security in 2026: security releases on 13 January, 24 March, 17 and 18 June, 28 and 29 July, and 20 and 21 October 2026. The October release fixed four high-rated issues, including an out-of-bounds write in the bundled Brotli decoder (CVE-2026-111497) and TLS hostname verification bypasses, and updated the bundled CA certificates after an eMudhra intermediate CA compromise. Across the five 2026 security releases, 14 fixes were for permission model bypasses or over-grants, rated from low to high.
- Licence: MIT. Governance: an Impact project of the OpenJS Foundation, steered by its Technical Steering Committee. Node.js is maintained mostly by volunteers, which the project itself gives as one reason for the new release schedule.
Deno
- Latest release: 2.9.8, published on 21 October 2026 at about 8:58 PM IST. Deno 2.9.0 came out on 25 June 2026. Deno 2.9.8 reports V8 15.0 and TypeScript 6.0.3.
- Release model, from the Deno documentation: a new minor version every 12 weeks, with patch releases in between; channels for stable, LTS, release candidate and canary.
- LTS: the LTS channel is on the 2.9 line, starting from 2.9.3, and is maintained until 31 January 2027. Earlier LTS lines were 2.1, 2.2 and 2.5; the 2.5 line ended on 30 April 2026 and the 2.9 line started on 1 July 2026, so there was a gap of two months without an LTS line.
- Supply chain defaults: Deno does not run npm lifecycle scripts such as
postinstallunless you allow them per package, and since 2.9 it skips npm package versions younger than 24 hours by default. - Future: on 9 October 2026 Ryan Dahl announced that the Deno team is joining Cloudflare. The post says the team will support the Deno runtime for another year with monthly releases containing bug fixes and security updates, after which it will end its development of the runtime. Deno will remain open source and others are welcome to continue it. Deno Deploy will shut down after six months, and JSR will continue on Cloudflare infrastructure.
- Security in 2026: the Deno repository lists 14 advisories published in 2026, including one critical (
node:cryptonot finalising a cipher, fixed in 2.6.0), several high-rated command injection issues aroundnode:child_processand process spawning, some of them specific to Windows, and six permission or sandbox bypasses, such asfetch()and WebSocket missing a check after DNS resolution (fixed in 2.8.1). - Licence: MIT. Governance: a company project of Deno Land Inc., now moving with its team to Cloudflare. The repository dates from May 2018.
Bun
- Latest release: 1.4.4, published on 22 October 2026 at about 3:41 PM IST. Bun 1.4.0 came out on 20 August 2026.
- Bun 1.4 is the first release written in Rust. The Bun blog post on the rewrite (8 July 2026) says the port from about 535,000 lines of Zig was done with AI assistance over 11 days and that 1.4.0 fixes 128 bugs that reproduced in 1.3.14. The 1.4 release notes say Bun now runs more than 1,500 additional tests from the Node.js test suite on every commit. Performance and memory numbers in those notes are Bun’s own measurements.
- Support: the Bun security policy lists 1.x as the supported version. There is no LTS line; fixes go into the next release, and you are expected to upgrade.
- Ownership: Bun was acquired by Anthropic, announced on 2 December 2025. The announcement says Bun stays open source and MIT licensed, with the same team.
- Security in 2026: three advisories published on GitHub, all on 23 and 24 October 2026 and all fixed in 1.4.4: the same Brotli out-of-bounds write that Node.js fixed, a trust problem with compromised eMudhra (emSign) intermediate CAs in the bundled root certificates, and an HTTP/3 QPACK amplification issue. The 1.4 release notes also list many security hardening fixes without separate advisories, so the advisory count alone understates the work.
- Licence: Bun itself is MIT. It statically links JavaScriptCore and WebCore from WebKit, which are LGPL-2, and the licence file explains how to relink. The repository dates from April 2021.
Nearby options, briefly
- Cloudflare workerd, the open-source runtime behind Cloudflare Workers, is where the Deno team says its future work goes. It follows a different, request-scoped programming model and is not a drop-in home for an Express service.
- Older Node.js lines still on Node 18 or 20 are past end of life. Some vendors sell extended support for them. That buys time, not a plan.
First question: why do you want to switch?
Write down the real reason before comparing runtimes. Each reason has a different, often cheaper, answer.
- “The runtime is out of support.” Upgrade within the same runtime first. This is the most urgent and the most common real problem.
- “We want to run TypeScript without a build step.” Node.js does this now for erasable syntax. Bun and Deno do it for more syntax. Either way, you still need
tsc(or another checker) for type checking in CI. - “Installs and tests are slow.” Measure where the time goes. Package manager choice, cache use in CI and test parallelism usually matter more than the runtime that executes the server.
- “We want stronger isolation.” Runtime permission flags help against mistakes and some supply-chain surprises. They are not a sandbox for hostile code. Containers with read-only file systems, network policy and separate users carry the real weight.
- “It is faster.” Possibly, for some workloads. For an API that spends most of its time waiting on PostgreSQL, the runtime is rarely the bottleneck. Measure your own service before believing anyone’s chart.
For the Hubballi team, the honest list was: the runtime is out of support, the build is slow mainly because tsc and Jest run without caches in CI, and the security worry is about dependencies, not the runtime.
What each runtime is
Node.js
Node.js combines Google’s V8 engine with libuv for its event loop and asynchronous I/O, plus a large standard library under node: names: fs, net, http, crypto, worker_threads and many more. Packages come from the npm registry through npm, which ships with Node, or through pnpm or Yarn. It understands CommonJS and ES modules, and in recent versions can require() ES modules.
Over the last two years Node.js has added features that used to be reasons to look elsewhere: a stable test runner (node:test), watch mode, .env file loading, built-in fetch and WebSocket client, a stable permission model, single executable applications and, now, TypeScript type stripping. Most production tooling (APM agents, profilers, cloud SDKs) is built and tested against Node first.
Deno
Deno is a runtime built in Rust on V8, started by Ryan Dahl, who also created Node.js. Its first design choices were reactions to Node: secure by default, URL and ES module imports, TypeScript out of the box, web standard APIs, and one binary with a formatter, linter, test runner, documentation generator and compiler to a single executable. Deno 2 (October 2024) made Node and npm compatibility a central goal: npm: specifiers, package.json and node_modules support, and most node: modules.
Deno type-checks TypeScript as part of its tooling, which Node and Bun do not do at runtime. Its permission flags (--allow-read, --allow-net, --allow-env, --allow-run and others) can be scoped to paths, hosts, variables and programs, and can be stored in configuration.
Bun
Bun is an all-in-one toolkit: runtime, npm-compatible package manager (bun install with a text bun.lock), bundler, and a Jest-like test runner (bun test). It uses Apple’s JavaScriptCore engine, the one in Safari, instead of V8. It implements most Node.js APIs and many of its own: a fast HTTP server, built-in SQLite, PostgreSQL, MySQL, Redis and S3 clients, shell scripting and single-file executables. TypeScript and JSX run without configuration, including enum.
Bun’s own Node.js compatibility page marks each node: module as fully, partly or not implemented, with notes. It is the best single document to read before a migration, because it lists exactly what differs.
Side by side
npm packages and Node API compatibility
- Node.js: the reference. If a package works anywhere, it works here. Native addons built with N-API work, and prebuilt binaries are published for Node first.
- Deno: most npm packages work through
npm:specifiers or apackage.json. Native addons need a localnode_modules,--allow-ffi, and often explicit permission to run their install scripts, since Deno does not run them by default. Somenode:APIs behave differently in edge cases. In our drill, spawning a process throughnode:child_processneeded full environment access, not just--allow-run. - Bun: aims to be a drop-in replacement and runs a growing share of Node’s own test suite. Its compatibility page still marks several modules as partial, for example
node:async_hooks(onlyAsyncLocalStorageandAsyncResourceare real;createHookis a stub),node:diagnostics_channel(missing thehttp.serverand several other built-in channels),node:perf_hooks(eventLoopUtilization()returns zeros),node:tlsandnode:crypto(BoringSSL instead of OpenSSL, some algorithms missing). These are exactly the areas that APM agents and security libraries touch.
Permissions and isolation
- Node.js: off by default. With
--permission, file system, network, child processes, workers, addons, WASI and FFI are denied unless granted. Environment variables are not covered. Grants are coarse in places:--allow-netis all or nothing, and in 26.12.1 it still prints an experimental warning. The audit mode lets you discover what a service needs before enforcing. The documentation is clear that this protects against mistakes by trusted code, not against malicious code. - Deno: on by default. Every category is denied until granted, including environment variables, and most grants can be scoped. This is the strongest model of the three. It is still a runtime check, and 2026 brought several bypass fixes.
- Bun: no permission model. A Bun process can do whatever its operating system user can do. Isolation has to come from the container or the operating system.
- All three: the real boundary for a production service is the container and the host: a non-root user, read-only root file system, only the mounts it needs, network policy, and secrets limited to what the service uses.
TypeScript
- Node.js: strips erasable types and runs the file. No type checking, no
enum, no parameter properties, no runtime namespaces in strip-only mode. The TypeScripterasableSyntaxOnlyoption (TypeScript 5.8 and later) makes the compiler warn about syntax that Node cannot strip. You still runtsc --noEmitin CI. - Deno: runs and type-checks TypeScript with its bundled compiler. Most syntax is supported.
- Bun: transpiles TypeScript with its own fast transpiler, including
enum, but does not type-check. You still runtsc --noEmitin CI.
Test runner and tooling
- Node.js:
node:testwith mocking, coverage and watch mode. Many teams keep Jest or Vitest, which run on Node first. - Deno:
deno test,deno fmt,deno lint,deno check,deno compile, all built in. - Bun:
bun testwith a Jest-compatible API, usually much faster to start. Some Jest features and plug-ins behave differently, so read the failures carefully when you switch.
Package management and supply chain
- Node.js: npm 11 ships with Node.js 26 and runs dependency lifecycle scripts by default. npm 12.0.0 (July 2026) blocks dependency lifecycle scripts by default unless allowed by the root package’s
allowScriptspolicy, but it is not the version bundled with Node.js 26.12.1, so check which npm your images and CI actually use. pnpm and Yarn are common alternatives. - Deno:
deno installreadspackage.jsonand keeps adeno.lock. Lifecycle scripts are off unless allowed per package, and, from 2.9, versions newer than 24 hours are skipped by default. - Bun:
bun installwritesbun.lock. Lifecycle scripts run only for packages intrustedDependencies(Bun ships a default list for popular npm packages). New monorepos default to an isolatednode_moduleslayout that prevents phantom dependencies. - Changing package manager changes your lockfile, and with it, the exact versions you run. Treat it as a separate change from the runtime.
Support windows and security releases
- Node.js: published schedule, 30 months of LTS, security releases a few times a year, coordinated across all supported lines with advance notice.
- Deno: LTS lines of about six months, the current one until 31 January 2027; overall runtime support announced for about one more year from October 2026.
- Bun: only the latest release. Frequent releases, fixes land quickly, but you must keep up.
Container images
- All three publish official images. For Node.js 26.12.1 there are Debian
trixie-slimandbookworm-slimvariants among others. Bun 1.4.4 hasdebian,slim,alpineanddistrolesstags. Deno 2.9.8 hasdebian,alpineanddistrolesstags, plus abinimage to copy the binary into your own base. - Pin to a full version, or better a digest, and let your dependency bot raise the update.
Observability
- Node.js: the reference for APM vendors and OpenTelemetry instrumentations, which rely on module loading hooks,
diagnostics_channelandAsyncLocalStorage. V8 heap snapshots and CPU profiles through the inspector are standard. - Deno: has built-in OpenTelemetry support for its own APIs. Node-based instrumentation packages may or may not attach, depending on the hooks they use.
- Bun:
AsyncLocalStorageworks, which covers request context in most loggers and tracers. Agents that depend onasync_hooks.createHook, missing diagnostics channels or event loop utilisation will be partly blind. Heap statistics describe JavaScriptCore, not V8, so dashboards built on V8 heap spaces change meaning.
How a request actually runs
Following one HTTP request through each runtime shows where behaviour can change after a switch.
- Accept. Node.js accepts the connection through libuv and its
httpmodule in JavaScript. Deno accepts it in Rust code (Deno.serve) or through itsnode:httplayer. Bun accepts it in native code built on uWebSockets. - Parse. Node.js uses llhttp. Deno and Bun use their own parsers. Header limits, chunked encoding edge cases and error messages can differ, which matters behind a load balancer or API gateway.
- Run your handler. V8 in Node.js and Deno, JavaScriptCore in Bun. Your Express or Fastify code runs on top of each runtime’s
node:httpcompatibility. - Call the database and other services. Your PostgreSQL driver uses
node:netandnode:tls. Node.js uses OpenSSL for TLS, Deno uses rustls, and Bun uses BoringSSL. Certificate stores, cipher support and hostname checks are where 2026’s security fixes clustered. - Carry context.
AsyncLocalStoragekeeps the request ID and trace context acrossawait. All three support it. Lower-level hooks differ. - Check permissions. Node.js checks only if started with
--permission. Deno checks on every privileged call. Bun does not check. - Write the response and log. Logging libraries that write directly to file descriptors or replace
process.stdout.writemay behave differently; Bun’s compatibility page notes that replacingprocess.stdout.writedoes not captureconsoleoutput.
Steps 4 and 5 are the ones to test carefully. A service can pass its unit tests on a new runtime and still lose traces, or fail a TLS handshake with one partner’s old server.
A worked plan for the Hubballi team
Step 1: get off end-of-life Node.js before anything else
A runtime migration takes weeks. The unsupported runtime is a risk today. Move the API and workers to a supported Node.js line first. The team runs CI on both Node.js 24 and Node.js 26, fixes what breaks (native addons such as bcrypt and sharp need versions with prebuilt binaries for the new line), and plans to ship Node.js 26 once it has been through November in staging. Node.js 24 stays as the fallback if a dependency is not ready, since it is supported until 30 April 2028.
Step 2: list what ties the service to a runtime
Search the code and the dependency tree for the things that decide portability:
- Native addons (
bcrypt,sharp, anynode-gypbuild). node:vm,worker_threads,child_process,cluster,async_hooks,diagnostics_channelandperf_hooksusage, directly or in dependencies.- The APM or OpenTelemetry setup and how it hooks in.
- TypeScript syntax that cannot be stripped:
enum, parameter properties, runtime namespaces, legacy decorators with metadata. - Assumptions about environment variables and file paths.
This list is useful even if you never switch, because it is exactly what breaks on a Node.js major upgrade too.
Step 3: fix the build and test time where it is spent
Cache ~/.npm and the TypeScript incremental build information in CI, run Jest with sharding across two runners, and split tsc --noEmit from the build. Turn on erasableSyntaxOnly in tsconfig.json and replace enums with as const objects over time, so scripts and tools can run with Node’s type stripping. These changes often recover most of the time that made Bun look attractive.
Step 4: add Node’s permission flags as a seat belt, in audit mode first
Run the service in staging with --permission-audit and a small diagnostics_channel subscriber that logs what would have been denied. When the log is quiet, switch to enforcement. A container for the API can look like this:
FROM node:26.12.1-trixie-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY dist ./dist
USER node
CMD ["node", "--permission", \
"--allow-fs-read=/app/", \
"--allow-fs-read=/run/secrets/", \
"--allow-fs-write=/tmp/", \
"--allow-addons", \
"--allow-net", \
"dist/server.js"]
Three notes on this example. --allow-addons is needed because bcrypt and sharp are native addons, and a native addon runs outside any JavaScript permission check, so this flag weakens the seat belt; moving password hashing to node:crypto scrypt would let the team drop it later. --allow-net is all or nothing, so keep network policy at the container or cluster level. And none of this replaces a non-root user, a read-only root file system and narrow secret mounts in the deployment.
Step 5: give Bun a bounded pilot
Pick one internal tool, for example the script that regenerates thumbnails for old warranty claims, and run it on Bun 1.4.x in CI for a month. Measure install time, test time and memory on your own machines. Note every compatibility difference you hit. If the results are good and the team is ready to upgrade Bun every few weeks, consider it for new internal services. Do not move the billing API in the same quarter.
Step 6: make runtime upgrades boring
- Subscribe to the Node.js security release announcements, and have the dependency bot propose base image updates with the full version or digest.
- Put the runtime’s end-of-life date in the team’s operations calendar, with a named owner, at least six months ahead.
- Run the Node.js Alpha or next Current line in a non-blocking CI job, so the next major upgrade shows its problems early.
When Deno or Bun is the better choice
- Choose Bun for new tools and services where fast installs, fast tests and a single binary matter, where the team is comfortable upgrading often, and where isolation comes from the container anyway. Command line tools distributed as single executables are a particularly good fit. For an existing service, check the compatibility page against your Step 2 list first.
- Choose or keep Deno for scripts and services that benefit from deny-by-default permissions and built-in tooling, especially where code from many authors runs with narrow grants. Teams already on Deno can stay on the LTS channel and watch what happens to the runtime after Cloudflare’s year of support. For a new long-lived service in late 2026, it is hard to recommend starting on a runtime whose maintainers have announced an end date for their own development, unless a community or vendor commitment appears.
- Choose Node.js for existing production services, for anything that depends heavily on native addons, APM agents or low-level Node APIs, and for teams that want a predictable support window from a foundation-hosted project.
When not to change at all
- You are on an end-of-life runtime. Upgrade within the same runtime first. Combining “unsupported” with “new runtime” doubles the risk.
- The slowness is in the database or the network. A faster JavaScript engine does not make a slow query fast.
- Your monitoring depends on Node internals. Losing traces during a migration is worse than a few milliseconds of latency.
- Nobody will own frequent upgrades. Bun’s single supported line and Deno’s short LTS lines need regular attention.
- The reason is a benchmark from a vendor. Vendor numbers are the vendor’s own runs on the vendor’s chosen workload. Measure your own service.
A short history, so the choices make sense
Ryan Dahl introduced Node.js in 2009: V8 from Chrome, an event loop, and non-blocking I/O for servers. It grew quickly with npm. Disagreements over governance and release pace led to the io.js fork in late 2014, which merged back into Node.js under the new Node.js Foundation in 2015, together with the semver and LTS model that the project used for a decade. In 2019 the Node.js Foundation and the JS Foundation merged to form the OpenJS Foundation. In 2026 the project announced its move to one major release a year from Node.js 27.
In 2018 Ryan Dahl gave a talk about design regrets in Node.js and introduced Deno as a fresh start: secure by default, TypeScript, web standards and ES modules. Deno 1.0 came out in 2020. Deno 2.0, in October 2024, embraced npm and Node compatibility, because users wanted Deno’s tooling without leaving the ecosystem. In October 2026 the team announced it was joining Cloudflare.
Jarred Sumner started Bun in 2021, beginning with a port of esbuild’s transpiler to Zig and embedding JavaScriptCore. Bun 0.1 was released in July 2022 and 1.0 in September 2023, with Windows support in 1.1. Anthropic acquired Bun in December 2025, and Bun 1.4 in August 2026 was the first release written in Rust.
The histories explain the designs. Node.js carries compatibility for a huge ecosystem and moves carefully. Deno started from security and standards, then added compatibility. Bun started from speed and an all-in-one toolkit, then chased compatibility test by test.
Operational costs to plan for
- Node.js: a major upgrade every one or two years, native addon rebuilds, and reading security release notes a few times a year. The cost is predictable.
- Deno: scoping permissions for every service and keeping them current as code changes, LTS channel upgrades every few months, and now, a plan for what happens after the runtime’s announced support year.
- Bun: frequent upgrades because only the latest release is supported, regression testing after each one, and verifying that monitoring and TLS behave as expected. The 1.4 rewrite to Rust is recent; give it time in your own environment.
- All three: base image updates,
tscin CI for type checking, lockfile reviews, and someone who owns the runtime as a dependency.
A decision guide
- Existing Node.js service on an end-of-life version: upgrade Node.js to 26 LTS (or 24 as fallback), then reconsider.
- Existing Node.js service on a supported version, working well: stay, add permission flags in audit mode and fix build caching.
- New internal tool or CLI, team happy to upgrade often: pilot Bun.
- Existing Deno service: stay on Deno LTS, follow the runtime’s future closely, and keep the code close to web and Node APIs so a move stays possible.
- New long-lived production service in late 2026: Node.js unless you have a specific, measured reason for Bun.
- Need to run untrusted code: none of the runtime permission models is enough. Use a real sandbox: separate processes, containers, gVisor or Firecracker, or WebAssembly with strict limits.
Checklist before you move another service off Node.js
- The current runtime version is supported, and its end-of-life date is in the calendar.
- A written list of native addons, low-level Node APIs and TypeScript syntax that affect portability.
- Measured numbers for install, build, test and request latency on your own service, before and after.
- Monitoring checked on the new runtime: traces, logs with request IDs, metrics, heap and CPU profiles.
- TLS tested against every external partner and database you connect to.
- Type checking with
tsc --noEmitin CI, whatever the runtime does at run time. - Package manager change treated as a separate, reviewed change with its own lockfile diff.
- Container isolation in place: non-root user, read-only root file system, narrow secret mounts and network policy.
- A named owner for runtime upgrades and security releases.
Common mistakes
- Treating permission flags as a sandbox. Node’s documentation says so plainly, and all three runtimes, including Deno, fixed permission bypasses in 2026.
- Switching runtime and package manager together. When something breaks, you will not know which change caused it.
- Dropping
tscbecause the runtime runs TypeScript. Running and type-checking are different jobs. Only Deno does both. - Trusting
--permissionwhile granting--allow-addons. Native code is outside the model. - Assuming APM works because the app starts. Check traces on the new runtime before production traffic.
- Reading a passing unit test suite as compatibility. The differences live in TLS, streams, child processes and hooks, which unit tests seldom touch.
- Letting the runtime age silently. Node.js 20 reached end of life on 30 April 2026. Many images built in 2024 still use it.
- Comparing vendor benchmarks. Each vendor picks its workload. Your workload is the only one that matters.
A small hands-on drill
We ran a small local drill on 27 October 2026, between about 2:21 AM and 2:26 AM IST, on a Linux 6.12 x86_64 test machine, using only the loopback interface. We downloaded the official Node.js 26.12.1, Deno 2.9.8 and Bun 1.4.4 Linux x86_64 release files and checked each against its published SHA-256 checksum. No npm packages were installed, no container images were pulled, and no benchmark was run.
One script, using only node:fs, node:child_process and node:net, tried six things: read a config file inside the project, read /etc/hostname, write a file in the project, read the HOME environment variable, spawn /bin/true, and open a TCP connection to a small listener on 127.0.0.1.
What we saw:
- Without flags, Node.js and Bun allowed all six. Deno denied all six with
NotCapable, including the environment variable. node --permissionwith no grants denied file reads (even the project config file), the write, the spawn and the TCP connection withERR_ACCESS_DENIED. It still allowed readingHOME, because the Node.js model does not cover environment variables.- With grants for the project directory, one output file and
--allow-net, Node.js allowed exactly those and still denied/etc/hostnameand the spawn. It printed “ExperimentalWarning: The flag –allow-net is under experimental phase.” **node --permission-audit**allowed everything and printed nothing by default. With adiagnostics_channelsubscriber onnode:permission-model:fs, reading/etc/hostnamepublished a message naming the permission and the path, and the read still succeeded.- Deno with scoped grants (
--allow-read=./data,--allow-write=./out.txt,--allow-net=127.0.0.1:<port>) allowed exactly those. Adding--allow-env=HOMEand--allow-run=/bin/truestill did not letnode:child_processspawn/bin/true; Deno asked for environment access. Only with full--allow-envdid the spawn work. - Bun accepted
--permissionand--allow-fs-readsilently and still allowed all six. Its help output lists no permission flags.
Two smaller checks:
- A TypeScript file with an interface and type annotations ran on all three. A file with an
enumfailed on Node.js 26.12.1 withERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(“TypeScript enum is not supported in strip-only mode”) and ran on Deno and Bun. - A small hooks script found that
AsyncLocalStoragekept a request ID acrossawaiton all three. Anasync_hooks.createHookinit hook fired on Node.js and Deno but not on Bun, andperformance.eventLoopUtilization()reported zero utilisation after a 50 ms busy loop on Deno and Bun, and a non-zero value on Node.js. The Bun result matches what Bun’s compatibility page documents.
This is not a benchmark. It shows three practical points. Permission flags behave very differently across the three, and a flag that one runtime ignores silently gives a false sense of safety. Node’s type stripping needs code without enum and similar syntax. And monitoring that depends on low-level hooks needs testing on the target runtime before a move.
Unlearn, re-learn
Unlearn the idea that Node.js is the slow, unsafe default you must escape. In 2026 it runs TypeScript directly, has a stable test runner and permission model, and offers the longest published support window of the three. Unlearn, too, the idea that a runtime’s permission flags make hostile code safe; every runtime here fixed bypasses this year, and Bun has no permission model at all. Re-learn the runtime as a dependency with an end-of-life date, a security release rhythm and an owner. Upgrade within the runtime first, measure your own service, test your monitoring and TLS on any candidate, and switch only when the reason is still there after those steps. Deno and Bun are both serious, useful projects. Choose them for the work they fit, with clear eyes about support windows and ownership, not because of a chart.
Sources
- Node.js release schedule (
schedule.json), release index, previous releases and Evolving the Node.js release schedule - Node.js security releases: January 2026, March 2026, June 2026, July 2026 and October 2026
- Node.js documentation: permissions, TypeScript, test runner and CLI options
- Node.js repository and OpenJS Foundation projects
- npm CLI changelog
- Deno repository, releases, security advisories, stability and releases, security and permissions and Node and npm compatibility
- Deno blog: Deno 2.9 and Deno is joining Cloudflare
- Bun repository, releases, security advisories, licence, security policy and Node.js compatibility
- Bun blog: Bun 1.4, Bun 1.4.4, Rewriting Bun in Rust and Bun is joining Anthropic
- Official container images: node, oven/bun and denoland/deno
Releted Posts
Self-hosted Git forges in 2026: revisit Gitea, Forgejo and GitLab CE before you move another repository
Picture a software company in Kochi that builds billing and export-documentation software for seafood exporters in Aroor, Alappuzha, Kollam and Thoothukudi (an invented example, not a real client).
Read moreContainer registries in 2026: revisit Harbor, Zot and CNCF Distribution before you push another image
Picture a product engineering team in Bengaluru that runs about forty microservices on Kubernetes for a B2B payments product used by NBFCs across Karnataka and Maharashtra (an invented example, not a real client).
Read moreAuthorization in 2026: revisit OpenFGA, SpiceDB and Cerbos before you hard-code another permission check
Picture a SaaS company in Nagpur that sells loan-origination software to cooperative banks across Vidarbha and Marathwada (an invented example, not a real client).
Read more