1 server. 22 lakh connections.
In January 2012 a single WhatsApp server held more than 2.2 million open connections at the same time. This is how: the problem every chat server hits, why Erlang fits it, the bottlenecks WhatsApp's team had to hunt down, and what it all added up to by 2014.
- TL;DR and key numbers
- Persistent connections, from zero
- The problem: C10K
- Step 1: why Erlang
- How a connection lives on the server
- Step 2: hunting the bottlenecks
- The result, 2012 to 2014
- So, what's the secret nobody tells you?
- Erlang vs the other approaches
- Try it yourself
- Caveats and limits
- FAQ
- Glossary and sources
TL;DR
- To deliver messages instantly, a chat app keeps an open TCP connection between your phone and its server. Millions of users online means millions of open connections.
- The classic way to serve connections, one operating-system thread per connection, gets expensive fast. In 1999 even 10,000 connections on one server was a famous challenge: the C10K problem.
- WhatsApp's server is written in Erlang, a language built for telephone networks. Erlang processes are tiny (about 2.6 KB when created) and an idle one uses no CPU, so one server can hold a huge number of them.
- The team then measured and removed bottlenecks: past about 425,000 connections, CPU cores were queueing on shared locks. They patched the Erlang VM, tuned FreeBSD, and went from 1 million to 2 million connections per server in about a month.
- By early 2014 WhatsApp had 465 million monthly users on about 550 servers. Facebook bought it for about $19 billion, when it had roughly 35 engineers.
Persistent connections, from zero
There are two ways a phone can find out it has a new message. It can ask the server every few seconds ("anything for me?"), which wastes battery and data and still arrives late. Or it can keep one connection open and let the server push the message the moment it arrives. Chat apps use the second way.
The catch is on the server. Every connected phone is an open socket the server must keep track of: some kernel memory for its buffers, a file descriptor, and whatever the application keeps per user. Most of those connections are idle most of the time (people aren't typing every second), but they all have to stay open and ready.
So the real question for a chat server isn't "how fast can it process messages?" but "how many mostly-idle connections can one machine hold, cheaply, while still reacting instantly when one of them speaks?"
The problem: C10K
In 1999, Dan Kegel's page "The C10K problem" asked how to get one server to handle ten thousand clients at the same time. That year ftp.cdrom.com was serving 10,000 clients at once on a single machine, and it was notable enough to become the page's opening example.
Why the classic way struggles
The straightforward design gives every client its own operating-system thread. It's easy to write: each thread reads, waits and writes as if it were the only client. But each thread needs its own stack, and the operating system has to schedule all of them.
Kegel's example: with 2 MB stacks, a 32-bit process runs out of address space at about 512 threads (1 GB ÷ 2 MB). Many operating systems of the time struggled with more than a few hundred threads.
Scale it to WhatsApp (illustrative): 2,277,845 connections × 2 MB of stack each ≈ 4.5 TB of reserved stack, on a machine with about 100 GB of RAM. Real systems reserve stack lazily and can use smaller stacks, so this overstates the real cost, but it shows why "one thread per connection" doesn't reach millions. (Our arithmetic.)
The C10K page's answer was to stop dedicating a thread to each connection and instead let one thread watch many sockets with an efficient event mechanism, such as kqueue on FreeBSD or epoll on Linux. That's fast, but the code turns into callbacks and state machines that are hard to get right. Erlang offers a third path.
Step 1: why Erlang
Erlang was designed at Ericsson for telephone systems: millions of calls, most of them just sitting there connected, and the system must never go down. That's almost exactly the shape of a chat server.
Processes, not threads
Erlang code runs in processes that belong to the Erlang virtual machine (the BEAM), not to the operating system. They share no memory and talk only by sending each other messages. Because the VM manages them itself, they're extremely light:
A process runs until it waits for a message in a receive. While it waits, the VM doesn't schedule it at all, so idle connections cost memory but no CPU. The VM runs processes on a small pool of scheduler threads, by default one per CPU core.
A waiter at every table
- One OS thread per connection
- Every thread has its own stack, even when its table is quiet
- The kernel has to juggle all of them
A bell on every table
- One tiny Erlang process per connection
- Quiet tables cost almost nothing
- A few waiters (schedulers, one per core) go only where a bell rings
Worked out: 2,277,845 processes × 2,616 bytes ≈ 6 GB at their starting size. That fits comfortably in 100 GB, next to the kernel's socket buffers and the data each user's process grows to hold. (Our arithmetic from the Erlang docs and WhatsApp's numbers; the real per-connection cost is higher than the starting size.)
How a connection lives on the server
WhatsApp's server started from ejabberd, an open-source chat (XMPP) server written in Erlang, and the team later rewrote large parts of it and moved to its own protocol. The general Erlang pattern for a server like this looks like the diagram below. (WhatsApp hasn't published its exact internal design, so treat this as the model Erlang gives you, not a blueprint of their code.)
Phone
Keeps one TCP connection open to the server.
Kernel
FreeBSD holds the sockets and tells the VM which ones have data.
BEAM VM
A few scheduler threads, one per core, run whichever processes have work.
Processes
One small process per connection, asleep in receive until a message arrives.
The pattern in code
A minimal Erlang server that gives every connection its own process (an echo server, for illustration):
%% one Erlang process per connection (illustrative, not WhatsApp's code) -module(echo). -export([start/1]). start(Port) -> {ok, Listen} = gen_tcp:listen(Port, [binary, {active, false}, {reuseaddr, true}]), accept(Listen). accept(Listen) -> {ok, Socket} = gen_tcp:accept(Listen), Pid = spawn(fun() -> receive go -> loop(Socket) end end), ok = gen_tcp:controlling_process(Socket, Pid), Pid ! go, accept(Listen). loop(Socket) -> ok = inet:setopts(Socket, [{active, once}]), receive %% asleep here: no CPU {tcp, Socket, Data} -> gen_tcp:send(Socket, Data), loop(Socket); {tcp_closed, Socket} -> ok end.
Accept
The acceptor takes a new connection and spawns a process for it (about 2.6 KB).
Hand over
The socket is handed to the new process, so its data arrives as Erlang messages to that process.
Sleep
The process waits in receive. Thousands or millions can wait at once, costing no CPU.
Wake
When data arrives, a scheduler runs just that process, it replies, and it goes back to sleep.
Step 2: hunting the bottlenecks
Erlang made millions of connections possible. Getting one machine there still took work. Rick Reed described the journey at Erlang Factory in March 2012: their servers started at about 200,000 connections each, and hit their first bottlenecks at about 425,000.
What was wrong: contention
Reed's summary was "contention, contention, contention". The server had 24 logical CPUs, and at high load the cores spent their time waiting in line for the same locks inside the VM and kernel (examples he gave include the timer wheel and the time-of-day lock). Adding connections stopped adding throughput because the cores were queueing instead of working.
What they changed
- Patched the Erlang VM (BEAM) to remove lock contention. Reed said most of the gains from 200k to 2M came from these fixes.
- Tuned FreeBSD: backported kernel improvements (the TSC time counter and the igb network driver) and raised limits with
sysctlsettings such as socket counts and the TCP hash table size. - Changed the app where shared data caused contention, for example by partitioning it.
The climb, to scale
Bars to scale against the 2.8M peak. The 10,000 bar is so small it barely shows.
The 6 January 2012 snapshot from WhatsApp's blog ("1 million is so 2011"): kern.ipc.numopensockets: 2277845 on one FreeBSD 8.x server with Erlang R14B03, 24 logical CPUs and about 100 GB of RAM, with roughly 42% of the CPU still idle.
The result, 2012 to 2014
One server holds 2,277,845 open connections. WhatsApp's blog calls 1 million "so 2011".
Reed's Erlang Factory talk: 200k → ~425k wall → 1M → 2M about a month later, with a 2.8M unplanned peak.
Facebook agrees to buy WhatsApp for about $19B ($4B cash, about $12B in shares, $3B in restricted stock units). WhatsApp has 450M+ monthly users and about 35 engineers.
Reed's second talk, "That's 'Billion' with a 'B'": 465M monthly users, 147M peak concurrent connections, about 550 servers and 11,000+ cores, 19B messages in and 40B out per day.
Wired: about 50 engineers serving 900M users.
Worked out: 465M users ÷ ~550 servers ≈ 845,000 users per server. 147M peak connections across about 150 chat servers ≈ 1M each, half of what one box proved it could hold in 2012. Reed put it as about 40M users per server engineer. (Our arithmetic from Reed's 2014 figures.)
WhatsApp didn't win scale with headcount. Two choices did most of the work:
The right tool
- Erlang's processes match "millions of mostly idle connections" almost exactly
- Idle users cost memory but no CPU
- Built for systems that must stay up
Measured engineering
- They found the real limit (lock contention) by measuring
- They fixed it at the source: VM patches, kernel tuning
- They kept the stack simple: Erlang + FreeBSD, few moving parts
Erlang vs the other approaches
Today there are several good ways to hold huge numbers of connections. How they compare in general:
| Approach | How it handles connections | Trade-off |
|---|---|---|
| Thread per connection | One OS thread each; the kernel schedules them | Simple code, but memory and scheduling cost grow with every connection |
| Event loop | One thread watches many sockets with epoll / kqueue | Very efficient, but logic turns into callbacks or async code; one slow handler can block others |
| Goroutines | Lightweight runtime threads multiplexed onto OS threads | Write simple blocking code; shared memory needs care |
| Erlang / Elixir processes | Tiny isolated VM processes, message passing, one scheduler per core | Simple per-connection code, idle is nearly free, crashes stay contained |
General characteristics, not a benchmark. Every approach on this list can reach very high connection counts with the right tuning.
Try it yourself
Spawn a million processes in Erlang
Install Erlang (for example brew install erlang on macOS or your Linux package manager), then start a shell with room for two million processes:
$ erl +P 2000000 1> [spawn(fun() -> receive stop -> ok end end) || _ <- lists:seq(1, 1000000)], ok. 2> erlang:system_info(process_count). %% MB used by all processes: 3> erlang:memory(processes) div 1000000.
A million sleeping processes take roughly 2.6 GB at their starting size (1,000,000 × 2,616 bytes), and your CPU stays quiet while they wait. That's the property WhatsApp built on.
The same in Elixir
Elixir runs on the same VM with a friendlier syntax:
$ iex --erl "+P 2000000" iex> Enum.each(1..1_000_000, fn _ -> spawn(fn -> receive do :stop -> :ok end end) end) iex> :erlang.memory(:processes)
Check your machine's connection limits
Before a server can hold millions of sockets, the operating system has to allow it:
# open files allowed for this shell ulimit -n # Linux: system-wide file limit cat /proc/sys/fs/file-max # FreeBSD: socket and file limits sysctl kern.ipc.maxsockets kern.maxfiles
Next step: run the echo server from section 5 (save it as echo.erl, then c(echo). and echo:start(5555). in the shell) and connect a few clients with nc localhost 5555. Each client gets its own process.
Caveats and limits
- The reel rounds down. On camera I said "around 40 crore users" and "500 servers". The sourced figures are 465M (about 46.5 crore) monthly users and about 550 servers.
- 2.6 KB is the starting size. A connection really costs more: the process grows as it holds data, and the kernel keeps buffers for every socket.
- Process per connection is Erlang's model. It's how Erlang servers like ejabberd work, but WhatsApp hasn't published its exact internal design.
- 2012 hardware and software. These numbers are from FreeBSD 8.x and Erlang R14B03. Erlang has changed a lot since; for example, kernel-space polling (kqueue / epoll) has been the default since OTP 21.
- Headcount comes from press reports. "About 35 engineers" at the sale and "about 50" in 2015 come from Wired. You'll also see "32 engineers" online, with no primary source.
- Not "always connected". A phone keeps the connection while the app is active. A backgrounded app can drop it, and push notifications take over.
FAQ
Is an Erlang process the same as an OS process?
No. It lives inside the Erlang VM, starts at about 2.6 KB, and shares nothing with other processes. The operating system only sees the VM and its few scheduler threads.
Does WhatsApp still use Erlang?
WhatsApp's server was built on Erlang through the period covered here (2012–2015). For anything after that, check WhatsApp's own engineering posts, as this breakdown only covers the sources listed.
Why FreeBSD and not Linux?
WhatsApp ran FreeBSD and tuned it heavily (kernel backports, sysctl limits). Reed's talks describe the tuning; Linux can be tuned to similar connection counts.
Can I do this with Node.js or Go?
Yes. Event loops and goroutines can also hold very large numbers of connections. Erlang's advantage is simple per-connection code with cheap idle processes and crash isolation built in.
What is C10K today?
Solved. Modern servers handle far more than 10,000 connections routinely. The term survives as shorthand for "one server, very many connections".
Should I learn Erlang or Elixir?
Elixir is the friendlier entry point today and runs on the same VM, so everything here applies. Erlang is worth reading to understand the VM itself.
Glossary
- TCP connection
- A two-way, ordered channel between two machines that stays open until one side closes it.
- Socket
- The operating system's handle for one connection.
numopensocketscounts them. - OS thread
- A unit of execution scheduled by the operating system, each with its own stack.
- C10K
- "Concurrent 10,000": the 1999 challenge of one server handling ten thousand clients at once.
- Erlang process
- A tiny, isolated unit of work inside the Erlang VM that communicates only by messages.
- BEAM
- The Erlang virtual machine, which Elixir also runs on.
- Scheduler
- A VM thread (by default one per CPU core) that runs Erlang processes that have work to do.
- Contention
- When several cores need the same lock at once and have to wait their turn.
- kqueue / epoll
- FreeBSD's and Linux's ways for one thread to watch many sockets efficiently.
- sysctl
- A command for reading and changing kernel settings, such as socket limits.
- 1 million is so 2011. WhatsApp blog, 6 Jan 2012.
- Scaling to Millions of Simultaneous Connections. Rick Reed, Erlang Factory SF, 30 Mar 2012.
- How WhatsApp grew to nearly 500 million users. High Scalability summary of Rick Reed's Erlang Factory 2014 talk.
- The C10K problem. Dan Kegel.
- Erlang efficiency guide: processes, concurrent programming, erl command and FAQ. erlang.org.
- It all started with ejabberd. ProcessOne blog.
- Facebook to buy WhatsApp for up to $19B. Global News, Feb 2014.
- Why WhatsApp only needs 50 engineers for its 900M users. Wired, Sept 2015 (reposted on the FreeBSD forums).