Erlang
Memory Management
Message Queue
Concurrency
Programming

How do I create a memory bound message queue in Erlang?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Erlang makes it easy to send messages, but it does not give you a built-in mailbox that is strictly capped by memory usage. Every process has a mailbox, and if producers outpace consumers, that mailbox can keep growing until the node is under pressure. The practical solution is to build a bounded queue process with explicit backpressure instead of relying on the raw mailbox as your queue.

Why A Plain Mailbox Is Not Enough

A process mailbox is append-only from the sender's point of view. Any process can send with the ! operator, and the receiver decides when to consume. That is excellent for decoupling, but it means you do not get a safe memory ceiling for free.

There are two separate issues:

  1. The logical queue of work your application wants to hold.
  2. The actual mailbox of the Erlang process that receives messages.

If you let producers fire messages without coordination, the mailbox itself becomes the unbounded queue. A proper design introduces a broker process that accepts or rejects work based on a limit, and it makes producers react to rejection instead of blindly continuing.

A Simple Bounded Queue Process

The example below stores up to Max items in an explicit queue. Producers call push/2 and receive either ok or {error, full}. Consumers call pop/1.

erlang
1-module(bounded_queue).
2-export([start/1, push/2, pop/1]).
3
4start(Max) when is_integer(Max), Max > 0 ->
5    spawn(fun() -> loop(Max, queue:new()) end).
6
7push(Pid, Item) ->
8    Ref = make_ref(),
9    Pid ! {push, self(), Ref, Item},
10    receive
11        {Ref, Reply} -> Reply
12    end.
13
14pop(Pid) ->
15    Ref = make_ref(),
16    Pid ! {pop, self(), Ref},
17    receive
18        {Ref, Reply} -> Reply
19    end.
20
21loop(Max, Q) ->
22    receive
23        {push, From, Ref, Item} ->
24            case queue:len(Q) < Max of
25                true ->
26                    From ! {Ref, ok},
27                    loop(Max, queue:in(Item, Q));
28                false ->
29                    From ! {Ref, {error, full}},
30                    loop(Max, Q)
31            end;
32        {pop, From, Ref} ->
33            case queue:out(Q) of
34                {{value, Item}, NewQ} ->
35                    From ! {Ref, {ok, Item}},
36                    loop(Max, NewQ);
37                {empty, _} ->
38                    From ! {Ref, empty},
39                    loop(Max, Q)
40            end
41    end.

This is runnable and easy to understand, but it bounds item count, not bytes. That is still useful when message sizes are roughly similar.

Adding Real Backpressure

Returning {error, full} is only useful if producers slow down, retry later, or drop work intentionally. Otherwise the system still overloads, just in a different place.

A producer can respond like this:

erlang
1send_with_retry(Pid, Item) ->
2    case bounded_queue:push(Pid, Item) of
3        ok ->
4            ok;
5        {error, full} ->
6            timer:sleep(100),
7            send_with_retry(Pid, Item)
8    end.

That pattern introduces backpressure. In larger systems you may want exponential backoff, dropping low-priority messages, or a credit-based protocol where the consumer grants permission before producers send more work.

What "Memory Bound" Really Means

A fixed item count is not the same as a fixed memory budget. One message might contain a small atom, while another contains a large binary or nested map. If exact memory usage matters, you need an estimate per item and you need to charge that estimate against a budget.

A common approach is to wrap items in metadata such as {Size, Payload} and keep a running total. The size can come from application knowledge, or from a measurement strategy that is good enough for your workload. The important part is to make the limit explicit in your state rather than hoping the runtime will stop mailbox growth for you.

Another subtle point is that the queue server's own mailbox can still grow if thousands of processes send requests simultaneously. If that risk is real, move from pure fire-and-forget sends to a stricter protocol, such as a pool of permits or a demand-driven consumer pipeline.

When To Use OTP Instead

For production systems, you will usually implement this as a gen_server or a supervised stage in a pipeline rather than a raw spawned process. OTP gives you restart strategy, monitoring, and clearer boundaries for overload policies.

If your data flow is already stream-like, demand-based tools such as gen_stage-style designs or explicit producer-consumer acknowledgments are often a better fit than a mailbox pretending to be a queue. The design question is not only "how do I store messages" but also "who is allowed to produce more work right now".

Common Pitfalls

  • Treating a process mailbox as a safe bounded queue. Mailboxes are easy to use, but they do not enforce a memory cap for you.
  • Limiting only message count when payload sizes vary wildly. Count-based limits can still allow large memory spikes.
  • Returning {error, full} without a producer policy. Rejection is pointless unless senders back off, retry, or drop work deliberately.
  • Forgetting that the queue process mailbox can itself fill under heavy fan-in. A bounded internal queue does not magically bound incoming request bursts.
  • Building the mechanism outside supervision. Overload handling matters most when processes crash or restart under pressure.

Summary

  • Erlang does not provide a native mailbox that is strictly memory-bounded.
  • The standard solution is an explicit queue process with accept or reject behavior.
  • A simple implementation can bound item count using the queue module.
  • True memory limits require estimating payload size and enforcing a budget.
  • Backpressure is the essential feature; without it, the system is still effectively unbounded.

Course illustration
Course illustration

All Rights Reserved.