I've been a NodeJS developer for a few years now. I loved the simplicity of the language and the community around it. The fact that I could re-use libraries between my frontend and backend was magic. But as my applications grew in complexity, I started to realise much of what I was told about NodeJS was a lie.
Recently I started investigating the Elixir, Erlang, Gleam and BEAM ecosystem. I was blown away by the simplicity and power of the runtime. I realised that much of what people say about NodeJS is actually true about Elixir.
Concurrency vs Parallelism
First, I can't help but mention Rob Pike's incredible talk "Concurrency is not Parallelism". Rob Pike is one of the creators of Go.
Concurrency
Completing multiple tasks at the same time, but not necessarily simultaneously. They don't actually run at the same time, but they are all being worked on at the same time.
A real-world example would be one person cooking breakfast by putting toast in the toaster, eggs on the pan and coffee in the coffee maker. They are all cooking at the same time because they are 'async' tasks. The reason you can do all three at the same time as one person is that they aren't blocking each other.
Parallelism
Doing multiple tasks simultaneously. It is the ability to actually run multiple CPU instructions at the same time. Literally doing multiple things at the same time.
NodeJS is concurrent but is terrible at parallelism. You can use worker_threads, but they are super heavy (50-100mb each) and feel like nothing more than a hack.
Elixir is both concurrent and parallel. The runtime handles all the heavy lifting for you. You can spawn thousands of processes and the runtime will handle the scheduling for you.
The real-world example would be one man chopping wood with an axe. He can only chop one piece of wood at a time, but if he had 10 friends, they could all chop wood at the same time. Chopping wood isn't an 'async' task, it's blocking; it's the only thing you can do at that one time.
The real magic of Elixir is that you can have a kitchen filled with hundreds of thousands of people cooking breakfast at the same time. Think about how much breakfast you could make.
Fault Tolerance
NodeJS is terrible at fault tolerance. If a process crashes, the whole application crashes. You can use PM2 to restart the process, but it's not a good solution.
I have never had a long-running node application that hasn't leaked memory. It's more a matter of if your application will be OOM killed in 2 days or 2 weeks. Debugging memory leaks in node is a nightmare.
Elixir's whole thing is to contain errors and crashes. If one route handler or job crashes, who cares? The process will be restarted and the application will continue to run. Why on earth would we want our entire application to restart because of a single error?
Performance
Most modern languages are JIT compiled. Performance almost always comes down to how the language handles concurrency and memory management. Node is slow because it's (almost, small ints use the stack) entirely heap allocated, and this causes massive slowdowns. Not to mention that modern language features which I use extensively like spreads and destructuring are slow because they copy memory.
Imagine writing an application in C where you just spam malloc and memcpy everywhere, including in loops. You'd be fired immediately. That's essentially what you're doing in Node.
Scaling
Scaling node is the worst thing on the planet. We use PM2 cluster mode to spawn multiple instances of our application, but all this does is basically run two separate instances of Node that are aware of each other.
import { isMainThread } from "worker_threads";
if (isMainThread) {
// This code is executed in the main thread
} else {
// This code is executed in the worker thread
}
You get the idea... it's a hack.
No one actually believes that Node can scale
With the increasing push for edge workers, lambdas, and the thousandth serverless way of deploying Node, something I ended up realising was that no one actually believes you can deploy Node in a way that doesn't burst into flames if the process is run for more than a few days.
Theo of t3.gg, known online as the 'Javascript guy' (who is also a big fan of Elixir) said:
I would never want to scale a system that wasn't either written via entirely serverless patterns like Lambda or was written on top of the Erlang OTP in the beam VM
SOURCE: Phoenix LiveView Is Making Me Reconsider React... (00:26:30)
Cloud monopolies
It's not particularly surprising that AWS, Azure and so on push so heavily for developers to deploy Javascript, Ruby, Python etc. They are all terrible at resource management and will have you paying for more resources than you need.
AWS's value prop: Hey guess what, We've solved those languages' problems, just launch a linux VM for every request you get, it takes 3-5 seconds to do it and we'll charge you 10x what a regular server would've cost, but at least your instance of node is killed quickly so it doesn't have the opportunity to leak... Also you're going to need a cache server, and a database (that isn't in our free tier), API gateway, NAT gateways... and so on, you get the idea.
Elixir allows you to network nodes together; you just use the cache functionality in Elixir itself. No need for a separate cache server.
The joke in the Elixir community goes:
Focus | Team for a Typical Stack | Elixir |
---|---|---|
Backend | Bob | Alex (Phoenix, Ash) |
Frontend | Amy | Alex (LiveView) |
Real-time UX (Global) | $$$$ Good luck! | Alex (PubSub, Presence) |
AI / Data Engineering | Kristen | Alex (Nx, Livebook) |
iOS, Android | Expensive offshore team | Alex (LiveView Native) |
DevOps / DB / Infra / Deploy | James + Contractors | Alex (Fly or Gigalixir) |
100 Users | Small team, 2 servers | Alex, 2 servers |
10,000 Users | Medium Team, 5 servers | Alex, 2 servers |
100k Users | Large Team, 20 servers | Alex, 3 servers |
I'm not treading new ground saying that cloud providers are Merchants of complexity. Realistically, you can scale to the moon with a single Elixir node and PostgreSQL. No, you don't need RabbitMQ; no, you don't need Redis; no, you don't need ElastiCache; no, you don't need a million other things that AWS will try to sell you. It's all built into the runtime or Postgres...
Misdirection
I was initially pulled into the farce of NodeJS performance when I read the case study of LinkedIn reducing its latency and server count drastically by moving to NodeJS. But what they don't tell you is that they moved from Ruby on Rails to NodeJS. Of course, they are going to see a massive performance increase.
This move is equivalent to preferring to step in cat shit over cow shit. The latter is worse, but at the end of the day, you're still stepping in shit.
Standard library
Node's standard library is a mess, understandably really. Javascript has changed so much over the years, and the standard library is a mess of callbacks, promises, and async/await. Interestingly enough, they are missing basic things like some way of limiting concurrency on my Promise.all calls.
In Elixir, we can limit concurrency by just writing:
[
&heavy_call_1/0,
&heavy_call_2/0,
&heavy_call_3/0
]
|> Task.async_stream(fn fun -> fun.() end, ordered: false, max_concurrency: 3)
|> Stream.filter(&match?({:ok, _}, &1))
|> Enum.take(2)
Not only can we avoid using the p-queue library, but we barely have to think about blocking the event loop, since we are spawning a new process for each item in the stream. You can do whatever you want in those tasks (within reason, Elixir still struggles with CPU-bound tasks).
Resource usage
Why on earth does my Next & Express application use 6GB of memory? My Express application launches 10 BullMQ workers (4 of which are run on worker_threads).
Next is its own beast; it uses a ton of memory and is slow as hell. Don't get me wrong, if you need to build a fully-featured website quickly, with built-in SEO, image-optimisation, and a thousand different ways of rendering pages (ISR, SSR, Prerendering, blah blah blah), Next is a great choice.
But please just keep your NextJS app as thin as possible, then just call out to a backend API. -- Sure, having full-stack type safety is nice, but it becomes harder to justify the larger your app gets.
Looking at what the Elixir people get with Phoenix LiveView, and Vercel's indifference to self-hosting NextJS (they only recently added NodeJS runtime for their middlewares...), I cannot help but feel like there are much better options.
Mental overhead
The constant mental overhead involved with writing node is 'will this block'? 'will this throw'? 'will this block'? Whenever I'm writing any JS that will be long-running, I am highly aware of whether some code I'm writing will block or crash the rest of the application -- Elixir has none of this.
The amount of time I have spent refactoring code to be nicer to the event loop basically makes up for the so-called 'developer productivity boost' that node gives you.
Fin
I honestly believe that if every time someone reached for NodeJS they instead reached for Elixir, they would be so many times happier and productive in the future.
Node pulls you in with its single language productivity boost (some people disagree with this, but honestly just writing one validator in Zod for my frontend and backend is a huge win for me) and gigantic package and developer ecosystem, but it's a trap. It's a trap that will have you debugging shitshow memory leaks and crashes for years to come.
I am a measured person. I don't like to make sweeping statements, but because of my real-world experience, NodeJS can comfortably join MongoDB in the 'usually a bad decision' pile.
Now the next question is how do I move my tens of thousands of lines of NodeJS to Elixir... Probably not.