[HN Gopher] Node.js worker threads are problematic, but they wor...
___________________________________________________________________
Node.js worker threads are problematic, but they work great for us
Author : goodoldneon
Score : 58 points
Date : 2026-03-18 16:51 UTC (4 days ago)
(HTM) web link (www.inngest.com)
(TXT) w3m dump (www.inngest.com)
| vilequeef wrote:
| It's not weird that you can't share state between totally
| different processes except by passing in args.
|
| And you can make it thread-like if you prefer by creating a "load
| balancer" setup to begin with to keep them CPU bound.
| require('os').cpus().length
|
| Spawn a process for each CPU, bind data you need, and it can feel
| like multithreading from your perspective.
|
| More here https://github.com/bennyschmidt/simple-node-
| multiprocess
| socketcluster wrote:
| I love the simplicity of Node.js that each process or child
| process can have its own CPU core with essentially no context
| switching (assuming you have enough CPU cores).
|
| Most other ways are just hiding the context switching costs and
| complicating monitoring IMO.
| derodero24 wrote:
| I went through a similar journey trying worker threads for CPU-
| bound work in Node. The serialization cost of passing data
| between threads ate most of my gains, especially with larger
| inputs. Ended up going the napi-rs route instead -- Rust addon
| running in the main thread with near-zero FFI overhead. Different
| tradeoff since you lose the parallelism, but for my workload the
| raw speed was already enough.
| socketcluster wrote:
| I like Node.js' simple and fully isolated concurrency model. You
| shouldn't be blocking the main event loop for 30 seconds! The
| main event loop is not intended to be used for heavy processing.
|
| You can just set up a separate child process for that. The main
| event loop which handles connections should just co-ordinate and
| delegate work to other programs and processes. It can await for
| them to complete asynchronously; that way the event loop is not
| blocked.
|
| I recall people have been able to get up to around a million
| (idle) WebSocket connections handled by a single process.
|
| I was able to comfortably get 20k concurrent sockets per process
| each churning out 1 outbound message every 3 to 5 seconds
| (randomized to spread out the load).
|
| It is a good thing that Node.js forces developers to think about
| this because most other engines which try to hide this complexity
| tend to impose a significant hidden cost on the server in the
| form of context switching... With Node.js, there is no such cost,
| your process can basically have a whole CPU core for itself and
| it can orchestrate other processes in a maximally efficient way
| if you write your code correctly... Which Node.js makes very easy
| to do. Spawning child processes and communicating with them in
| Node.js is a breeze.
| winrid wrote:
| You like limitations, which is ok, I'll take a better toolset.
|
| You can not limit yourself when you need more but still follow
| these patterns in any other language.
| ptrwis wrote:
| I'm currently writing simulations of trading algorithms for my
| own use. I'm using worker_threads + SharedArrayBuffer and running
| them in Bun. I also tried porting the code to C# and Go, but the
| execution time ended up being very similar to the Bun version.
| NodeJS was slower. Only C gave a clear, noticeable performance
| advantage -- but since I haven't written C in a long time, the
| code became significantly harder to maintain.
| shepardrtc wrote:
| I built an algorithmic trader years ago just using Python, but
| for the hot paths I gave each algorithm its own function in its
| own file, and then I would compile the files with Cython. The
| speedup was pretty significant. I barely wrote any "Cython"
| stuff (meaning declaring variables and other minor assists).
| The code is still very much python, just with a few little
| extras that are easy to understand.
| Jcampuzano2 wrote:
| I get its a constraint of the language but the ubiquitousness of
| bundlers and differing toolchains in the JS world has always made
| me regret trying to use worker primitives, whether they be web
| workers, worker threads and more. Not to mention trying to ship
| them to users via a library being a nightmare as mentioned in the
| article.
|
| Almost none of them treat these consistently (if they consider
| these at all) and all require you to work around them in strange
| ways.
|
| It feels like there is a lot they could help with in the web
| world, especially in complex UI and moving computation off the
| main thread but they are just so clunky to use that almost nobody
| tries to work around it.
|
| The ironic part is if bundlers, transpilers, compilers etc.
| weren't used at all they would probably have much more widespread
| use.
| rco8786 wrote:
| Yea, for the vast, vast majority of workloads just forking
| separate node process ends up being better than mucking with
| threads.
| baublet wrote:
| Reading the article, I didn't see this answered: why not scale to
| more nodes if your workload is CPU bound? Spin off 1 cpu and a
| few gb of ram container and scale that as wide as you need?
|
| e.g., this certainly helps when the event loop is blocked, but so
| could FFI calls to another language for the CPU bound work. I'd
| only reach for a new Node thread if these didn't pan out, because
| there's usually a LOT that goes into spinning up a new node
| process in a container (isolating the data, making sure any
| bundlers and transpilers are working, making sure the worker
| doesn't pull in all the app code, etc.).
|
| Side car processes aren't free, either. Now your processes are
| contending for the same pool of resources and can't share
| anything, which IME means more likelihood of memory issues, esp
| if there isn't anything limiting the workers your app can spawn.
|
| Still, good article! Love seeing the ways people tackle CPU bound
| work loads in an otherwise I/O bound Node app.
| n_e wrote:
| > but so could FFI calls to another language for the CPU bound
| work
|
| Worker threads can be more convenient than FFI, as you don't
| need to compile anything, you can reuse the main application's
| functions, etc.
| baublet wrote:
| True! Although in a lot of Node you DO have a compile chain
| (typescript) you need to account for. There's a transactional
| cost there to get these working well, and only sharing the
| code it needs. These days it's much smaller than it used to
| be, though, so worker functions are seeing more use.
|
| I make my comment to note tho that in many envs it's easier
| to scale out than account for all the extra complications of
| multiple processes in a single container.
| zer00eyz wrote:
| > few gb of ram ...
|
| 5 years ago I never would have given this comment a second
| thought.
|
| Now I read it and have to wonder: when does the price of ram
| start showing up in the butchers bill from your cloud provider?
| fragmede wrote:
| I don't know about you, but my cloud provider has been
| charging me for the ram on my compute instances since the
| beginning.
| zer00eyz wrote:
| Ram has always been one of the major price drivers...
|
| But the prices have gotten stupid:
| https://pcpartpicker.com/trends/price/memory/
|
| https://appleinsider.com/articles/26/02/27/the-global-ram-
| an...
| baublet wrote:
| You have to pay that cost in a worker thread anyway, too.
| There's no free lunch.
| groundzeros2015 wrote:
| - you should be using multiple node processes - you should be
| spawning tools to do heavy computation
| chrisweekly wrote:
| Related tangent: Platformatic's "Watt" server^1 takes a pretty
| interesting approach to Node, leveraging worker threads on all
| available cores for maximum efficiency.
|
| 1. https://docs.platformatic.dev/docs/overview/architecture-
| ove...
| kketch wrote:
| The article calls worker threads "problematic", but it doesn't
| really make a strong case for why they're supposedly problematic.
|
| Having a separate isolate in each threads spawned with the worker
| threads with a minimal footprint of 10MB does not seem like a
| high price to pay. It's not like you're going to spawn hundreds
| of them anyway is it? You will very likely spawn less or as much
| threads as your CPU cores can handle concurrently. You typically
| don't run a hundred of threads (OS threads) you use a thread pool
| and you cap the concurrency by setting a limit of maximum threads
| to spawn.
|
| This is also how goroutines work under the hood, they are "green
| threads", an abstraction that operate on top of a much small OS
| thread pool.
|
| Worker threads have constraints but most of them are intentional,
| and in many cases desirable.
|
| I'd also add that SharedArrayBuffer doesn't limit you to "shared
| counters or coordination primitives". It's just raw memory, you
| could store structured data in it using your own memory layout.
| There are libraries out there that implement higher-level data
| structures this way already
| everforward wrote:
| The problematic part is mostly in nomenclature. They're called
| "threads" but don't really behave the way you'd expect threads
| to.
|
| They're heavy, they don't share the entire process memory space
| (ie can't reference functions), and I believe their imports are
| separate from each other (ie reparsed for each worker into its
| own memory space).
|
| In many ways they're closer to subprocesses in other languages,
| with limited shared memory.
|
| It's not "clean" to spin up thousands of threads, but it does
| work and sometimes it's easier to write and reason about than a
| whole pipeline of distributing work to worked threads. I
| probably wouldn't do it in a server, but in a CLI I would
| totally do something like spawn a thread for each file a user
| wants to analyze and let the OS do task scheduling for me. If
| they give me a thousand files, they get a thousand threads.
| That overhead is pretty minimal with OS threads (on Linux,
| Windows is a different beast).
| jwilliams wrote:
| If it's any comfort, I don't hear many JS/TS/Node/etc
| developers calling them threads or really thinking of them
| that way. Usually just Workers or Web Workers -- "worker
| threads" mostly slips in from Node. Even then, "worker"
| dominates.
|
| In terms of tradeoffs, if you're coming from the single event
| loop model, they're pretty consistent with the rest of JS.
| Isolation-first, explicit sharing, fewer footguns. So I think
| the tradeoffs are the right tradeoffs.
|
| FWIW, traditional threads have their own tradeoffs
| (especially around IO). In JS that's mostly a non-issue, so
| the "I need 1000s of threads" case just doesn't come up very
| often.
| kketch wrote:
| I think the isolation and memory safety guarantees that
| worker threads (or Web Workers) provide are very welcome. The
| friction mainly comes from ergonomics, as pointed out in the
| article. So there's definitely room for improvement there
| (even within the current constraints).
|
| A worker thread or Web Worker runs in its own isolate, so it
| needs to initialise it by parsing and executing its entry
| point. I'm not quite sure whether that's something that
| already happens but you could imagine optimising this by
| caching or snapshotting the initial state of an isolate when
| multiple workers use the same entry point, so new workers can
| start faster.
|
| That cannot be done with the original main thread isolate
| because usually the worker environment has both different
| capabilities than the main isolate and a different entry
| point.
|
| If I have to handle 1000 files in a small CLI I would
| probably just use Node.js asynchronous IO in a single thread
| and let it handle platform specifics for me! You'll get very
| good throughput without having to handle threads yourself.
| whizzter wrote:
| No, they're threads as far as the OS is concerned (they'll
| map to OS threads) and actually _do_ share physical process
| and memory (that's how SharedArrayBuffer works).
|
| However, apart from atomic "plain" memory no objects are
| directly shared (For Node/V8 they live in so called Isolated
| iirc) so from a logical standpoint they're kinda like a
| process.
|
| The underlying reason is that in JavaScript objects are by
| default open to modification, ie: const t =
| {x:1,y:2}; t.z = 3; console.log(t); // => { x: 1,
| y: 2, z: 3 }
|
| To get sane performance out of JS there are a ton of tricks
| the runtime does under the hood, the bad news is that those
| are all either slow (think Python GIL) or heavily exploitable
| in a multithreaded scenario.
|
| If you've done multithreaded C/C++ work and touched upon
| Erlang the JS Worker design is the logical conclusion,
| message passing works for small packets (work orders,
| structured cloning) whilst large data-shipping can be
| problematic with cloning.
|
| This is why SharedArrayBuffer:s allows for no-copy sharing
| since the plain memory arrays they expose don't offer any
| security surprises in terms of code execution (spectre style
| attacks is another story) and also allows for work-
| subdivision if needed.
| goldenarm wrote:
| The lack of backpressure handling nor promise api for postMessage
| is also quite annoying, I had many OOMs because of it.
| whizzter wrote:
| It's not ideal, the api is kind of low-yet-high-level and that
| brings some complications.
|
| Move backpressure handling onto the task producer and use a
| SharedArrayBuffer between the producer and worker, where the
| worker atomically updates a work-count or current work item ID
| in that SharedArrayBuffer that the producer can read
| (atomically) to determine how far along the worker has gotten.
| esprehn wrote:
| The worker situation would be much better with inline workers (or
| modules).
|
| https://github.com/tc39/proposal-module-declarations
|
| Unfortunately the JS standards folks have refused so far to make
| this situation better.
|
| Ex. it should just be `new Worker(module { ... })`.
| bakkoting wrote:
| We haven't refused, it just takes time! There was an update at
| the meeting two weeks ago [1]. There's a lot of other machinery
| which needs to be specified and implemented before module
| declarations will work but it's coming along.
|
| [1]
| https://docs.google.com/presentation/d/1inTcnb4hugyAvKrjFX_X...
| whizzter wrote:
| It's all about security, see my other comment.
| https://news.ycombinator.com/item?id=47480080
| cpursley wrote:
| Or you could use Elixir with Postgrest and not have to bold on
| all this wonky 3rd paid tools for basic stuff like background
| jobs:
|
| https://elixirisallyouneed.dev/tools?q=Pgflow
___________________________________________________________________
(page generated 2026-03-22 23:01 UTC)