cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Inside Deno, Bun & edge runtimes

Compare Deno, Bun, and edge runtimes through engines, host APIs, V8 isolates, common web APIs, and cold-start models.

By the end, you can
  • 01
    Separate language from runtimeName the engine, host APIs, and native layers that make Deno, Bun, Node, and edge platforms different.
  • 02
    Write portable handlersUse Request, Response, URL, fetch, streams, and feature detection without assuming one runtime owns JavaScript.
  • 03
    Reason 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.

Definition

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.

A small runtime-agnostic handlerPop out in the code editor (opens in a new tab)JavaScript
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.

Step through the portable handler
Step 0 of 5Ready
Your turn: follow the blue line

Replay a real call to the lesson handler. This is instrumented example code, not a runtime debugger.

Running in
  1. script
Next: line 7
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
async function handle(request) {  const url = new URL(request.url);  return new Response(`Hello from ${url.pathname}`);} (async () => {  console.log(await response.text());})();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Use a standard response, not a Deno-only APIPop out in the code editor (opens in a new tab)JavaScript
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.

Runtime layers are easy to mix up
NameEngine or host shapeWhat code notices
DenoV8 with Rust operations and the Tokio async runtimeWeb APIs plus Deno-specific APIs and permissions
BunJavaScriptCore with native runtime layersWeb APIs, Node compatibility, and Bun-specific APIs
Edge platformOften a V8-isolate host, but platform details varyA focused server API surface and platform bindings
WinterTCAn Ecma TC55 interoperability effort, not a runtimeA 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.

The shared handler result stays ordinary JavaScriptPop out in the code editor (opens in a new tab)JavaScript
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.

Real-life analogyThe same recipe in different kitchens

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.

A cautious runtime detectorPop out in the code editor (opens in a new tab)JavaScript
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.

A small separate-global teaching modelPop out in the code editor (opens in a new tab)JavaScript
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.

Step through the portable handler
Step 0 of 5Ready
Your turn: follow the blue line

Replay a real call to the lesson handler. This is instrumented example code, not a runtime debugger.

Running in
  1. script
Next: line 7
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
async function handle(request) {  const url = new URL(request.url);  return new Response(`Hello from ${url.pathname}`);} (async () => {  console.log(await response.text());})();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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 durable services for shared state

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.

Use a few common web-style APIsPop out in the code editor (opens in a new tab)JavaScript
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.

Report a missing portable API clearlyPop out in the code editor (opens in a new tab)JavaScript
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.

Language, common API, or runtime extra?
  • Promise and async functions
  • Request passed to a handler
  • new Response("ok")
  • The Deno global
  • The Bun global
  • TextEncoder
Try it yourself
0 of 6 correct

Sort the lesson cards by what supplies each capability.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.

A clearly illustrative cold-start modelPop out in the code editor (opens in a new tab)JavaScript
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.

Step through a cold-start teaching model
Step 0 of 7Ready
Your turn: follow the blue line

Replay a small, explicitly illustrative warm-reuse model. Platforms choose their own reuse and eviction behavior.

Running in
  1. script
Next: line 11
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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;} 
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Playground: change the request gap
Cold-start modelJavaScript
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));
Illustrative result1 cold
request times0, 15, 30, 45

Four invented arrival times, spaced by your one control.

warm window20 ms

Any larger gap counts as cold in this model.

cold starts1

This number explains the model only, not a vendor performance result.

Try it yourself

With a 15 ms made-up gap, the model counts 1 cold start. A real platform chooses its own reuse and eviction policy.

This playground counts model events. It does not benchmark Deno, Bun, Node, or an edge provider.
Real-life analogyFood trucks near customers

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.
Terms that should stay separate
TermWhat it meansWhat it is not
IsolateA lightweight V8 execution context with its own memoryA new operating-system process per request
Global stateMay be reused or evicted by the platformA durable database or guaranteed request store
Cold startWork needed before a fresh execution environment can handle workOne universal fixed number
Warm reuseA platform may reuse an existing environmentA promise that every later request sees the same global state

Practice exercises

Exercise 1 · Warm-upPredict a portable handler

What does the handler print for the cart URL?

Starter codePop out in the code editor (opens in a new tab)JavaScript
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());
})();

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upName the Node detector result

    What label does this detector print under Node?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const runtime = globalThis.process?.versions?.node ? "node" : "unknown";
    console.log(runtime);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeChoose the portable contract

      You are moving a URL handler between environments. Which API pair should shape its input and output?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeStore a real order safely

        A checkout order must survive routing to another isolate. Where should it live?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengePredict the teaching model

          How many cold starts does the illustrative model count?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          console.log(countColdStarts([0, 5, 10, 50], 20));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a production feature

            Your app needs one runtime-specific API. What should you feature-detect before using it?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Use the handler's code, the engine/runtime boundary, and the cold-start model to test each answer.

              Deno, Bun, and edge runtimes quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the portable handler print for https://shop.example/cart?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                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());
                })();

                Choose an answer to see the explanation.

              2. Question 2 of 7Which statement correctly separates an engine from a runtime?

                Choose an answer to see the explanation.

              3. Question 3 of 7What does the lesson detector return in Node when process.versions.node exists?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does Cloudflare document about V8 isolates in Workers?

                Choose an answer to see the explanation.

              5. Question 5 of 7What is WinterTC / Ecma TC55 trying to standardize?

                Choose an answer to see the explanation.

              6. 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.

              7. 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.

              CompleteFrontend Clear concepts. Working examples.