Skip to content
Volume E · The TensorCore.si Library

Erlang

Built for systems that never stop.

Erlang was created at Ericsson to run telephone switches that must never go down. Its virtual machine (the BEAM) runs millions of lightweight processes, isolates failures and lets you update code while the system runs. WhatsApp, RabbitMQ and many telecom and messaging systems are built on it.

Prologue

Why learn Erlang?

Erlang's ideas of isolated processes and "let it crash" supervision are exactly what large AI serving systems need for reliability.

  • Massive concurrency: millions of processes on one machine.
  • Fault tolerance: supervisors restart failed processes automatically.
  • Proven: decades of use in systems that need very high uptime.
Prologue

Set up and run your first program

Install Erlang/OTP from your package manager. Use erl for the shell and rebar3 for projects.

terminal
sudo apt install erlang
erlc hello.erl
erl -noshell -s hello start -s init stop
hello.erl
-module(hello).
-export([start/0]).

start() ->
    Name = "Ada",
    io:format("Hello, AI! Welcome to Erlang, ~s.~n", [Name]).
TipVariables start with a capital letter. Statements are separated by commas and functions end with a period.
Chapter 1

Values, atoms and pattern matching

Variables are bound once and never change. Atoms like ok and error are named constants. = is a pattern match.

basics.erl
-module(basics).
-export([run/0]).
run() ->
    Layers = 32,
    Model = "llama3.1",
    {ok, Vram} = {ok, 80},
    [First | Rest] = [h100, h200, b200],
    io:format("~s: ~p layers, ~p GB~n", [Model, Layers, Vram]),
    io:format("~p then ~p~n", [First, Rest]),
    io:format("~.1f GB~n", [8.03e9 * 2 / 1.0e9]).
Chapter 2

Functions and guards

Functions can have several clauses chosen by pattern matching and guards. Loops are written with recursion.

funcs.erl
-module(funcs).
-export([precision/1, sum/1, params/2]).

precision(32) -> "FP32";
precision(16) -> "FP16";
precision(Bits) when Bits < 16 -> "low precision".

sum([]) -> 0;
sum([H | T]) -> H + sum(T).

params(Layers, Width) -> 12 * Layers * Width * Width.
Chapter 3

Lists, maps and higher-order functions

