Build a Chat Room with Erlang

Spawn Erlang processes and pass messages to build a multi-user chat room.

Introduction

30 Second Summary

Every time you send a message in a group chat, your text reaches everyone else instantly without anyone blocking each other. Behind the scenes, apps like WhatsApp and Discord handle millions of these simultaneous conversations using isolated, lightweight processes that talk to each other by passing messages.

In this project, you will build a terminal-based chat room system using Erlang processes. You will spawn a server process that manages connected clients and broadcasts messages between them, all through Erlang's built-in message passing with zero shared state.

What You'll Build

A multi-process chat room where a server process manages client processes that register, send messages, and receive broadcasts from other users.

By the end of this project, you'll have:

  • A working chat server process that manages connected clients and broadcasts messages in real time.
  • Hands-on experience spawning lightweight Erlang processes and passing messages between them using the ! operator.
  • A chat client module that registers with the server, sends messages, and displays incoming broadcasts from other users.
  • Secret Mission: Add a supervisor process that monitors the server and automatically restarts it if it crashes.

Are there any prerequisites?

You need basic programming experience in any language and comfort with the command line. No prior Erlang knowledge is required.

Before We Start

To build a chat room with Erlang processes, you need the Erlang runtime installed on your machine. Erlang comes with a built-in shell where you can spawn processes, send messages, and see them interact in real time.

You also need Rebar3, Erlang's official build tool. Rebar3 creates your project structure and compiles your code into something the Erlang VM can run.

In this step, get ready to:
  • Install Erlang/OTP and verify it works.
  • Install Rebar3 and verify it works.
  • Create your project structure and confirm it compiles.
Install Erlang/OTP

Erlang/OTP is the runtime that powers your chat room. Every process you spawn, every message you send, runs inside the Erlang VM.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Install Erlang by running the command for your operating system:

macOS

  • Install Erlang using Homebrew by running this command:
brew install erlang

What does this command do?

Homebrew downloads and installs the full Erlang/OTP runtime, including the erl shell, the compiler, and all standard libraries your project needs.

Windows

  • Install Erlang using Chocolatey by running this command in PowerShell (as Administrator):
choco install erlang

What does this command do?

Chocolatey downloads the official Erlang/OTP Windows installer and runs it silently. It also adds the Erlang binaries to your system PATH so you can use them from the command line.

No Chocolatey?

You can also download the Windows installer directly from erlang.org/downloads. After installing, add the Erlang bin directory to your system PATH manually.

Now verify the installation by checking if the erl command works.

  • Check your Erlang version by running this command:
erl -version

✔️ I see a version number

You should see output similar to Erlang (SMP,ASYNC_THREADS) (BEAM) emulator version.... Erlang is installed and ready to go.

ⓧ Command not found

If you see an error, the install may not have finished or your PATH needs updating.

  • On macOS, try closing and reopening your terminal, then run the command again.
  • On Windows, ensure the Erlang bin folder is added to your system PATH, then restart your terminal.

If Homebrew reports that Erlang is already installed, you are good to proceed.

What is Erlang/OTP?

Erlang/OTP is a programming language and runtime designed for building concurrent, fault-tolerant systems. OTP stands for Open Telecom Platform, a set of libraries and design patterns that come bundled with Erlang.

Systems like WhatsApp, RabbitMQ, and Discord use Erlang because it handles millions of lightweight processes communicating through messages.

Install Rebar3

Rebar3 is the official build tool for Erlang. It creates the standard project structure, compiles your modules, and manages dependencies.

macOS

  • Install Rebar3 using Homebrew by running this command:
brew install rebar3

Windows

On Windows, you install Rebar3 from source after Erlang is on your PATH.

  • Download and build Rebar3 by running these commands in PowerShell:
git clone https://github.com/erlang/rebar3.git
cd rebar3
.\bootstrap.bat
  • Install it globally by running:
.\rebar3.cmd local install
  • Add the Rebar3 bin directory to your PATH. The output of the previous command tells you the exact path to add.
  • Verify Rebar3 is installed by running this command:
rebar3 version

✔️ I see a version number

You should see output like rebar 3.27.0 (or similar). Rebar3 is ready.

