A Tale of Two Runtimes
Node.js vs. Workers
First, an introduction…
I'm James (jasnell)
● Node.js core contributor and TSC member since 2015
○ URL, AbortController, Web Crypto, Web Streams, HTTP/2, HTTP/3
● Cloudflare Principal Engineer on Workers Runtime since 2021
○ workerd (open source core of workers), API surface, WinterCG
● WinterCG Founder and Co-chair
What's the goal here?
To give a bit more insight into the internal workings of
Node.js by comparing it to Workers…
Node.js vs. workers - Similarities
Both use v8 to execute JavaScript and Web Assembly
Both are implemented using a mix of C++ and JavaScript
Both provide implementations of Web Platform APIs (WinterCG common minimum):
AbortController
AbortSignal
Blob
ByteLengthQueuingStrategy
CompressionStream
CountQueuingStrategy
Crypto
CryptoKey
DecompressionStream
DOMException
Event
EventTarget
File
FormData
Headers
ReadableByteStreamController
ReadableStream
ReadableStreamBYOBReader
ReadableStreamBYOBRequest
ReadableStreamDefaultController
ReadableStreamDefaultReader
Request
Response
SubtleCrypto
TextDecoder
TextDecoderStream
TextEncoder
TextEncoderStream
TransformStream
TransformStreamDefaultController
URL
URLSearchParams
WebAssembly.Global
WebAssembly.Instance
WebAssembly.Memory
WebAssembly.Module
WebAssembly.Table
WritableStream
WritableStreamDefaultController
But that's about where the similarities end.
Let's start with the process model
Node.js
Node.js Process
Main Thread
i/o and gc threads
"Environment" / "Realm"
Node.js' internal state, v8:Isolate, v8::Context
Event Loop Node.js' JavaScript
Users' JavaScript + modules
Node.js
Node.js Process
Main Thread
i/o and gc threads
"Environment" / "Realm"
Node.js' internal state, v8:Isolate, v8::Context
Event Loop Node.js' JavaScript
Users' JavaScript + modules
The i/o threads perform system tasks like reading/writing
from files and sockets, crypto operations, garbage
collection, etc. They do not execute JavaScript at all!
Node.js
Node.js Process
Main Thread
i/o and gc threads
"Environment" / "Realm"
Node.js' internal state, v8:Isolate, v8::Context
Event Loop Node.js' JavaScript
Users' JavaScript + modules
The Event Loop monitors the i/o threads. When i/o completes, the event loop
triggers a callback function to run.
Node.js
Node.js Process
Main Thread
i/o and gc threads
"Environment" / "Realm"
Node.js' internal state, v8:Isolate, v8::Context
Event Loop Node.js' JavaScript
Users' JavaScript + modules
The callback triggered by the event loop typically calls a JavaScript function
provided by either the user's code or Node.js built-in JavaScript.
When Node.js starts, it runs an initial bit of JavaScript (the entry point), which may or
may not schedule i/o on the event loop. If it does not… the process exits. If it does, the
process waits for the i/o to complete, runs some JavaScript, then checks to see if
there's more i/o to wait on…
Run Entry Point JavaScript
i/o
scheduled?
No
i/o
completed?
No
Run i/o Handler JavaScript
Yes
Yes
Every time the event loop triggers a callback, we call a JavaScript function. That
function can schedule more i/o, schedule a "next tick", or resolve/reject a promise.
https://www.nearform.com/blog/optimise-node-js-performance-avoiding-broken-promises/
When the JavaScript function returns control back to c++, we call all next ticks and
drain the promise "microtask queue"... then continue on with the event loop
https://www.nearform.com/blog/optimise-node-js-performance-avoiding-broken-promises/
When Node.js is running as a Web server
● The initial entry point creates and starts an http server (this schedules an i/o
task on the event loop… "wait for socket connections"
● Whenever a new socket connection is received, the event loop triggers a
callback function that executes some JavaScript.
● That JavaScript uses Node.js built-in mechanisms to perform the TLS
handshake (if any), parse out the HTTP headers, then call the users request
handling code.
● While a lot of this involves scheduling smaller i/o operations on the event
loop, Node.js is generally only capable of handling one request at a time.
In Node.js, all requests time-share the same thread/isolate
R1 R2 R1 R3 R2 R1 …
Event
Loop
Performance / Request-per-second is entirely dependent on how quickly
each individual callback finish and return control back to the event loop
so it can move on to the next task…
Node.js – Other key characteristics
● All code is trusted.
● All code in the process is considered one application.
● Inherently "single tenant"
● There is always a Single Process with a Main Thread with a single event loop
and a single v8::Isolate.
○ Worker Threads have their own event loop and v8::Isolate. Model is essentially the same.
● HTTP request dispatching happens in the main thread, in JavaScript…
○ This is a key difference from Workers… as we'll see in a minute.
● Node.js can do a lot more than just handling HTTP requests…
○ This is another key difference from Workers…
So what about Workers
Let's define a few things…
What is a "Worker"
Logically, a "Worker" is an application that can do one of:
● Handling an HTTP request ("fetch handler")
● Handling a scheduled task ("cron trigger", "alarm")
● Processing logs from other Workers ("tail handler")
● Maintain persistent state ("durable objects")
Fundamentally, a Worker is much more constrained in what it can do relative to a
Node.js app.
For example, a Worker cannot be a CLI
Let's define a few things…
A Worker consists of a bundle of Modules and Bindings.
A Module can be: ESM, CommonJS, WebAssembly, Text, Binary Data, JSON
A Binding is some capability (e.g. fetching to a private network, using a specific KV
store, interacting with cache, etc)
A Worker always has a Main module that exports at least one entry point handler
(most typically a "fetch" handler).
Workers
Workers Process
Request Thread (Processes one request at a time)
Request Thread (Processes one request at a time)
Request Thread (Processes one request at a time)
With workers, the process starts with a single thread that begins listening for
connections on an inbound socket. When it starts to process a request, it
spawns another thread to wait for the next connection…
Anatomy of a Workers Connection Handling Thread…
Event
Loop
Microtask Queue
Modules
Handler
Connection
Workers vs Node.js – The key differences
● With Node.js, the v8::Isolate is bound to one thread for its lifetime.
● With Workers, the v8::Isolate is bound to the Worker…
○ …which runs on any thread currently handling a request for that worker
○ …but only one at a time
● With Node.js, the thread will run for as long as there is i/o scheduled on the
event loop… then exit
● With Workers, a thread runs endlessly in a loop, processing requests for
multiple Workers, one at a time.
Workers vs Node.js – The key differences
● With Node.js, receiving an http request, parsing it, determining how to route it,
all happens in JavaScript, with every request handled by a single thread.
● With Workers, receiving the http request, parsing it, determining how to route
it happens before JavaScript is run. Every request potentially handled by a
different thread.
● With Node.js, the event loop and promise microtask queues are distinctly
separate things. callbacks !== promises.
● With Workers, the event loop is entirely promise-based
○ …there is no concept of "nextTick()" or "setImmediate()" or other async callbacks. It's all just
promises
Workers vs Node.js – The key differences
● With Node.js, the thread will not exit if the event loop still has i/o tasks.
● With Workers, when a request is complete all pending i/o tasks associated
with that request are canceled.
● With Node.js, JavaScript runs at startup, whenever some scheduled i/o
completes, or when the microtask queue is drained
● With Workers, JavaScript runs when the worker is being bootstrapped, when
a request is being handled, or when the microtask queue is drained.
Workers vs Node.js – The key differences
● With Node.js, all code in the process is considered trusted. Worker threads
are allowed to freely share memory, communicate with each other, etc.
● With Workers, no code is considered trusted. Every Worker is considered a
trust boundary. Workers are never permitted to share state/memory and
sandboxing is carefully applied.
● With Node.js, a single process is inherently single tenant. It runs a single
application always.
● With Workers, a single process is multi-tenant. It runs multiple discrete
applications (up to thousands of Workers simultaneously)
A Node.js example…
import { createServer } from 'node:http'
const server = createServer((req, res) => {
setTimeout(() => console.log('hello'), 1000);
res.end('hello world');
});
server.listen(8888);
A Node.js example…
import { createServer } from 'node:http'
const server = createServer((req, res) => {
setTimeout(() => console.log('hello'), 1000);
res.end('hello world');
});
server.listen(8888);
The entrypoint JavaScript has to create the server,
configure it, tell it to listen…
When a request is received, it is processed on the same
thread that is listening for new requests.
The timeout fires even after the request is completed.
A Workers example…
export default {
async fetch(req) {
setTimeout(() => console.log('hello'), 1000);
return new Response('hello world');
}
}
A Workers example…
export default {
async fetch(req) {
setTimeout(() => console.log('hello'), 1000);
return new Response('hello world');
}
}
The entry point is just the request handler.
Every request may be handled by a different thread.
The timeout is canceled when the response is returned.
Thank you.
Будь сильним. Залишатися в безпеці.

"Node.js vs workers — A comparison of two JavaScript runtimes", James M Snell

  • 1.
    A Tale ofTwo Runtimes Node.js vs. Workers
  • 2.
    First, an introduction… I'mJames (jasnell) ● Node.js core contributor and TSC member since 2015 ○ URL, AbortController, Web Crypto, Web Streams, HTTP/2, HTTP/3 ● Cloudflare Principal Engineer on Workers Runtime since 2021 ○ workerd (open source core of workers), API surface, WinterCG ● WinterCG Founder and Co-chair
  • 3.
    What's the goalhere? To give a bit more insight into the internal workings of Node.js by comparing it to Workers…
  • 4.
    Node.js vs. workers- Similarities Both use v8 to execute JavaScript and Web Assembly Both are implemented using a mix of C++ and JavaScript Both provide implementations of Web Platform APIs (WinterCG common minimum): AbortController AbortSignal Blob ByteLengthQueuingStrategy CompressionStream CountQueuingStrategy Crypto CryptoKey DecompressionStream DOMException Event EventTarget File FormData Headers ReadableByteStreamController ReadableStream ReadableStreamBYOBReader ReadableStreamBYOBRequest ReadableStreamDefaultController ReadableStreamDefaultReader Request Response SubtleCrypto TextDecoder TextDecoderStream TextEncoder TextEncoderStream TransformStream TransformStreamDefaultController URL URLSearchParams WebAssembly.Global WebAssembly.Instance WebAssembly.Memory WebAssembly.Module WebAssembly.Table WritableStream WritableStreamDefaultController
  • 5.
    But that's aboutwhere the similarities end.
  • 6.
    Let's start withthe process model
  • 7.
    Node.js Node.js Process Main Thread i/oand gc threads "Environment" / "Realm" Node.js' internal state, v8:Isolate, v8::Context Event Loop Node.js' JavaScript Users' JavaScript + modules
  • 8.
    Node.js Node.js Process Main Thread i/oand gc threads "Environment" / "Realm" Node.js' internal state, v8:Isolate, v8::Context Event Loop Node.js' JavaScript Users' JavaScript + modules The i/o threads perform system tasks like reading/writing from files and sockets, crypto operations, garbage collection, etc. They do not execute JavaScript at all!
  • 9.
    Node.js Node.js Process Main Thread i/oand gc threads "Environment" / "Realm" Node.js' internal state, v8:Isolate, v8::Context Event Loop Node.js' JavaScript Users' JavaScript + modules The Event Loop monitors the i/o threads. When i/o completes, the event loop triggers a callback function to run.
  • 10.
    Node.js Node.js Process Main Thread i/oand gc threads "Environment" / "Realm" Node.js' internal state, v8:Isolate, v8::Context Event Loop Node.js' JavaScript Users' JavaScript + modules The callback triggered by the event loop typically calls a JavaScript function provided by either the user's code or Node.js built-in JavaScript.
  • 11.
    When Node.js starts,it runs an initial bit of JavaScript (the entry point), which may or may not schedule i/o on the event loop. If it does not… the process exits. If it does, the process waits for the i/o to complete, runs some JavaScript, then checks to see if there's more i/o to wait on… Run Entry Point JavaScript i/o scheduled? No i/o completed? No Run i/o Handler JavaScript Yes Yes
  • 12.
    Every time theevent loop triggers a callback, we call a JavaScript function. That function can schedule more i/o, schedule a "next tick", or resolve/reject a promise. https://www.nearform.com/blog/optimise-node-js-performance-avoiding-broken-promises/
  • 13.
    When the JavaScriptfunction returns control back to c++, we call all next ticks and drain the promise "microtask queue"... then continue on with the event loop https://www.nearform.com/blog/optimise-node-js-performance-avoiding-broken-promises/
  • 14.
    When Node.js isrunning as a Web server ● The initial entry point creates and starts an http server (this schedules an i/o task on the event loop… "wait for socket connections" ● Whenever a new socket connection is received, the event loop triggers a callback function that executes some JavaScript. ● That JavaScript uses Node.js built-in mechanisms to perform the TLS handshake (if any), parse out the HTTP headers, then call the users request handling code. ● While a lot of this involves scheduling smaller i/o operations on the event loop, Node.js is generally only capable of handling one request at a time.
  • 15.
    In Node.js, allrequests time-share the same thread/isolate R1 R2 R1 R3 R2 R1 … Event Loop Performance / Request-per-second is entirely dependent on how quickly each individual callback finish and return control back to the event loop so it can move on to the next task…
  • 16.
    Node.js – Otherkey characteristics ● All code is trusted. ● All code in the process is considered one application. ● Inherently "single tenant" ● There is always a Single Process with a Main Thread with a single event loop and a single v8::Isolate. ○ Worker Threads have their own event loop and v8::Isolate. Model is essentially the same. ● HTTP request dispatching happens in the main thread, in JavaScript… ○ This is a key difference from Workers… as we'll see in a minute. ● Node.js can do a lot more than just handling HTTP requests… ○ This is another key difference from Workers…
  • 17.
  • 18.
    Let's define afew things… What is a "Worker" Logically, a "Worker" is an application that can do one of: ● Handling an HTTP request ("fetch handler") ● Handling a scheduled task ("cron trigger", "alarm") ● Processing logs from other Workers ("tail handler") ● Maintain persistent state ("durable objects") Fundamentally, a Worker is much more constrained in what it can do relative to a Node.js app. For example, a Worker cannot be a CLI
  • 19.
    Let's define afew things… A Worker consists of a bundle of Modules and Bindings. A Module can be: ESM, CommonJS, WebAssembly, Text, Binary Data, JSON A Binding is some capability (e.g. fetching to a private network, using a specific KV store, interacting with cache, etc) A Worker always has a Main module that exports at least one entry point handler (most typically a "fetch" handler).
  • 20.
    Workers Workers Process Request Thread(Processes one request at a time) Request Thread (Processes one request at a time) Request Thread (Processes one request at a time) With workers, the process starts with a single thread that begins listening for connections on an inbound socket. When it starts to process a request, it spawns another thread to wait for the next connection…
  • 21.
    Anatomy of aWorkers Connection Handling Thread… Event Loop Microtask Queue Modules Handler Connection
  • 22.
    Workers vs Node.js– The key differences ● With Node.js, the v8::Isolate is bound to one thread for its lifetime. ● With Workers, the v8::Isolate is bound to the Worker… ○ …which runs on any thread currently handling a request for that worker ○ …but only one at a time ● With Node.js, the thread will run for as long as there is i/o scheduled on the event loop… then exit ● With Workers, a thread runs endlessly in a loop, processing requests for multiple Workers, one at a time.
  • 23.
    Workers vs Node.js– The key differences ● With Node.js, receiving an http request, parsing it, determining how to route it, all happens in JavaScript, with every request handled by a single thread. ● With Workers, receiving the http request, parsing it, determining how to route it happens before JavaScript is run. Every request potentially handled by a different thread. ● With Node.js, the event loop and promise microtask queues are distinctly separate things. callbacks !== promises. ● With Workers, the event loop is entirely promise-based ○ …there is no concept of "nextTick()" or "setImmediate()" or other async callbacks. It's all just promises
  • 24.
    Workers vs Node.js– The key differences ● With Node.js, the thread will not exit if the event loop still has i/o tasks. ● With Workers, when a request is complete all pending i/o tasks associated with that request are canceled. ● With Node.js, JavaScript runs at startup, whenever some scheduled i/o completes, or when the microtask queue is drained ● With Workers, JavaScript runs when the worker is being bootstrapped, when a request is being handled, or when the microtask queue is drained.
  • 25.
    Workers vs Node.js– The key differences ● With Node.js, all code in the process is considered trusted. Worker threads are allowed to freely share memory, communicate with each other, etc. ● With Workers, no code is considered trusted. Every Worker is considered a trust boundary. Workers are never permitted to share state/memory and sandboxing is carefully applied. ● With Node.js, a single process is inherently single tenant. It runs a single application always. ● With Workers, a single process is multi-tenant. It runs multiple discrete applications (up to thousands of Workers simultaneously)
  • 26.
    A Node.js example… import{ createServer } from 'node:http' const server = createServer((req, res) => { setTimeout(() => console.log('hello'), 1000); res.end('hello world'); }); server.listen(8888);
  • 27.
    A Node.js example… import{ createServer } from 'node:http' const server = createServer((req, res) => { setTimeout(() => console.log('hello'), 1000); res.end('hello world'); }); server.listen(8888); The entrypoint JavaScript has to create the server, configure it, tell it to listen… When a request is received, it is processed on the same thread that is listening for new requests. The timeout fires even after the request is completed.
  • 28.
    A Workers example… exportdefault { async fetch(req) { setTimeout(() => console.log('hello'), 1000); return new Response('hello world'); } }
  • 29.
    A Workers example… exportdefault { async fetch(req) { setTimeout(() => console.log('hello'), 1000); return new Response('hello world'); } } The entry point is just the request handler. Every request may be handled by a different thread. The timeout is canceled when the response is returned.
  • 30.
    Thank you. Будь сильним.Залишатися в безпеці.