Workers is the open source kernel of the Cloudflare Workers platform, and despite being built around v8, and running JavaScript and WebAssembly, it is quite different from Node.js. This talk will explore the differences and similarities and hopefully give you a bit more insight into how both operate.
2. 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
3. What's the goal here?
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
7. 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
8. 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!
9. 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.
10. 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.
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 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/
13. 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/
14. 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.
15. 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…
16. 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…
18. 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
19. 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).
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 a Workers 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…
export default {
async fetch(req) {
setTimeout(() => console.log('hello'), 1000);
return new Response('hello world');
}
}
29. 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.