ⓧ Command not found

If you see an error, Rebar3 may not be on your PATH.

  • On macOS, try closing and reopening your terminal, then run the command again.
  • On Windows, ensure the directory from the local install output is in your system PATH, then restart your terminal.

What is Rebar3?

Rebar3 is the standard build tool for Erlang. It handles project scaffolding, compilation, dependency management, testing, and releases.

Think of it like npm for Node.js or cargo for Rust.

Create the project structure

Rebar3 has a built-in template that generates the standard Erlang project layout. This gives you the correct directory structure, config files, and application metadata.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create a new Erlang project called chat_room by running this command:
rebar3 new app chat_room

What does this command do?

The rebar3 new app chat_room command creates a folder called chat_room on your Desktop with the standard OTP application structure inside.

This includes a src/ directory for your code, a rebar.config file for build settings, and a src/chat_room.app.src file that describes your application to the Erlang VM.

  • Move into the new project folder by running:
cd chat_room
  • Confirm the files were created by running:
ls src

You should see chat_room.app.src listed in the output (along with other generated files).

Now you need to update the generated files to match the project configuration. Rebar3 generates some boilerplate that you do not need for this project.

  • Open the chat_room folder in your preferred text editor.
  • Open rebar.config and replace its contents with this:
{erl_opts, [debug_info]}.
{deps, []}.

What does this file do?

The rebar.config file tells Rebar3 how to build your project. The debug_info option includes debugging metadata in your compiled modules.

The empty deps list means this project has no external dependencies.

  • Open src/chat_room.app.src and replace its contents with this:
{application, chat_room, [
  {description, "A multi-process chat room"},
  {vsn, "0.1.0"},
  {registered, []},
  {applications, [kernel, stdlib]},
  {env, []}
]}.

What does this file do?

The .app.src file is your application's metadata. It tells the Erlang VM the application's name, version, and which other applications it depends on at runtime.

The kernel and stdlib dependencies are always required for any Erlang application.

  • Save both files.
  • Delete any other generated files in the src/ directory (such as chat_room_app.erl and chat_room_sup.erl). You will create your own modules from scratch in the next steps.
  • Compile the project to confirm everything works by running this command:
rebar3 compile

✔️ I see a success message

You should see output ending with ===> Compiling chat_room. Your project compiled successfully.

ⓧ I see an error

If you see a compile error, check the following:

  • Make sure you are inside the chat_room directory when running the command.
  • Confirm your rebar.config has no typos. Erlang config files end each line with a period.
  • Confirm your src/chat_room.app.src matches the code above exactly, including the trailing period.

Still seeing errors?

If you deleted the boilerplate files but still see errors, make sure no .erl files remain in src/ other than the ones you create in later steps. Leftover files that reference modules you removed will cause compile failures.

help me fix a rebar3 compile error in my chat_room project

✔️ Awesome, I've got everything!

Great. Double check you have saved both rebar.config and src/chat_room.app.src.

ⓧ I'd like to double check the full code

Here is the complete rebar.config:

{erl_opts, [debug_info]}.
{deps, []}.

Here is the complete src/chat_room.app.src:

{application, chat_room, [
  {description, "A multi-process chat room"},
  {vsn, "0.1.0"},
  {registered, []},
  {applications, [kernel, stdlib]},
  {env, []}
]}.

Your Erlang environment is ready and your project compiles cleanly. Next up, you will build the chat server module that manages connected clients and broadcasts messages.

Build the Chat Server Module

Your project structure is ready and compiling cleanly. Now it's time to build the core of your chat room: the server process that manages connections and routes messages.

In Erlang, there is no shared memory between processes. Every process has its own mailbox, and communication happens exclusively through message passing. The server you build in this step will receive messages, decide what to do with them using pattern matching, and forward them to the right recipients.

In this step, get ready to:
  • Create a server module that spawns a process and registers it by name.
  • Implement a recursive loop that handles different message types.
  • Build a broadcast function that delivers messages to all connected clients.
Create the chat server module with start and stop functions

