July 26th, 2024

Why is spawning a new process in Node so slow?

Node.js shows slow performance in spawning processes, handling 651 requests per second compared to over 2,200 for Deno and Bun, and over 5,000 for Go and Rust. Improvements were noted with child processes.

Read original articleLink Icon
CuriosityInterestSurprise
Why is spawning a new process in Node so slow?

Node.js has been observed to have slow performance when spawning new processes, particularly under load, with a maximum of 40 spawns per second and significant main thread blocking. This issue was highlighted in a benchmarking exercise comparing Node.js with Deno, Bun, Go, and Rust, where Node.js performed the worst, handling only 651 requests per second. In contrast, Deno and Bun achieved over 2,200 requests per second, while Go and Rust reached over 5,000.

To address the performance bottleneck, various strategies were explored, including using Node's cluster module to spawn multiple processes and distribute requests. This approach improved performance to 1,766 requests per second but still lagged behind Deno and Bun. Another method involved moving spawn calls to worker threads, which resulted in even lower performance due to the overhead of coordinating between threads.

Ultimately, switching to child processes for handling requests yielded better results, with Node achieving 2,209 requests per second when using child processes. The exploration also included attempts to optimize logging and output handling, with mixed results across different implementations. The findings suggest that while Node.js can be improved through various techniques, it still struggles to match the performance of compiled languages like Go and Rust, as well as newer runtimes like Deno and Bun.

Related

Show HN: Simulating 20M Particles in JavaScript

Show HN: Simulating 20M Particles in JavaScript

This article discusses optimizing JavaScript performance for simulating 1,000,000 particles in a browser. It covers data access optimization, multi-threading with SharedArrayBuffers and web workers, and memory management strategies.

How we tamed Node.js event loop lag: a deepdive

How we tamed Node.js event loop lag: a deepdive

Trigger.dev team resolved Node.js app performance issues caused by event loop lag. Identified Prisma timeouts, network congestion from excessive traffic, and nested loop inefficiencies. Fixes reduced event loop lag instances, aiming to optimize payload handling for enhanced reliability.

The Cost of JavaScript

The Cost of JavaScript

JavaScript significantly affects website performance due to download, execution, and parsing costs. Optimizing with strategies like code-splitting, minification, and caching is crucial for faster loading and interactivity, especially on mobile devices. Various techniques enhance JavaScript delivery and page responsiveness.

Deno 1.45: Workspace and Monorepo Support

Deno 1.45: Workspace and Monorepo Support

Deno 1.45 introduces workspace and monorepo support, enhances Node.js compatibility, updates deno install, deprecates deno vendor, and more. Improved dependency management and configuration sharing streamline development workflows.

Node.js Is Here to Stay

Node.js Is Here to Stay

Node.js, a key technology for 15 years, powers 6.3 million sites and 98% of Fortune 500 companies. Its lightweight, event-driven design suits real-time and high-concurrency needs. Security updates, Linux preference, and new features like ECMAScript Modules ensure Node.js remains a versatile and secure tool for modern web development.

AI: What people are saying
The comments on the article highlight several key points regarding Node.js performance and process spawning.
  • Performance differences in process spawning are influenced by the operating system, with Linux generally being faster due to features like vfork().
  • Profiling tools like strace can provide insights into system calls and help understand speed discrepancies.
  • There are limitations in Deno's implementation of the cluster module, which may affect performance compared to Node.js.
  • Some users share personal experiences with process spawning in Node.js, noting variations in speed based on their environments.
  • Comments suggest exploring alternatives to fork(), such as vfork() or posix_spawn(), to improve performance.
Link Icon 10 comments
By @jart - 9 months
It's important to specify which OS is being used because spawning goes a lot faster on Linux which has vfork(). If 40 spawns per second is the Linux speed, then I don't even want to know what that looks like on Darwin and OpenBSD. With Cosmopolitan Libc, 20 spawns per second is about the speed I get when simulating fork() on Windows. What makes fork() slow for large programs like Node is that fork() needs to lock and hold every single mutex that exists in a process while it happens. See the IEEE POSIX.1 notes surrounding pthread_atfork(). So yeah, that means everything is de facto blocked until the spawn completes, regardless of the thread that's doing it. Using the separate process to spawn is a smart idea. Especially if you can talk to it via a process shared condition variable.
By @throwitaway1123 - 9 months
One addendum I would add to the section on node:cluster is that Deno hasn't actually implemented the cluster module yet (it's just a stub), so using it with Deno is pure overhead [1]. Also, there's ongoing work to add node:cluster to Bun [2].

[1] https://docs.deno.com/runtime/manual/node/compatibility/

[2] https://github.com/oven-sh/bun/pull/11492

By @ambicapter - 9 months
> Turns out, that if you use a Unix socket and the filename starts with a null byte, like \0foo the socket will not exist on the filesystem and it’ll be automatically removed when no longer used. Weird! Cool!

Is that…intentional on Unix’s part? Seems kind of a weird thing to implement.

By @efilife - 9 months
Nice article, definitely wasn't expecting some of its results. Was good to see Node finally beat Deno near the end of the article.

As a heads up, the author confuses its and it's. Makes the article look unprofessional. https://youryoure.com/?its here's how to differentiate

By @Denvercoder9 - 9 months
I'm curious if anyone has any insights into the answer to the titular question. The article, while certainly interesting, mostly discusses workarounds, but doesn't really dive into a root cause analysis.
By @bsaunder - 9 months
Have you looked into profiling with strace? You should be able to see the actual system calls each of your tests are making. This will probably give you some good insights into what accounts for the speed differences.
By @thomasfromcdnjs - 9 months
Thanks for the super interesting read.

Did you happen to look at how the load on the 8 cores looked at any given time?

By @tamimio - 9 months
Nice article! I remember three years ago, I needed to spawn a few processes in Node, and I ended up using child process spawn to spawn an external binary. It was fast and instantaneous (no benchmarks). It was on a Linux OS, ARM-based.
By @cryptonector - 9 months
The problem is `fork()`. Use `vfork()` or `posix_spawn()`.
By @yu3zhou4 - 9 months
A really nice read, thanks for posting. I'm curious about why it's not as fast as Go and is it possible to speed up to Go's level?