Oh wow, I never knew Klarna uses BEAM. I always thought BEAM is vastly underrated and it's even hard for me to convince clients of showing how powerful it (Elixir/BEAM) really is, especially when they're already on something like NodeJS. This will be a nice reference to use in conversations with future prospects.
To add on to your statement that the BEAM is vastly underrated, it's the only runtime that I know of that has enabled multiple small sub 50 people teams to scale to millions of daily active users.
I second this. We serve 500 million requests a month ourselves for one of our clients. We are a sub-20 people team. Previously we were on Ruby (which is awesome to get something out there really quickly) and we were on NodeJS / SvelteKit prior to that. I view all of these platforms as tools like hammers and chisels. You should use the right one for the right use case. We still use all 3, but 90% is now in Elixir, 8% about in SvelteKit, maybe 1% in RoR.
I've been building with Elixir for the last 5 years, and comparing to Python or NodeJS - it's a breath of fresh air. Such decent developer experience, so easy to understand how your system works because of actors and well-defined primitives. If you haven't - you definitely should give it a try!
In an AI era I feel like the tiny amount of Elixir code in comparison with other languages, but all that code having higher signal to noise ration vastly contributed to how good LLM-written Elixir code is. Especially in a well-maintainted codebase that relies on a decent db schema and Oban.
It judging by many microbenchmarks and my prior experience with RabbitMQ and a few Erlang systems, performance has often been a bottleneck. I’m happy that it’s got some typing. I’ll take it to ChatGPT about that. But I’m wondering about real world experience. Could you share more? I don’t have any meaningful experience with Erlang hands on and have never used Elixir. But I’d be fancy to try.
In real world, we currently serve 20M+ visitors and 500M+ requests with Elixir. Previously the client was on PHP, let's just say they have the lowest cost compared to any of the competitors in the region just by switching to Elixir.
All that gloating aside, a lot of it depends on the architecture itself, which Elixir allows you to do eloquently. Just the fact that there are no object oriented concepts alone should be reason enough to use Elixir.
Appreciate that. What’s the infrastructure that it runs on? Is it about delegating the workloads to BEAM as opposed to something like Node.js scaled on AWS Lambdas or a single large JDK container or VM instance for a Spring project where the JVM would serve the requests?
I'd been looking for an excuse to build something in Elixir pretty much since the language was released, and in just the past year and a half I've had the good fortune of getting to use it to build an in house AI agent orchestration system and it's been an absolute pleasure
There will be winners in this new AI age, and they will be the ones that are technically better, rather than the most popular or convenient for software engineers
I use Elixir (BEAM-based), with LLMs daily, on a fast growing site (current pace is 5 billion HTTP queries per year on a single front node).
Elixir is well handled by LLMs at this point, and we keep the “easy to scale and maintain” aspect (operational characteristics of the runtime are very good).
I can safely say that total cost of ownership is excellent.
Fair enough! That’s how we track growth, it’s easier to communicate to state sponsors.
Roughly 14M per day, but growing 2.5 to 3 on a YoY basis.
My point stands though: much more serenity in ops with that growth, and LLMs do work nicely, especially when most of the code is stateless by language design.
~160 req/s on average, higher peaks. Not trying to impress anyone though, my point was that LLMs work perfectly fine on an Elixir codebase, and that we get future-proof ops on a single node with zero issues. In my experience, it has been easier than with any other stack I've tried.
To be fair, Erlang runtime semantics (cooperative "async threads") never needed changing to tackle C10k problem, they got that right as part of the early designs before C10k was even on the tasble.
Everyone else just moved towards the same kind of cooperative thread runtime semantics (even if the lingo differs, async in C#, callbacks first, then async in NodeJS, virtual threads in Java, async in C++ and so on).
I think an interesting historical tidbit is that Java actually had M:N threading in early versions (1.1, 1.2, 1.3?) that got ripped out because it was deemed complex and error-prone since people back then often did native bindings naively (And perhaps file/socket API's were less complete?) only for them to reintroduce it again in Java 20 I think it was under the virtual threads name?
Preemptive, not cooperative. Cooperative scheduling is based on the programmer releasing control manually (much like manual memory management) or having yield points baked into the language, often at function calls or IO events. Processes on BEAM can be preempted, they're allotted a number of "reductions" and when those are exhausted, the process is preempted (the process may also yield earlier). This guarantees every process is able to get a turn. Cooperative is fine, but it requires much more care. Go prior to 1.14 suffered from cooperative scheduling, a tight loop (not that you would write this one specifically, but as an example) could hog an OS thread:
i := 0
for {
i++
}
The conceptual equivalent in Erlang would only execute the loop so many times before it would be preempted.
> Cooperative scheduling is based on the programmer releasing control manually (much like manual memory management) or having yield points baked into the language, often at function calls or IO events. Processes on BEAM can be preempted, they're allotted a number of "reductions" and when those are exhausted, the process is preempted (the process may also yield earlier).
BEAM is actually cooperative [1]. In userspace, function calls and IO (receive and !) are yield points, and reduction count is simply the number of function calls you're allowed. Something something Computer Sciencey reduction basically just means function call.
It just feels preemptive because as a functional language, you have to call functions all the time. Your loop becomes
loop(I) -> loop(I + 1).
And that will yield once it calls itself enough times.
Native code (NIFs, BIFs, and drivers) has to count reductions and/or yield explicitly.
You can easily hog scheduler threads in native code that does significant computation or does i/o without using the runtime facilities; most people who write NIFs will mess it up at least once, I certainly have. It's much harder to do in userspace code, but if you fully unroll a very large loop that does not call functions or anything else that can yield, you can do it; I don't see how this would happen without specific intent, I've never run into it, but it's possible.
A blocked scheduler can be vaguely fine or terrible depending on how long it stays blocked and what the system is up to. Often it won't be terrible until you do something that needs all schedulers to synchronize, such as operations around code loading, but you can probably wedge the whole BEAM if you try.
[1] there's not really any other sane way to build preemptive userland threading anyway. You could schedule a timer signal, and task switch in the timer signal handler... but no sane person will recommend doing non-trivial work in a signal handler.
I'm not 100%, but i don't think think get(...) is charged any reductions, so it won't yield. gc_bif potentially does garbage collection which does get charged reductions and BEAM can yield during gc.
I'm just adding at the end to do something with the values, so the compiler doesn't ignore them. If you wanted to write BEAM assembly, you can probably make a terrible function there without nearly as much work. smallnum math doesn't usually charge reductions from what I was seeing, so you could probably do something almost useful with that.
My recollection is that M:N ("green") threads were pulled out because in the absence of a runtime that can guarantee a thread is never blocked, you essentially have to reintroduce all the complexity of an OS scheduler without the benefits of OS-wide context.
"Project Loom" spent much of its time (AFAICT) figuring out how to make every
Java system-level API non-blocking without breaking backward compatibility. The only one they didn't get to for the initial release was Java's "interesting" built-in `synchronized` object monitor construct.
bcantrill has strong opinions on M:N and have always been curious what he thinks about Loom.
10k requests/sec is not really the same thing as the C10k problem which is 10k concurrent connections. You might serve much more or less than one request per second per connection, so req/s and conncurrent connections are not really related.
I also wouldn't say OTP was built to overcome the C10k problem. It is a very good solution for lots of concurrent connections (with a suitable OS), but OTP was built to manage concurrency needs in the context of telephone switches. The constraints chosen to meet those needs does work (very) well to meet the challenges of large numbers of concurrent IP connections, but I wouldn't say it was built to overcome those challenges.
The important metrics is peak requests per second. 14M / day sounds impressive, but it’s only a sustained 160 per second which literally any language and framework can handle on the lowest end hardware.
Sure, peaks are much higher. But a good part of it is reverse proxying with in-memory caching and locking, which I've implemented in several stacks over the years, and many struggle with it (I have been paid to do comprehensive benchmarks on the topic). On the BEAM it's a non-event.
Even pre ai you could toss together a decent cache in about a minute using ets or similar, and faster with a solution like cachex (which was basically what you'd build yourself but packaged into a neat little bundle)
Sure, that's why I highlighted peaks as a more important metric than average. Even an order of magnitude higher peaks over the average is still fairly trivial, compared to where BEAM shines. Maintaining hundreds of thousands of concurrent long-lived connections per server is what it excels at, but also not something that's relevant to 99% of web apps out there.
> I have done several experiments where, the only difference are a couple of paragraphs describing the technology stack for the AI tools.
The differences emerge as the project goes on. Comparing feature velocity and defect count is difficult unless your experiment is to build the same project over a long timeframe across several languages. Then winners would indeed emerge, but then that's only in the context of whatever domain said project is tackling. For instance, if concurrency or infrastructure isn't a large part of the complexity, BEAM languages might not confer much benefit.
Immutable value languages like BEAM langs are probably useful too, the LLM doesn't have to guess whether a value passed into a function will get mangled by the callee
Oh wow, I never knew Klarna uses BEAM. I always thought BEAM is vastly underrated and it's even hard for me to convince clients of showing how powerful it (Elixir/BEAM) really is, especially when they're already on something like NodeJS. This will be a nice reference to use in conversations with future prospects.
I'm interested to see how this goes, I find it difficult to convince anyone to change from the herp-derp status quo.
One of the early investors worked for Ericsson on Erlang.
Klarna was one of the biggest Erlang shops in 2010s. Then there was some turmoil, but their main system is still in Erlang
Wow, I didn't know this. Thanks for sharing!
To add on to your statement that the BEAM is vastly underrated, it's the only runtime that I know of that has enabled multiple small sub 50 people teams to scale to millions of daily active users.
I second this. We serve 500 million requests a month ourselves for one of our clients. We are a sub-20 people team. Previously we were on Ruby (which is awesome to get something out there really quickly) and we were on NodeJS / SvelteKit prior to that. I view all of these platforms as tools like hammers and chisels. You should use the right one for the right use case. We still use all 3, but 90% is now in Elixir, 8% about in SvelteKit, maybe 1% in RoR.
I've been building with Elixir for the last 5 years, and comparing to Python or NodeJS - it's a breath of fresh air. Such decent developer experience, so easy to understand how your system works because of actors and well-defined primitives. If you haven't - you definitely should give it a try!
In an AI era I feel like the tiny amount of Elixir code in comparison with other languages, but all that code having higher signal to noise ration vastly contributed to how good LLM-written Elixir code is. Especially in a well-maintainted codebase that relies on a decent db schema and Oban.
I think the new typing system will also help immensely. LLMs do like their nice clear error messages
It judging by many microbenchmarks and my prior experience with RabbitMQ and a few Erlang systems, performance has often been a bottleneck. I’m happy that it’s got some typing. I’ll take it to ChatGPT about that. But I’m wondering about real world experience. Could you share more? I don’t have any meaningful experience with Erlang hands on and have never used Elixir. But I’d be fancy to try.
In real world, we currently serve 20M+ visitors and 500M+ requests with Elixir. Previously the client was on PHP, let's just say they have the lowest cost compared to any of the competitors in the region just by switching to Elixir.
All that gloating aside, a lot of it depends on the architecture itself, which Elixir allows you to do eloquently. Just the fact that there are no object oriented concepts alone should be reason enough to use Elixir.
Appreciate that. What’s the infrastructure that it runs on? Is it about delegating the workloads to BEAM as opposed to something like Node.js scaled on AWS Lambdas or a single large JDK container or VM instance for a Spring project where the JVM would serve the requests?
just picked up a copy
I'd been looking for an excuse to build something in Elixir pretty much since the language was released, and in just the past year and a half I've had the good fortune of getting to use it to build an in house AI agent orchestration system and it's been an absolute pleasure
There will be winners in this new AI age, and they will be the ones that are technically better, rather than the most popular or convenient for software engineers
I use Elixir (BEAM-based), with LLMs daily, on a fast growing site (current pace is 5 billion HTTP queries per year on a single front node).
Elixir is well handled by LLMs at this point, and we keep the “easy to scale and maintain” aspect (operational characteristics of the runtime are very good).
I can safely say that total cost of ownership is excellent.
I think that's the first time I see someone use qpy...
Fair enough! That’s how we track growth, it’s easier to communicate to state sponsors.
Roughly 14M per day, but growing 2.5 to 3 on a YoY basis.
My point stands though: much more serenity in ops with that growth, and LLMs do work nicely, especially when most of the code is stateless by language design.
Being generous... That's about 10k/s? So you're just in range of the C10k problem that OTP was built to overcome?
~160 req/s on average, higher peaks. Not trying to impress anyone though, my point was that LLMs work perfectly fine on an Elixir codebase, and that we get future-proof ops on a single node with zero issues. In my experience, it has been easier than with any other stack I've tried.
To be fair, Erlang runtime semantics (cooperative "async threads") never needed changing to tackle C10k problem, they got that right as part of the early designs before C10k was even on the tasble.
Everyone else just moved towards the same kind of cooperative thread runtime semantics (even if the lingo differs, async in C#, callbacks first, then async in NodeJS, virtual threads in Java, async in C++ and so on).
I think an interesting historical tidbit is that Java actually had M:N threading in early versions (1.1, 1.2, 1.3?) that got ripped out because it was deemed complex and error-prone since people back then often did native bindings naively (And perhaps file/socket API's were less complete?) only for them to reintroduce it again in Java 20 I think it was under the virtual threads name?
> I think an interesting historical tidbit is that Java actually had M:N threading in early versions (1.1, 1.2, 1.3?)
Yeah, they called that “green threads”. https://en.wikipedia.org/wiki/Green_thread
> Erlang runtime semantics (cooperative "async threads")
Preemptive, not cooperative. Cooperative scheduling is based on the programmer releasing control manually (much like manual memory management) or having yield points baked into the language, often at function calls or IO events. Processes on BEAM can be preempted, they're allotted a number of "reductions" and when those are exhausted, the process is preempted (the process may also yield earlier). This guarantees every process is able to get a turn. Cooperative is fine, but it requires much more care. Go prior to 1.14 suffered from cooperative scheduling, a tight loop (not that you would write this one specifically, but as an example) could hog an OS thread:
The conceptual equivalent in Erlang would only execute the loop so many times before it would be preempted.
> Cooperative scheduling is based on the programmer releasing control manually (much like manual memory management) or having yield points baked into the language, often at function calls or IO events. Processes on BEAM can be preempted, they're allotted a number of "reductions" and when those are exhausted, the process is preempted (the process may also yield earlier).
BEAM is actually cooperative [1]. In userspace, function calls and IO (receive and !) are yield points, and reduction count is simply the number of function calls you're allowed. Something something Computer Sciencey reduction basically just means function call.
It just feels preemptive because as a functional language, you have to call functions all the time. Your loop becomes
And that will yield once it calls itself enough times.
Native code (NIFs, BIFs, and drivers) has to count reductions and/or yield explicitly.
You can easily hog scheduler threads in native code that does significant computation or does i/o without using the runtime facilities; most people who write NIFs will mess it up at least once, I certainly have. It's much harder to do in userspace code, but if you fully unroll a very large loop that does not call functions or anything else that can yield, you can do it; I don't see how this would happen without specific intent, I've never run into it, but it's possible.
A blocked scheduler can be vaguely fine or terrible depending on how long it stays blocked and what the system is up to. Often it won't be terrible until you do something that needs all schedulers to synchronize, such as operations around code loading, but you can probably wedge the whole BEAM if you try.
[1] there's not really any other sane way to build preemptive userland threading anyway. You could schedule a timer signal, and task switch in the timer signal handler... but no sane person will recommend doing non-trivial work in a signal handler.
Show me how you'd unroll a loop which doesn't call functions in userspace.
I don't have a great understanding of the beam opcodes, but something like this:
But go out to I dunno a million (you might want to raise your atom limit) and I think it'll block a scheduler for a while. Beam Assembly for that is:
I'm not 100%, but i don't think think get(...) is charged any reductions, so it won't yield. gc_bif potentially does garbage collection which does get charged reductions and BEAM can yield during gc.
I'm just adding at the end to do something with the values, so the compiler doesn't ignore them. If you wanted to write BEAM assembly, you can probably make a terrible function there without nearly as much work. smallnum math doesn't usually charge reductions from what I was seeing, so you could probably do something almost useful with that.
My recollection is that M:N ("green") threads were pulled out because in the absence of a runtime that can guarantee a thread is never blocked, you essentially have to reintroduce all the complexity of an OS scheduler without the benefits of OS-wide context.
"Project Loom" spent much of its time (AFAICT) figuring out how to make every Java system-level API non-blocking without breaking backward compatibility. The only one they didn't get to for the initial release was Java's "interesting" built-in `synchronized` object monitor construct.
bcantrill has strong opinions on M:N and have always been curious what he thinks about Loom.
10k requests/sec is not really the same thing as the C10k problem which is 10k concurrent connections. You might serve much more or less than one request per second per connection, so req/s and conncurrent connections are not really related.
I also wouldn't say OTP was built to overcome the C10k problem. It is a very good solution for lots of concurrent connections (with a suitable OS), but OTP was built to manage concurrency needs in the context of telephone switches. The constraints chosen to meet those needs does work (very) well to meet the challenges of large numbers of concurrent IP connections, but I wouldn't say it was built to overcome those challenges.
The important metrics is peak requests per second. 14M / day sounds impressive, but it’s only a sustained 160 per second which literally any language and framework can handle on the lowest end hardware.
Sure, peaks are much higher. But a good part of it is reverse proxying with in-memory caching and locking, which I've implemented in several stacks over the years, and many struggle with it (I have been paid to do comprehensive benchmarks on the topic). On the BEAM it's a non-event.
Seriously
Even pre ai you could toss together a decent cache in about a minute using ets or similar, and faster with a solution like cachex (which was basically what you'd build yourself but packaged into a neat little bundle)
Only if users are nice and spread the load evenly over the day, but they are never so considerate.
Sure, that's why I highlighted peaks as a more important metric than average. Even an order of magnitude higher peaks over the average is still fairly trivial, compared to where BEAM shines. Maintaining hundreds of thousands of concurrent long-lived connections per server is what it excels at, but also not something that's relevant to 99% of web apps out there.
Elixir is perfect for the ai age as it allows you to move a lot of orchestration safely into the application layer.
All 3GL languages are looser in the AI age, beyond formal verification tooling.
I have done several experiments where, the only difference are a couple of paragraphs describing the technology stack for the AI tools.
Hardly any different than changing the target CPU architecture on a compiler output.
Quite sad, as now the fun of why language X instead of Y is gone.
> I have done several experiments where, the only difference are a couple of paragraphs describing the technology stack for the AI tools.
The differences emerge as the project goes on. Comparing feature velocity and defect count is difficult unless your experiment is to build the same project over a long timeframe across several languages. Then winners would indeed emerge, but then that's only in the context of whatever domain said project is tackling. For instance, if concurrency or infrastructure isn't a large part of the complexity, BEAM languages might not confer much benefit.
> BEAM languages might not confer much benefit.
Immutable value languages like BEAM langs are probably useful too, the LLM doesn't have to guess whether a value passed into a function will get mangled by the callee
I love Erlang, Elixir, and BEAM by extension. Lucky to have been forced to learn it at KTH.
The second edition is out now. https://happihacking.com/resources/the-beam-book/
Life in Sweden is a series of moments between fika:)
Perhaps github should read it
Why? Historically they're mostly a Ruby shop.
Right, then a 15ms pause for them is just a method lookup :)
thank you. I've not had time to learn Erlang but always been intrigued by beamp + otp. now with llms I might have a chance to see what's possible
Ufufkneklr
Did he write the book? Because he didn't write this post.