The server needs a way to start up and shut down. The start/0 function will spawn a new Erlang process and register it under the name chat_server so other processes can find it without knowing its PID.

  • Create a new file called src/chat_server.erl inside your chat_room project folder.
  • Add the module declaration, exports, and the start/stop functions by pasting this code:
-module(chat_server).
-export([start/0, stop/0]).

% Spawn a new process running loop/1 and register it by name
start() ->
    Pid = spawn(fun() -> loop([]) end),
    register(chat_server, Pid),
    {ok, Pid}.

% Send a stop message to the registered server process
stop() ->
    chat_server ! stop,
    ok.

What does this code do?

spawn(fun() -> loop([]) end) creates a brand new process that runs the loop function with an empty list (no clients connected yet).

register(chat_server, Pid) gives the process a name so anyone can send it messages using chat_server ! Message instead of needing to know its numeric PID. The ! operator is how processes send messages in Erlang.

You won't be able to compile yet because loop/1 doesn't exist. The next chunk adds it.

Implement the server loop with receive blocks

Erlang processes maintain state through recursion. Instead of mutating a variable, the loop function calls itself with an updated client list each time it processes a message.

  • In src/chat_server.erl, add the loop/1 function below the stop/0 function by pasting this code:
loop(Clients) ->
    receive
        % A new client wants to join
        {register, ClientPid, Name} ->
            io:format("Server: ~s joined the chat~n", [Name]),
            loop([{ClientPid, Name} | Clients]);
        % A client is leaving
        {unregister, ClientPid} ->
            NewClients = lists:keydelete(ClientPid, 1, Clients),
            io:format("Server: a user left the chat~n"),
            loop(NewClients);
        % A client sent a message to broadcast
        {send_message, FromPid, Text} ->
            case lists:keyfind(FromPid, 1, Clients) of
                {FromPid, SenderName} ->
                    broadcast(Clients, SenderName, Text, FromPid);
                false ->
                    ok
            end,
            loop(Clients);
        % Shut down the server
        stop ->
            io:format("Server: shutting down~n"),
            ok
    end.

What does this code do?

The receive block pauses the process until a message arrives in its mailbox. Each clause uses pattern matching to handle a different message type.

register adds a client tuple to the list and calls loop again with the updated list. unregister removes the client using lists:keydelete/3. send_message looks up the sender's name with lists:keyfind/3 and calls broadcast.

The stop clause does not call loop again, so the process terminates. Every other clause recurses to keep the server alive.

The module still won't compile because broadcast/4 is called but not yet defined. The next chunk adds it.

Implement the broadcast function

When a client sends a message, the server needs to deliver it to every other connected client. The broadcast function iterates through the client list and sends the message to everyone except the original sender.

  • In src/chat_server.erl, add the broadcast/4 function below the loop/1 function by pasting this code:
broadcast(Clients, SenderName, Text, FromPid) ->
    % Send the message to every client except the sender
    lists:foreach(
        fun({Pid, _Name}) ->
            case Pid of
                FromPid -> ok;
                _ -> Pid ! {new_message, SenderName, Text}
            end
        end,
        Clients
    ).

What does this code do?

lists:foreach/2 iterates through the Clients list. For each client tuple, it checks if the PID matches the sender.

If the PID is the sender, it skips them with ok. Otherwise it uses the ! operator to send a {new_message, SenderName, Text} tuple to that client's mailbox.

  • Save src/chat_server.erl.
  • Compile the project by running:
rebar3 compile
  • You should see output ending with a success message and no errors.

Seeing a compile error?

If you see a "function loop/1 undefined" or "function broadcast/4 undefined" error, make sure all three code sections are in the same file with no missing lines between them.

If you see a syntax error, check that every clause in the receive block ends with a semicolon except the last one (stop), which has no separator before end.

help me fix my chat_server.erl compile error

✔️ Awesome, I've got everything!

Great. Double check you have saved src/chat_server.erl and the compile completed without errors.

ⓧ I'd like to double check the full code

Here is the complete src/chat_server.erl file:

-module(chat_server).
-export([start/0, stop/0]).

start() ->
    Pid = spawn(fun() -> loop([]) end),
    register(chat_server, Pid),
    {ok, Pid}.

