Inside Deno, Bun & edge runtimes
Compare Deno, Bun, and edge runtimes through engines, host APIs, V8 isolates, common web APIs, and cold-start models.
- 01Separate language from runtimeName the engine, host APIs, and native layers that make Deno, Bun, Node, and edge platforms different.
- 02Write portable handlersUse Request, Response, URL, fetch, streams, and feature detection without assuming one runtime owns JavaScript.
- 03Reason about edge executionExplain isolated globals, platform-managed reuse, and cold-start trade-offs without treating a teaching model as a benchmark.
One language, different runtimes
JavaScript is the recipe. A runtime is the kitchen that runs that recipe and adds a timer, network access, files, modules, and a process model. Deno, Bun, Node, and edge platforms can all run JavaScript while making different choices around the language.
A JavaScript engine executes the language. A runtime combines an engine with host APIs, native code, scheduling, modules, and deployment choices. That is why the same JavaScript can behave differently around files, globals, and startup.
This lesson uses small web-style handlers because they make the shared part visible. It then names the extra layers accurately: Deno uses V8 and Rust with Tokio; Bun uses JavaScriptCore and native layers; many edge systems use V8 isolates. The details are not interchangeable.
The previous Node.js architecture lesson explains one V8-based server runtime. This lesson compares choices around it, and Embedding V8 explains the isolate vocabulary underneath.
Start with portable web APIs
A server handler often needs only a request URL and a response body. The example below works in a modern browser sandbox and Node because it uses the web-style Request, Response, and URL objects instead of a runtime-only namespace.
async function handle(request) { const url = new URL(request.url); return new Response(`Hello from ${url.pathname}`);} (async () => { const response = await handle(new Request("https://shop.example/cart")); console.log(await response.text());})();Line 1 names the handler and receives one request. Line 2 creates a URL from request.url. Line 3 returns a response whose body includes the pathname. Lines 7 and 8 call it and print Hello from /cart.
Replay a real call to the lesson handler. This is instrumented example code, not a runtime debugger.
script
async function handle(request) { const url = new URL(request.url); return new Response(`Hello from ${url.pathname}`);} (async () => { console.log(await response.text());})();This does not mean every runtime has every browser API. It means small pieces of code can target a common shape first. Keep file access, database clients, and provider bindings behind a small adapter when they are not part of that shape.
Deno: V8, Rust, and Tokio
Deno's official architecture describes V8 as its JavaScript engine, Rust operations around the engine, and Tokio as the scheduler for asynchronous work. Its documentation also presents Deno as a JavaScript, TypeScript, and WebAssembly runtime with secure defaults.
A secure default is a host policy, not a new JavaScript syntax rule. Deno documentation says code has no file, network, or environment access until permission is granted. Your handler can still use ordinary JavaScript objects, promises, and web APIs.
const path = "/cart";console.log(new Response(`Saved ${path}`).status);Line 1 stores a path. Line 2 creates a standard response and prints 200. The status is the normal default for a new response, so this tiny part does not need the Deno global.
| Name | Engine or host shape | What code notices |
|---|---|---|
| Deno | V8 with Rust operations and the Tokio async runtime | Web APIs plus Deno-specific APIs and permissions |
| Bun | JavaScriptCore with native runtime layers | Web APIs, Node compatibility, and Bun-specific APIs |
| Edge platform | Often a V8-isolate host, but platform details vary | A focused server API surface and platform bindings |
| WinterTC | An Ecma TC55 interoperability effort, not a runtime | A minimum common API target for server runtimes |
Bun: JavaScriptCore and native layers
Bun's runtime documentation says Bun uses Apple's JavaScriptCore engine, the engine used by Safari. JavaScriptCore is a different engine from V8, yet both execute JavaScript and can expose web-style APIs such as fetch, Request, and Response.
The curriculum label mentions Zig because Bun was originally known for its Zig implementation. Current official Bun runtime documentation describes its transpiler and runtime as written in Rust. This lesson follows the current runtime documentation rather than freezing an old detail.
const order = { item: "tea" };console.log(order.item);Line 1 creates an ordinary object. Line 2 prints tea. Engines differ in implementation, but this language behavior is the same JavaScript idea that portable application code should lean on.
One recipe can be cooked in different kitchens. The food can be recognizably the same, while each kitchen has different stoves and tools.
- In real life: One recipe
- In JavaScript: JavaScript language
- In real life: A V8 stove in one kitchen
- In JavaScript: Deno's engine choice
- In real life: A JavaScriptCore stove in another
- In JavaScript: Bun's engine choice
- In real life: Kitchen tools
- In JavaScript: Runtime APIs and native layers
Where the analogy stops: A runtime is more complex than a kitchen. This comparison only separates shared JavaScript from each host's choices.
Detect a runtime carefully
Portable code should not guess a runtime from an operating system name or a user agent. When you truly need a runtime-only capability, test for the specific global or API you plan to use, then keep that branch small.
const runtime = typeof Deno !== "undefined" ? "deno" : typeof Bun !== "undefined" ? "bun" : globalThis.process?.versions?.node ? "node" : "unknown"; console.log(runtime);Lines 1 through 5 choose a label in order: Deno, Bun, Node, then unknown. Under Node, the third condition reads the version field and the snippet prints node. The label is useful for diagnostics; capability detection is better for a real feature.
For example, do not branch merely because the answer is Bun. Test that the API you need exists, document its fallback, and leave the portable handler free of that branch. This makes later migrations less surprising.
V8 isolates at the edge
An isolate is a lightweight V8 execution context with its own memory and globals. Cloudflare documents that one Workers runtime instance can run many isolates, and that each isolate's memory is isolated from other code in that runtime.
That isolation is useful for running many pieces of untrusted or separate application code. It does not turn a module-level variable into durable storage. Cloudflare also documents that isolates can be evicted, and requests are not guaranteed to reach the same Worker instance.
function createStation(name) { return { name, globals: { orders: 0 } };} function serve(station) { station.globals.orders += 1; return station.name + ": " + station.globals.orders;} const first = createStation("first");const second = createStation("second");console.log(serve(first));console.log(serve(second));Line 1 creates a model station with its own orders global. Line 5 increments that station only. Lines 9 and 10 create two stations, and lines 11 and 12 print first: 1 then second: 1. It is a teaching model, not a V8 implementation.
Read Embedding V8 for engine-level isolate and context terminology. Here the practical rule is simpler: pass state through requests or durable services, not through a hope that one isolate will remain alive.
Separate globals in a teaching model
The handler replay uses real lesson code and follows a request from URL to response. It does not pause Deno, Bun, Node, or an edge provider. Its purpose is to make the portable boundary visible before the runtime begins routing real traffic.
Replay a real call to the lesson handler. This is instrumented example code, not a runtime debugger.
script
async function handle(request) { const url = new URL(request.url); return new Response(`Hello from ${url.pathname}`);} (async () => { console.log(await response.text());})();Notice what the replay does not show: an engine heap, a provider scheduler, or a promise about reuse. Those are runtime and platform details. The portable contract remains the request in and response out.
Use a database, cache service, queue, or platform-provided durable primitive when an order must survive another request or another isolate.
WinterTC and common runtime APIs
WinterTC, also called Ecma TC55, is a standards committee for web-interoperable server runtimes. Its scope is a minimum common API: a curated base of web-standard interfaces that server environments can support together.
In daily code, think of the shared minimum as familiar building blocks: fetch, Request, Response, URL, TextEncoder, crypto.randomUUID, timers, and streams. A runtime still decides versions, limits, and additional APIs.
const commonApis = [ ["fetch", typeof fetch], ["Request", typeof Request], ["Response", typeof Response], ["URL", typeof URL], ["TextEncoder", typeof TextEncoder], ["crypto.randomUUID", typeof crypto.randomUUID],]; console.log(commonApis.map(([name, type]) => name + ": " + type).join(", "));Lines 1 through 8 list the portable names and inspect each one. Line 10 prints fetch: function, Request: function, Response: function, URL: function, TextEncoder: function, crypto.randomUUID: function when all six are available. This is a capability check, not a promise that every runtime has the same limits or extra APIs.
const commonApis = { fetch, Request, Response, URL, TextEncoder, "crypto.randomUUID": crypto.randomUUID,}; const missing = Object.entries(commonApis) .filter(([, value]) => typeof value !== "function") .map(([name]) => name); console.log(missing.length === 0 ? "portable base available" : "missing: " + missing.join(", "));Lines 1 through 8 gather the functions a handler wants. Lines 10 and 11 collect only missing names. Line 14 prints portable base available when all are present. In a real adapter, use that result to choose a fallback or state a deployment requirement.
PromiseandasyncfunctionsRequestpassed to a handlernew Response("ok")- The
Denoglobal - The
Bunglobal TextEncoder
Sort the lesson cards by what supplies each capability.
Cold starts and warm reuse
A cold start is the work a platform performs before a fresh execution environment handles work. The exact work and timing depend on the provider, deployed code, engine, cache state, and platform policy. Do not turn a number from one benchmark into a universal rule.
Edge platforms often use isolates rather than a whole new container for every request. Cloudflare documents isolates as lightweight contexts created inside an existing environment. That explains the design goal, but it does not promise one timing for every edge platform or deployment.
function countColdStarts(requestTimes, warmForMs) { let lastRequest = null; let coldStarts = 0; for (const time of requestTimes) { if (lastRequest === null || time - lastRequest > warmForMs) coldStarts += 1; lastRequest = time; } return coldStarts;} console.log(countColdStarts([0, 5, 10, 50], 20));Lines 1 through 9 count a cold start for the first request or for a gap larger than a made-up warm window. Line 11 prints 2 for the invented request times. The number teaches the decision rule only; it is not a Deno, Bun, or edge benchmark.
Replay a small, explicitly illustrative warm-reuse model. Platforms choose their own reuse and eviction behavior.
script
function countColdStarts(requestTimes, warmForMs) { let lastRequest = null; let coldStarts = 0; for (const time of requestTimes) { if (lastRequest === null || time - lastRequest > warmForMs) coldStarts += 1; lastRequest = time; } return coldStarts;} function countColdStarts(requestTimes, warmForMs) { let lastRequest = null; let coldStarts = 0; for (const time of requestTimes) { if (lastRequest === null || time - lastRequest > warmForMs) coldStarts += 1; lastRequest = time; } return coldStarts;} console.log(countColdStarts([0, 5, 10, 50], 20));0, 15, 30, 45Four invented arrival times, spaced by your one control.
20 msAny larger gap counts as cold in this model.
1This number explains the model only, not a vendor performance result.
With a 15 ms made-up gap, the model counts 1 cold start. A real platform chooses its own reuse and eviction policy.
A food truck can have several small cooking stations ready near customers. Lighting a cold stove takes preparation; a station that is already ready can start the next order sooner.
- In real life: Small cooking stations in one truck
- In JavaScript: Many isolates in one runtime
- In real life: A station serves a nearby order
- In JavaScript: One request executes in one environment
- In real life: A stove needs lighting first
- In JavaScript: A cold start prepares a fresh environment
- In real life: A ready station serves another order
- In JavaScript: Warm reuse may avoid preparation
Where the analogy stops: Platforms choose whether to reuse or remove environments. This analogy does not describe a provider's exact scheduler or timings.
Choose APIs for portability
Start a server feature with a standard handler shape when it fits: receive a request, parse a URL, fetch a service, and return a response. This lets you test most behavior without binding the whole application to one runtime on day one.
Then make runtime-specific code explicit. A Deno permission choice, a Bun namespace API, a Node built-in module, and an edge provider binding are all reasonable when needed. Place each behind a named adapter with a clear fallback or deployment requirement.
- Prefer
Request,Response,URL,fetch, streams, and encoders for shared server code. - Feature-detect the exact optional API before calling it.
- Keep durable data outside isolate globals.
- Measure cold-start behavior in the target deployment instead of quoting generic numbers.
Common runtime misconceptions
It is tempting to use short labels such as “V8 runtime” or “edge is fast” as complete explanations. They are not. An engine, runtime, and hosted deployment each own different parts of the behavior a developer sees.
- “JavaScript requires V8.” Bun demonstrates that JavaScript can run on JavaScriptCore.
- “An isolate is durable storage.” Isolates can be reused or evicted; durable state belongs in a durable service.
- “WinterTC removes all runtime differences.” It aims for a common base, while runtimes keep extras and policies.
- “Cold start has one published number.” It depends on the platform and deployment, so measure the actual target.
| Term | What it means | What it is not |
|---|---|---|
| Isolate | A lightweight V8 execution context with its own memory | A new operating-system process per request |
| Global state | May be reused or evicted by the platform | A durable database or guaranteed request store |
| Cold start | Work needed before a fresh execution environment can handle work | One universal fixed number |
| Warm reuse | A platform may reuse an existing environment | A promise that every later request sees the same global state |
Practice exercises
What does the handler print for the cart URL?
async function handle(request) {
return new Response(new URL(request.url).pathname);
}
(async () => {
const response = await handle(new Request("https://shop.example/cart"));
console.log(await response.text());
})();The program prints /cart. new URL(request.url).pathname extracts the part after the host.
What label does this detector print under Node?
const runtime = globalThis.process?.versions?.node ? "node" : "unknown";
console.log(runtime);It prints node, because the Node version field exists in Node.
You are moving a URL handler between environments. Which API pair should shape its input and output?
Use Request and Response. They describe the portable handler boundary in this lesson.
A checkout order must survive routing to another isolate. Where should it live?
Use a database or other durable storage. A module global is not a cross-request durability contract.
How many cold starts does the illustrative model count?
console.log(countColdStarts([0, 5, 10, 50], 20));The model prints 2: once at time 0 and once after the large gap before time 50.
Your app needs one runtime-specific API. What should you feature-detect before using it?
Feature-detect the runtime API, such as the Deno or Bun capability you need. Keep that branch small and documented.
Check your understanding
Use the handler's code, the engine/runtime boundary, and the cold-start model to test each answer.
Question 1 of 7What does the portable handler print for
https://shop.example/cart?Read the code, then predictasync function handle(request) { return new Response(new URL(request.url).pathname); } (async () => { const response = await handle(new Request("https://shop.example/cart")); console.log(await response.text()); })();Choose an answer to see the explanation.
Question 2 of 7Which statement correctly separates an engine from a runtime?
Choose an answer to see the explanation.
Question 3 of 7What does the lesson detector return in Node when
process.versions.nodeexists?Choose an answer to see the explanation.
Question 4 of 7What does Cloudflare document about V8 isolates in Workers?
Choose an answer to see the explanation.
Question 5 of 7What is WinterTC / Ecma TC55 trying to standardize?
Choose an answer to see the explanation.
Question 6 of 7In the cold-start teaching model, why is the request at 50 cold after 10 when warmForMs is 20?
Choose an answer to see the explanation.
Question 7 of 7Where should an edge handler keep an order that must survive routing and eviction?
Choose an answer to see the explanation.
Key takeaways
- JavaScript is the language; engines execute it; runtimes add host APIs and deployment choices.
- Deno uses V8 with Rust and Tokio, while Bun uses JavaScriptCore with native runtime layers.
- Request, Response, URL, fetch, streams, and encoders make a useful portable server baseline.
- Edge isolates have separate memory and globals, but platform reuse and eviction mean globals are not durable storage.
- WinterTC / Ecma TC55 works toward a minimum common API for server runtimes.
- Cold-start numbers must be measured in the target platform; the lesson model only explains warm reuse.
Remember the one-liner.
Write to the common web API surface first, then isolate each runtime or platform extra.
Coming next: Small & embedded engines, where JavaScript engines trade memory, startup, and language completeness for different devices.