Inside Deno, Bun & edge runtimes
Compare Deno, Bun, and edge runtimes through engines, host APIs, V8 isolates, common web APIs, and cold-start models.
- 01Tell the language from the runtimeName the engine and the extra tools that make Deno, Bun, Node, and edge platforms different.
- 02Write handlers that run anywhereUse Request, Response, URL, fetch, and streams, and check that a tool exists before you use it.
- 03Explain how code runs at the edgeExplain why each isolate has its own globals, why data there may not last, and what a cold start is.
One language, different runtimes
JavaScript is a language. On its own, it can't start a timer, read a file, or talk to the network. A runtime is the program that runs your JavaScript and gives it those extra tools.
Deno, Bun, and Node are three runtimes. Edge platforms, which run your code on servers close to your users, are runtimes too. They all run the same language. Each one makes its own choices about the extra tools.
A JavaScript engine is the part that reads and runs the language itself. A runtime is an engine plus extra tools: ways to reach files and the network, a way to load modules, and a way to schedule work for later. That is why the same JavaScript can act differently when it touches files, global names, or startup.
app.js can run in Deno, Bun, and Node. Each runtime has an engine inside: Deno and Node use V8, and Bun uses JavaScriptCore.This lesson uses small request handlers. A handler is a function that gets one web request and gives back one answer. Handlers show the part every runtime shares. Then the lesson names what is different in each one:
- Deno uses the V8 engine, plus code written in Rust.
- Bun uses the JavaScriptCore engine, plus its own native code.
- Many edge platforms run code in V8 isolates (explained below).
The previous Node.js architecture lesson looked inside one runtime built on V8. This lesson compares other runtimes with it. The Embedding V8 lesson explains isolates in more depth.
Start with web tools every runtime shares
A server handler often needs only two things: the web address that was asked for, and some text to send back. This example uses three tools that browsers also have:
Requestholds one incoming request, including its address inrequest.url.URLsplits an address into parts, such as the path/cart.Responseholds the answer you send back.
Because the code uses only these shared tools, and no tool that belongs to one runtime, it runs in a browser and in Node without changes. Code like this is called portable.
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 handle. It gets one request. The word async means it gives back a promise, and line 7 waits for that promise with await. Line 2 makes a URL from request.url. Line 3 sends back a response. Its text includes the path. Line 7 makes a request for https://shop.example/cart and calls the handler. Line 8 reads the response text and prints Hello from /cart.
Request for /cart goes in, and handle() gives back a Response. Reading the response body prints Hello from /cart.These steps replay the lesson's handler, one line at a time. The values were rebuilt from the example. They were not read from a running runtime.
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 tool. It means small pieces of code can use the shared tools first. Some code needs more, such as reading files, talking to a database, or using a tool only one platform offers. Put that code in a small separate function, called an adapter, so the rest of the handler stays portable.
Deno: V8, Rust, and Tokio
Deno's own documents describe three main parts:
- V8 is the engine. It runs your JavaScript. Chrome and Node use it too.
- Rust is the language Deno's extra tools are written in, such as reading files.
- Tokio is a Rust library that keeps track of slow work, like network requests, and runs the next step when each one finishes.
Deno runs JavaScript, TypeScript, and WebAssembly. It is also careful by default. A permission is a yes or no you give when you start Deno. Your code needs permission before it can read files, use the network, or read environment variables. Loading the modules your code imports is the one thing that needs no permission. Give only what the code needs, such as one network address.
Permissions are a rule of the runtime, not of the language. Your handler is still ordinary JavaScript that uses the shared web tools.
Deno 2 can also run a lot of code written for Node:
- Node's own modules, with imports that start with
node:. - npm packages, with imports that start with
npm:, or listed inpackage.json.
Not everything works. Some npm packages include compiled native code, called native addons. Those need a local node_modules folder and an extra permission. npm code also has to follow Deno's permissions.
const path = "/cart";console.log(new Response(`Saved ${path}`).status);Line 1 stores a path. Line 2 makes a shared Response and prints its status code, 200. A status code is a number that says how the request went, and 200 means "OK". A new response gets 200 unless you choose another one. This code works in Deno, but it does not use Deno. That is a global, a name you can use anywhere without importing it, and only Deno has it. So this code works in other runtimes too.
The table compares the runtimes. Read each row as: the name, what it is built from, and what your code can see.
| Name | What is it built from? | What does your code see? |
|---|---|---|
| Deno | The V8 engine, with extra tools written in Rust and the Tokio library. | The shared web tools, plus the Deno global and permissions. |
| Bun | The JavaScriptCore engine, with its own native code. | The shared web tools, many Node modules, and the Bun global. |
| Edge platform | Often V8 isolates, but each platform is different. | A small set of server tools, plus tools that only that platform has. |
| WinterTC | Not a runtime. It is a standards group, also called Ecma TC55. | A short list of web tools that every server runtime can offer. |
Bun: JavaScriptCore and native code
Bun's documents say Bun uses JavaScriptCore as its engine. JavaScriptCore is Apple's engine, the one inside Safari. It is a different engine from V8. Both engines run the same JavaScript. Both runtimes also offer the shared web tools, such as fetch, Request, and Response.
You may read that Bun is written in Zig, another programming language. Bun was first known for that. Bun's current documents say its runtime is written in Rust. This lesson follows the current documents.
Bun also comes with two more tools: a bundler, which joins many files into one, and a test runner. Bun is still working on running all code written for Node. Before you move a Node app to Bun, check that each Node module and package your app uses works there.
const order = { item: "tea" };console.log(order.item);Line 1 makes an ordinary object. Line 2 prints tea. V8 and JavaScriptCore are built differently on the inside, but this code means the same thing in both. Code that sticks to plain language features like this moves between runtimes easily.
You can cook one recipe in two kitchens. Each kitchen has a different stove and different tools. The food still comes out the same.
- In real life: One recipe
- In JavaScript: The JavaScript language
- In real life: A V8 stove in one kitchen
- In JavaScript: The engine Deno uses
- In real life: A JavaScriptCore stove in another
- In JavaScript: The engine Bun uses
- In real life: The kitchen tools around the stove
- In JavaScript: The runtime's extra tools
Find out which runtime you are in
Don't guess the runtime from the name of the operating system or from the browser's name. Sometimes you really do need a tool that only one runtime has. Then check that the exact tool you plan to use is there. Keep the code that uses it short.
The example below uses three tools you may not know yet:
typeof Denogives the type ofDenoas text. If no such name exists, it gives"undefined"instead of throwing an error.a ? b : cmeans "ifais true, useb, otherwise usec".?.reads a property only if the thing before it exists. Otherwise it givesundefinedand does not throw.
const runtime = typeof Deno !== "undefined" ? "deno" : typeof Bun !== "undefined" ? "bun" : globalThis.process?.versions?.node ? "node" : "unknown"; console.log(runtime);Lines 1 to 5 pick one name, checking in this order:
- Line 2: if a
Denoglobal exists, the name is"deno". - Line 3: if a
Bunglobal exists, the name is"bun". - Line 4: if
process.versions.nodeexists, the name is"node". Node puts its version number there. - Line 5: if none of these exist, the name is
"unknown".
Line 7 prints the name. In Node, it prints node.
This name is handy in an error report. To use a feature, check for the feature itself instead. For example, don't call a Bun tool just because the name is bun. Check that the tool exists, write down what happens when it doesn't, and keep that check out of the portable handler. Then moving to another runtime later brings fewer surprises.
V8 isolates at the edge
An edge platform runs your code on many servers around the world, so each user reaches one close by. Cloudflare Workers is one example.
An isolate is a small, separate space inside V8 where one piece of code runs. It has its own memory and its own global names. Cloudflare's documents say:
- One Workers runtime can run many isolates at once.
- The memory of each isolate is kept apart from all other code in that runtime.
Keeping code apart like this is useful. Many apps, from many different people, can run side by side safely. But an isolate is not a safe place to keep data. Cloudflare also says:
- The platform can shut down an isolate at any time.
- Two requests may not reach the same isolate.
So a variable at the top of your module can vanish, or another request may never see it.
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));This code is a model. It is not how V8 works inside. Each "station" stands for one isolate.
- Line 1 makes a station. Each station gets its own
globalsobject with anorderscount of 0. - Line 5 starts
serve. Line 6 adds 1 to the count of that one station only. - Lines 10 and 11 make two stations,
firstandsecond. - Lines 12 and 13 serve each station once. They print
first: 1, thensecond: 1.
Serving first did not change the count in second. Each station keeps its own globals, just like each isolate does.
orders count, so serving each one once prints first: 1 and second: 1.The Embedding V8 lesson explains isolates inside the engine. Here the rule is simple: pass data in with each request, or keep it in a storage service. Don't count on one isolate staying alive.
Edge platforms also set limits. Cloudflare Workers counts CPU time, the time your code is actually running, apart from time spent waiting for the network. How much CPU time you get depends on your plan and settings.
Node's modules are not promised on every edge platform. On Cloudflare, each project sets a compatibility date. Projects with a date of 2026-08-04 or later get Node support turned on by default. Projects with an older date can turn it on with the nodejs_compat setting. Some Node modules are only partly there, or can be imported but do nothing. Check the exact module and your project's date before you rely on it.
Step through the handler again
Step through the handler from earlier once more. It follows one request from its address to its response. The steps replay the lesson's own handler code. They do not pause Deno, Bun, Node, or an edge platform. The values you see were rebuilt from the example, not read from a running runtime.
These steps replay the lesson's handler, one line at a time. The values were rebuilt from the example. They were not read from a running runtime.
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 steps do not show:
- the engine's memory,
- how the platform decides which isolate gets the request,
- whether the next request will reach the same isolate.
Those belong to the runtime and the platform. The part your code controls stays the same everywhere: a request goes in, and a response comes out.
Sometimes an order must still be there for the next request, or for another isolate. Keep it in a database, a cache service, a queue, or a storage tool the platform offers. Data kept there lasts. Data kept in an isolate's globals may not.
WinterTC and the shared web tools
WinterTC is a group that writes standards. Its official name is Ecma TC55. It works on server runtimes that use the same tools as the web. Its goal is a short, agreed list of web tools that every server runtime can offer. This list is called the minimum common API. An API is a set of tools your code can call.
In everyday code, the shared tools are ones you may know from the browser:
fetch, to send a request over the network,Request,Response, andURL,TextEncoder, to turn text into bytes,crypto.randomUUID, to make a random id,- timers, such as
setTimeout, - streams, to send or read data in small pieces.
Each runtime still decides its own versions, limits, and extra tools.
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 to 8 make a list. Each item is a pair: a tool's name and its type from typeof. Line 10 joins the pairs into one line of text. When all six tools exist, it prints fetch: function, Request: function, Response: function, URL: function, TextEncoder: function, crypto.randomUUID: function.
This only checks that the tools exist. It does not promise that every runtime has the same limits or the same extra tools.
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 to 8 collect the tools a handler wants, by name.
- Lines 10 to 12 keep only the names whose tool is not a function. Those are the missing ones.
- Line 14 prints
portable base availablewhen nothing is missing. Otherwise it printsmissing:and the missing names.
In a real adapter, use this result to pick a backup plan. Or write down that the app needs a runtime with these tools.
PromiseandasyncfunctionsRequestpassed to a handlernew Response("ok")- The
Denoglobal - The
Bunglobal TextEncoder
Put each card where it comes from: the JavaScript language, the shared web tools, or one runtime or platform.
Cold starts and warm starts
Before new code can answer its first request, the platform has to get a fresh place ready to run it. That setup work is called a cold start. When the platform can reuse a place that is already ready, that is a warm start, and it skips the setup.
How long a cold start takes depends on many things: the platform, your code, the engine, what is already cached, and the platform's own rules. So don't take a number from one test and treat it as true everywhere.
Edge platforms often start a new isolate instead of a whole new container, which is a full, separate copy of a system for your app. Cloudflare says isolates are small and are made inside a runtime that is already running. That is why they can be quick to start. It does not promise one start time for every edge platform or every app.
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));The model uses made-up numbers. A request counts as warm if it comes within a short time of the last one. This time is called the warm window. Here it is 20.
- Line 1 gets a list of request times and the warm window,
warmForMs. - Line 5 counts a cold start when there is no earlier request, or when the gap since the last request is more than the warm window.
- Line 6 remembers this request's time for the next turn.
- Line 11 runs the model with times 0, 5, 10, and 50. It prints
2.
The 2 shows how the rule works. It is not a timing from Deno, Bun, or any edge platform.
These steps follow a small, made-up model of cold and warm starts. A real platform makes its own choices about when to reuse an isolate and when to shut it down.
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 made-up request times. Each one comes one gap after the last.
20A gap of more than 20 counts as cold.
1This count comes from the model. It is not a timing from a real platform.
With a gap of 15 between requests, the model counts 1 cold start. The first request is always cold. Each later one is cold only if its gap is more than 20.
A food truck parks near its customers. Inside are several small cooking stations. Lighting a cold stove takes time. A station that is already hot can start the next order right away.
- In real life: Small cooking stations in one truck
- In JavaScript: Many isolates in one runtime
- In real life: A station cooks one nearby order
- In JavaScript: One request runs in one isolate
- In real life: A cold stove needs lighting first
- In JavaScript: A cold start sets up a fresh isolate
- In real life: A hot station cooks the next order
- In JavaScript: A warm start reuses an isolate
Pick tools that work everywhere
When you start a server feature, begin with a plain handler if it fits. It gets a request, reads the address with URL, maybe calls another service with fetch, and sends back a response. You can test most of this code without tying the whole app to one runtime.
Then make the runtime-only code easy to spot. Each of these is fine to use when you need it:
- a Deno permission,
- a tool from the
Bunglobal, - a Node module,
- a tool that only one edge platform offers.
Put each one in its own named adapter. Next to it, write down the backup plan, or which runtime the app needs.
- For shared server code, use
Request,Response,URL,fetch, streams, andTextEncoder. - Before you call an optional tool, check that it exists.
- Keep data that must last outside isolate globals.
- Measure cold starts on the platform you really use. Don't rely on numbers from somewhere else.
Common mistakes about runtimes
Short phrases like "the V8 runtime" or "the edge is fast" are easy to say, but they skip details. The engine, the runtime, and the platform your app is put on each control different parts of what you see.
- "JavaScript needs V8." No. Bun runs JavaScript on JavaScriptCore.
- "An isolate is a safe place to keep data." No. The platform can reuse an isolate or shut it down. Data that must last belongs in a storage service.
- "WinterTC removes all differences between runtimes." No. It aims for a shared list of tools. Each runtime still keeps its own extra tools and rules.
- "A cold start takes one fixed amount of time." No. It depends on the platform and the app, so measure the one you use.
The table puts four words side by side. Read each row as: the word, what it means, and what people often think it means.
| Word | What does it mean? | What does it not mean? |
|---|---|---|
| Isolate | A small, separate space inside V8 with its own memory. | A new program started by the operating system for each request. |
| Global data | The platform may reuse it, or throw it away. | A database, or a safe place that every request can see. |
| Cold start | The setup needed before a fresh place can run your code. | One fixed amount of time that is the same everywhere. |
| Warm start | The platform may reuse a place that is already set up. | A promise that every later request sees the same globals. |
Practice exercises
The handler sends back the path of the address. What does it print for the cart address?
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());
})();It prints /cart. new URL(request.url).pathname gives the part of the address after the site name, shop.example.
What name does this code print when it runs in Node?
const runtime = globalThis.process?.versions?.node ? "node" : "unknown";
console.log(runtime);It prints node. In Node, process.versions.node holds a version number, so the check is true.
You want one handler to run in several runtimes. Which two shared tools should it get and send back?
Use Request and Response. The handler gets a Request and sends back a Response, and every runtime in this lesson has both.
A checkout order must still be there when the next request reaches a different isolate. Where should you keep it?
Keep it in a database or another storage service. A variable at the top of a module can vanish with its isolate, and another request may not reach that isolate.
The requests come at times 0, 5, 10, and 50, and the warm window is 20. How many cold starts does the model count?
console.log(countColdStarts([0, 5, 10, 50], 20));The model prints 2. Time 0 is cold because it comes first. Time 50 is cold because the gap of 40 is more than the warm window of 20.
Your app needs one tool that only one runtime has. What should you check for before you use it?
Check that the tool itself exists, such as the part of Deno or Bun you need. Keep that check short, and write down what happens when the tool is missing.
Check your understanding
For each question, first ask which part it is about: the language, the engine, the runtime, the platform, or the made-up cold-start model.
Question 1 of 7What does this 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 sentence tells an engine and a runtime apart correctly?
Choose an answer to see the explanation.
Question 3 of 7What name does the lesson's runtime check give in Node, where
process.versions.nodeexists?Choose an answer to see the explanation.
Question 4 of 7What does Cloudflare say about V8 isolates in Workers?
Choose an answer to see the explanation.
Question 5 of 7What is WinterTC, or Ecma TC55, working on?
Choose an answer to see the explanation.
Question 6 of 7In the cold-start model,
warmForMsis 20. Why is the request at 50 cold when the one before it came at 10?Choose an answer to see the explanation.
Question 7 of 7An edge handler takes an order that must still be there for later requests, even if its isolate is shut down. Where should it keep the order?
Choose an answer to see the explanation.
Key takeaways
- JavaScript is the language. An engine runs it. A runtime adds extra tools, such as files, network, and timers.
- Deno uses the V8 engine, with Rust and Tokio around it. Bun uses the JavaScriptCore engine, with its own native code.
Request,Response,URL,fetch, streams, andTextEncoderare a good shared base for server code.- Each edge isolate has its own memory and globals. The platform can reuse or shut down an isolate, so globals are not a safe place to keep data.
- WinterTC, also called Ecma TC55, works on a short list of web tools that every server runtime can offer.
- Measure cold starts on the platform you use. The lesson's model only shows how warm starts are counted.
Remember the one-liner.
Use the shared web tools first, and keep each runtime-only tool in its own small adapter.
Coming next: Small & embedded engines. These engines run on small devices. They use less memory and start faster, but some leave out parts of the language.