stop() ->
    chat_server ! stop,
    ok.

loop(Clients) ->
    receive
        {register, ClientPid, Name} ->
            io:format("Server: ~s joined the chat~n", [Name]),
            loop([{ClientPid, Name} | Clients]);
        {unregister, ClientPid} ->
            NewClients = lists:keydelete(ClientPid, 1, Clients),
            io:format("Server: a user left the chat~n"),
            loop(NewClients);
        {send_message, FromPid, Text} ->
            case lists:keyfind(FromPid, 1, Clients) of
                {FromPid, SenderName} ->
                    broadcast(Clients, SenderName, Text, FromPid);
                false ->
                    ok
            end,
            loop(Clients);
        stop ->
            io:format("Server: shutting down~n"),
            ok
    end.

broadcast(Clients, SenderName, Text, FromPid) ->
    lists:foreach(
        fun({Pid, _Name}) ->
            case Pid of
                FromPid -> ok;
                _ -> Pid ! {new_message, SenderName, Text}
            end
        end,
        Clients
    ).

Your chat server is compiled and ready to accept connections. Next up, you'll build the client module that lets processes register with the server and exchange messages.

Build the Chat Client Module

Your chat server can accept connections, broadcast messages, and remove users. Now you need the other half of the system.

In this step, you'll create a client module where each user is their own Erlang process. Clients register with the server, send messages through it, and display broadcasts from other users.

In this step, get ready to:
  • Create src/chat_client.erl with a start/1 function that spawns a client process.
  • Implement a receive loop that displays incoming messages.
  • Add send/2 and stop/1 functions for sending messages and disconnecting.
Create the client module and start function

Each user in the chat room is a separate process. The start/1 function spawns a new process and immediately tells the server that a new client has joined.

  • Create a new file called chat_client.erl inside the src/ folder of your chat_room project.
  • Add the module declaration, exports, and start/1 function by pasting the following code:
-module(chat_client).
-export([start/1, send/2, stop/1]).

% Spawns a new client process and registers it with the server
start(Name) ->
    Pid = spawn(fun() -> loop(Name) end),
    chat_server ! {register, Pid, Name},
    {ok, Pid}.

What does this code do?

The -module(chat_client) line declares the module name. The -export line makes three functions available to call from outside the module.

Inside start/1, spawn creates a brand new process that runs the loop function. Then the ! operator sends a {register, Pid, Name} message to the server so it knows this client exists.

  • Save chat_client.erl. You won't compile yet because the module still needs its loop/1 function.
Implement the client receive loop

The client process needs to sit and wait for messages from the server. Like the server's loop, this is a recursive function that calls itself after handling each message.

  • Add the loop/1 function below start/1 in chat_client.erl:
% Listens for incoming messages and prints them
loop(Name) ->
    receive
        {new_message, SenderName, Text} ->
            io:format("[~s] ~s: ~s~n", [Name, SenderName, Text]),
            loop(Name);
        stop ->
            io:format("~s: leaving chat~n", [Name]),
            ok
    end.

What does this code do?

The receive block pattern matches on two message types. When a {new_message, SenderName, Text} arrives, the client prints it and loops again to wait for the next message.

When a stop atom arrives, the function prints a goodbye and returns ok. Returning without recursing causes the process to terminate.

  • Save chat_client.erl. The next chunk adds the remaining functions to complete the module.
Add the send and stop functions

Clients need a way to send messages and disconnect gracefully. Both functions work by sending a message to the server using the ! operator.

  • Add the send/2 and stop/1 functions between start/1 and loop/1 in chat_client.erl:
% Sends a message from this client through the server
send(ClientPid, Text) ->
    chat_server ! {send_message, ClientPid, Text},
    ok.

% Unregisters the client from the server and stops the process
stop(ClientPid) ->
    chat_server ! {unregister, ClientPid},
    ClientPid ! stop,
    ok.

What does this code do?

The send/2 function sends a {send_message, ClientPid, Text} tuple to the server. The server looks up the sender's name and broadcasts the text to everyone else.