The lists module offers map, filter and fold. List comprehensions and maps (#{}) make data handling concise.

colls.erl
-module(colls).
-export([run/0]).
run() ->
    Vram = #{h100 => 80, h200 => 141, rtx5090 => 32},
    Big = [Name || {Name, Gb} <- maps:to_list(Vram), Gb >= 80],
    Total = lists:sum(maps:values(Vram)),
    Squares = lists:map(fun(X) -> X * X end, lists:seq(1, 5)),
    io:format("~p ~p ~p~n", [Big, Total, Squares]).
Chapter 4

Processes and messages

spawn starts a process; ! sends a message; receive waits for one. Processes share no memory, so there are no locks.

procs.erl
-module(procs).
-export([run/0, worker/0]).

worker() ->
    receive
        {From, {square, N}} -> From ! {self(), N * N}, worker();
        stop -> ok
    end.

run() ->
    Pid = spawn(?MODULE, worker, []),
    Pid ! {self(), {square, 12}},
    receive {Pid, Result} -> io:format("result: ~p~n", [Result])
    after 1000 -> io:format("timeout~n")
    end,
    Pid ! stop.
Chapter 5

OTP: gen_server and supervisors

OTP is Erlang's framework for reliable systems. A gen_server holds state; a supervisor restarts it if it crashes.

counter.erl
-module(counter).
-behaviour(gen_server).
-export([start_link/0, add/1, value/0]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, 0, []).
add(N) -> gen_server:cast(?MODULE, {add, N}).
value() -> gen_server:call(?MODULE, value).

init(N) -> {ok, N}.
handle_cast({add, X}, N) -> {noreply, N + X}.
handle_call(value, _From, N) -> {reply, N, N}.
Part II · AI with Erlang

AI with Erlang

Erlang is not a model-training language, but its strengths fit AI infrastructure: handling huge numbers of concurrent requests, routing jobs to GPU workers and keeping chat services alive. Many teams use Elixir's Nx and Bumblebee on the same BEAM for model serving, and call LLM APIs from Erlang with built-in HTTP support.

The essential libraries

httpc (inets)Built-in HTTP client.
json (OTP 27+)Built-in JSON encode and decode.
Nx / BumblebeeML on the BEAM (via Elixir).
RabbitMQErlang message broker for AI job queues.

Parallel work with processes

Spawn one process per job and gather results, the pattern behind fan-out inference requests.

fanout.erl
-module(fanout).
-export([run/0]).

score(Prompt) -> timer:sleep(100), length(Prompt).   % stand-in for a model call

run() ->
    Parent = self(),
    Prompts = ["hello", "tensor cores", "agents"],
    Pids = [spawn(fun() -> Parent ! {self(), score(P)} end) || P <- Prompts],
    Results = [receive {Pid, R} -> R end || Pid <- Pids],
    io:format("~p~n", [Results]).          % all done in about 100 ms
Part III · Build an AI agent

Build an AI agent in Erlang

OTP 27 added a built-in json module, so with httpc you can write a tool-calling agent using only the standard library.

Your goal→Model plans→Tool call→Your code runs the tool→Result back to model→Final answer
agent.erl
-module(agent).
-export([run/0]).
-define(URL, "http://localhost:11434/v1/chat/completions").

tools() ->
    [#{type => <<"function">>, function => #{name => <<"get_weather">>, description => <<"Weather for a city">>,
       parameters => #{type => <<"object">>, properties => #{city => #{type => <<"string">>}}, required => [<<"city">>]}}}].

chat(Messages) ->
    Body = iolist_to_binary(json:encode(#{model => <<"llama3.1">>, messages => Messages, tools => tools()})),
    {ok, {_, _, Resp}} = httpc:request(post, {?URL, [], "application/json", Body}, [], [{body_format, binary}]),
    #{<<"choices">> := [#{<<"message">> := Msg} | _]} = json:decode(Resp),
    Msg.

loop(_, 0) -> io:format("gave up~n");
loop(Messages, N) ->
    Msg = chat(Messages),
    case maps:get(<<"tool_calls">>, Msg, null) of
        Calls when is_list(Calls), Calls =/= [] ->
            Replies = [begin
                #{<<"city">> := City} = json:decode(maps:get(<<"arguments">>, maps:get(<<"function">>, C))),
                #{role => <<"tool">>, tool_call_id => maps:get(<<"id">>, C),
                  content => <<"It is 24 C and sunny in ", City/binary, ".">>}
              end || C <- Calls],
            loop(Messages ++ [Msg | Replies], N - 1);
        _ -> io:format("~ts~n", [maps:get(<<"content">>, Msg)])
    end.

run() ->
    inets:start(),
    loop([#{role => <<"user">>, content => <<"Should I take an umbrella in Lahore today?">>}], 5).

Run each conversation in its own process so one slow model call never blocks others.

Run it locallyInstall Ollama, run ollama pull llama3.1, and keep it running. The code talks to its OpenAI-compatible API at http://localhost:11434/v1, so the same program also works with vLLM, llama.cpp server or a cloud API by changing the URL, model name and adding an API key.
Part IV · Your path

Erlang learning roadmap

Tick items off as you go. Your progress is saved in this browser.

Beginner

Intermediate

Advanced

Part IV · Your path

Projects to build

Chat serverA TCP chat room with one process per user.
Job queueDistribute work to worker processes.
LLM routerRoute prompts to several model servers with retries.
Reference

Erlang cheat sheet

TaskSyntax
BindX = 5,
Atomok
Tuple{ok, Value}
List comprehension[X*X || X <- L]
SpawnPid = spawn(fun() -> ... end)
Send / receivePid ! Msg, receive M -> M end
Reference

Common mistakes

  • Variables start with uppercase; lowercase words are atoms.
  • Punctuation matters: commas between expressions, semicolons between clauses, a period at the end.
  • Strings in double quotes are lists of characters; use binaries <<"text">> for efficiency.
Reference

Where to go next

On TensorCore.si: Elixir volume, Go volume.

Keyboard: [ previous language · ] next language · / search the library