11/28/2025 6 min read

why erlang makes concurrency look easy

erlang's approach to concurrency is different from everything else. let me tell you why it's actually brilliant.

most languages make concurrency feel like juggling chainsaws. erlang? it’s like the language was built for this shit from day one.

i’ve been working with erlang for a while now, and every time i write concurrent code in other languages, i find myself missing erlang’s approach. let me break down why erlang’s concurrency model is actually genius.

the actor model: processes that don’t share shit

erlang uses the actor model, which means every process is isolated. they don’t share memory. they don’t step on each other’s toes. they just send messages back and forth.

% spawn a process
Pid = spawn(fun() -> 
    receive
        {hello, From} ->
            io:format("got hello from ~p~n", [From]),
            From ! {reply, self()}
    end
end).

% send it a message
Pid ! {hello, self()}.

% wait for reply
receive
    {reply, Pid} ->
        io:format("got reply from ~p~n", [Pid])
end.

this is clean. no locks. no mutexes. no shared state to worry about. each process is its own little world.

lightweight processes that actually scale

in most languages, threads are expensive. you can’t just spawn thousands of them. erlang processes? they’re lightweight as hell. you can spawn millions of them.

% spawn 100,000 processes? no problem
spawn_many(0) -> ok;
spawn_many(N) ->
    spawn(fun() -> 
        timer:sleep(1000),
        io:format("process ~p done~n", [N])
    end),
    spawn_many(N - 1).

try doing that with threads in java or python. your machine will cry. erlang? it handles this like it’s nothing.

”let it crash” is actually a strategy

this is where erlang gets interesting. in other languages, you try to prevent crashes. in erlang, you let things crash and recover.

% a process that might crash
worker() ->
    receive
        {do_work, Data} ->
            % might crash here, who cares?
            process_data(Data),
            worker();
        stop ->
            ok
    end.

% supervisor that restarts it
supervisor() ->
    process_flag(trap_exit, true),
    Pid = spawn_link(fun worker/0),
    receive
        {'EXIT', Pid, Reason} ->
            io:format("worker crashed: ~p, restarting...~n", [Reason]),
            supervisor()  % restart
    end.

the idea is: if something goes wrong, just restart it. don’t try to handle every possible error. let it fail, catch it, restart it. this is how erlang systems achieve “nine nines” reliability.

pattern matching everywhere

pattern matching in erlang is beautiful. it’s not just for function heads - you use it everywhere.

% function heads
factorial(0) -> 1;
factorial(N) when N > 0 -> N * factorial(N - 1).

% in receive blocks
handle_message({login, Username, Password}) ->
    authenticate(Username, Password);
handle_message({logout, UserId}) ->
    logout_user(UserId);
handle_message({send_msg, From, To, Msg}) ->
    route_message(From, To, Msg);
handle_message(Unknown) ->
    io:format("unknown message: ~p~n", [Unknown]).

this makes code readable. you see the pattern, you know what it does. no nested if-else chains. no switch statements. just patterns.

hot code swapping: update without downtime

this is wild. erlang lets you update code while it’s running. no downtime. no restart. just swap the code.

% old version
module_v1() ->
    io:format("version 1~n").

% update to new version
% compile new code
c(module).

% old processes keep running old code
% new processes use new code
% you can gradually migrate

telecom systems use this. they can’t just restart to deploy updates. erlang lets them update code while handling millions of calls. that’s the kind of reliability that matters.

OTP: the framework that makes it all work

OTP (Open Telecom Platform) is erlang’s standard library and framework. it gives you:

  • gen_server - generic server processes
  • gen_statem - state machines
  • supervisor - process supervision trees
  • application - application structure
% a gen_server example
-module(my_server).
-behaviour(gen_server).

init([]) ->
    {ok, #{count => 0}}.

handle_call(increment, _From, State) ->
    NewCount = maps:get(count, State) + 1,
    {reply, NewCount, State#{count => NewCount}}.

handle_call(get_count, _From, State) ->
    {reply, maps:get(count, State), State}.

this is the pattern. you don’t write raw processes for everything. you use OTP behaviors. they handle all the boilerplate - message handling, error recovery, state management.

where erlang actually shines

erlang isn’t for everything. but for what it’s good at, nothing else comes close:

  • telecom systems - whatsapp, wechat use erlang
  • chat systems - discord uses elixir (erlang VM)
  • gaming servers - real-time multiplayer games
  • financial trading - low latency, high reliability
  • distributed systems - systems that need to stay up

the common thread? systems that need to handle millions of concurrent connections, stay up 99.999% of the time, and recover from failures gracefully.

the functional programming angle

erlang is functional. no mutable state. everything is immutable. this makes reasoning about concurrent code easier.

% no mutation, just transformation
process_list([]) -> [];
process_list([H|T]) ->
    [transform(H) | process_list(T)].

% state is passed around, not mutated
loop(State) ->
    receive
        {update, NewState} ->
            loop(NewState);  % new state, not mutation
        {get, From} ->
            From ! State,
            loop(State)
    end.

when you can’t mutate state, you can’t have race conditions from shared mutable data. it’s that simple.

why other languages struggle

most languages bolt concurrency on later. java added threads. python added asyncio. javascript added promises and async/await. but the core language wasn’t designed for it.

erlang was designed from the ground up for concurrency and fault tolerance. it shows. the syntax, the runtime, the tooling - everything is built around these concepts.

the learning curve

erlang has a learning curve. pattern matching, immutability, the actor model - it’s different. but once it clicks, writing concurrent code becomes natural.

you stop thinking about locks and semaphores. you think about processes and messages. you stop trying to prevent every possible error. you design for failure and recovery.

the takeaway

erlang’s concurrency model isn’t just different - it’s fundamentally better for certain problems. lightweight processes, message passing, “let it crash”, hot code swapping - these aren’t features you can just add to another language. they’re built into erlang’s DNA.

if you’re building systems that need to handle massive concurrency, stay up forever, and recover from failures, erlang (or elixir) is worth learning. it’ll change how you think about concurrent programming.

tl;dr: erlang makes concurrency feel natural. lightweight processes, message passing, and “let it crash” philosophy make it perfect for systems that need to scale and stay reliable. once you get it, other languages feel clunky.