The stop/1 function does two things. It tells the server to remove the client from its list with {unregister, ClientPid}. Then it sends stop directly to the client process so the loop exits.

  • Save chat_client.erl.
  • Compile the project to verify everything is correct by running this command in your terminal:
rebar3 compile

You should see output starting with ===> Verifying dependencies... followed by ===> Compiling chat_room with no errors or warnings.

Seeing a syntax error?

  • Check that each function ends with a period (.). In Erlang, a period marks the end of a function definition.
  • Make sure clauses inside receive are separated by semicolons (;) and the block ends with end followed by a period.
  • Verify the module name in -module(chat_client). matches the filename chat_client.erl.

help me fix a compile error in my chat_client.erl file

✔️ Awesome, I've got everything!

Great. Double check you have saved chat_client.erl and the compile passed with no errors.

ⓧ I'd like to double check the full code

Here is the complete chat_client.erl file. Make sure yours matches exactly:

-module(chat_client).
-export([start/1, send/2, stop/1]).

start(Name) ->
    Pid = spawn(fun() -> loop(Name) end),
    chat_server ! {register, Pid, Name},
    {ok, Pid}.

send(ClientPid, Text) ->
    chat_server ! {send_message, ClientPid, Text},
    ok.

stop(ClientPid) ->
    chat_server ! {unregister, ClientPid},
    ClientPid ! stop,
    ok.

loop(Name) ->
    receive
        {new_message, SenderName, Text} ->
            io:format("[~s] ~s: ~s~n", [Name, SenderName, Text]),
            loop(Name);
        stop ->
            io:format("~s: leaving chat~n", [Name]),
            ok
    end.

Your client module is ready. In the next step, you'll fire up the Erlang shell and run a live multi-user chat session between your processes.

Run a Multi-User Chat Session

Your chat server and client modules are complete. Now it's time to bring them to life by compiling the code and running a live chat session in the Erlang shell.

In this step, you'll see Erlang's concurrency model in action. Multiple processes will communicate through message passing with zero shared state.

In this step, get ready to:
  • Compile the project and start the Erlang shell.
  • Spawn multiple client processes and send messages between them.
  • Test process isolation by killing a client process.
Compile the project and start the Erlang shell

Before you can run any Erlang code, the source files need to be compiled into bytecode. Rebar3 handles this for you.

  • Make sure your terminal is inside the chat_room project folder.
  • Compile the project by running this command:
rebar3 compile
  • You should see output that includes ===> Compiling chat_room with no errors.

Seeing a compilation error?

Check that your chat_server.erl and chat_client.erl files are saved inside the src/ directory. Rebar3 only compiles files it finds in src/.

If you see a syntax error, double check the module matches the filename. The -module(chat_server). declaration must match the file chat_server.erl exactly.

Help me fix a rebar3 compile error in my Erlang chat room project.

  • Start the Erlang shell with your project loaded by running:
rebar3 shell
  • You should see the Erlang shell prompt 1> appear. Your compiled modules are now available to call.

What is rebar3 shell?

This command starts an interactive Erlang session with all your project modules pre-loaded. You can call any exported function directly without manual compilation steps.

Spawn multiple clients and send messages

With the shell running, you can now start the chat server and create several client processes. Each client is its own lightweight Erlang process with its own mailbox.

  • Start the chat server by running this in the Erlang shell:
chat_server:start().
  • You should see {ok, <0.X.0>} returned, where X is a number. This is the server's process ID.
  • Spawn three client processes by running these commands one at a time:
{ok, Alice} = chat_client:start("Alice").
{ok, Bob} = chat_client:start("Bob").
{ok, Carol} = chat_client:start("Carol").

What's happening here?

Each call to chat_client:start/1 spawns a brand new process and registers it with the server. The pattern match {ok, Alice} captures the client's process ID into a variable you can use later.

After each command, the server prints a join message like Server: Alice joined the chat. You should see three join messages total.

  • Send a message from Alice by running:
chat_client:send(Alice, "Hello everyone!").
  • You should see output from Bob's and Carol's processes:
[Bob] Alice: Hello everyone!
[Carol] Alice: Hello everyone!

Why doesn't Alice see her own message?

The broadcast/4 function in chat_server.erl skips the sender's PID. This prevents the echo effect you'd get if every sender received their own message back.

  • Send a message from Bob to confirm two-way communication:
chat_client:send(Bob, "Hey Alice!").
  • You should see Alice's and Carol's processes print the message:
[Alice] Bob: Hey Alice!
[Carol] Bob: Hey Alice!

Not seeing any output?

Make sure you started the server first with chat_server:start(). before spawning clients. Clients send a register message to chat_server on startup, so the server must exist.

Check that you included the period at the end of each command. The Erlang shell requires a . to terminate every expression.

Help me troubleshoot why my Erlang chat messages aren't showing up.

Test process isolation by killing a client

One of Erlang's most powerful features is process isolation. Each process runs independently, so a crash in one does not affect the others.

  • Kill Carol's process by running:
exit(Carol, kill).
  • The shell returns true. Carol's process is now dead.
  • Verify the server and remaining clients are unaffected by sending another message from Alice:
chat_client:send(Alice, "Is everyone still here?").
  • You should see only Bob's process respond:
[Bob] Alice: Is everyone still here?

Carol's process is gone, but Alice, Bob, and the server kept running without interruption. This is process isolation in action.

Why does this matter?

In most languages, a crash in one thread can bring down the entire application. In Erlang, processes are fully isolated. One process crashing has zero impact on the rest of the system.

This is the foundation of Erlang's reliability model. Systems like WhatsApp and RabbitMQ use this property to stay running even when individual components fail.

Server stopped responding?

If you accidentally killed the server process instead of a client, you'll need to restart it with chat_server:start(). and then re-spawn your clients.

Double check that you used the Carol variable (the client PID) in the exit/2 call, not the atom chat_server.

Help me recover my Erlang chat server after I accidentally killed the wrong process.

Your multi-process chat room is fully operational. You've seen Erlang processes communicate through pure message passing and survive individual failures. Next up, you'll take this resilience further with a supervisor that automatically restarts crashed processes.

Secret mission

Add a Supervisor Process

Your chat server works, but what happens when it crashes? In production Erlang systems, crashes are expected. The solution is not to prevent crashes, but to recover from them automatically. Add a supervisor process that watches the server and restarts it instantly if it goes down.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing costs or cloud resources to worry about.

Decide whether to keep your project files and tools installed, or remove them entirely.

Resources you used:

  • The chat_room project folder on your Desktop.
  • Erlang/OTP installed via Homebrew.
  • Rebar3 installed via Homebrew.

Keep everything running

No action needed. Choose this if you want to keep building Erlang projects or plan to extend this chat room further.

  • Your chat_room folder stays on your Desktop.
  • Erlang and Rebar3 remain installed and available from any terminal session.

Pause - I'll come back to this later

Since everything runs locally and there are no background processes after you close the Erlang shell, there is nothing to pause.

  • If you still have the Erlang shell open, type q(). to quit.
  • Your project files and installed tools stay in place for when you return.

Delete - I don't want to use this again

Remove all project files and uninstall the tools you installed during this project.

  • If you still have the Erlang shell open, type q(). to quit.
  • Delete the project folder by running:

macOS

rm -rf ~/Desktop/chat_room

Windows

rmdir /s /q %USERPROFILE%\Desktop\chat_room
  • Uninstall Rebar3 by running:
brew uninstall rebar3
  • Uninstall Erlang by running:
brew uninstall erlang

What about Homebrew itself?

Homebrew is a general-purpose package manager useful for many other tools. You do not need to uninstall it.

Nice Work!

Nice Work!

You just built a concurrent chat room powered entirely by Erlang processes. Multiple users can send and receive messages in real time, all without a single line of shared mutable state.

You've learned how to:

  • Spawn lightweight Erlang processes and pass messages between them using the ! operator.
  • Use pattern matching in receive blocks to route different message types to different handlers.
  • Build a recursive server loop that maintains state through function calls instead of mutable variables, and demonstrate process isolation where one crash does not take down the system.
  • Secret Mission: Added a supervisor process that monitors the chat server and automatically restarts it on crash, demonstrating Erlang's "let it crash" philosophy.

Ready to quiz yourself?