Choosing Between ETS and Mnesia for Erlang State Management
A technical guide on choosing between ETS and Mnesia for Erlang state management, comparing performance, durability, and distribution with a hybrid implementation example.
28 Sept 2025, 09:54 UTC

The State Management Dilemma
In Erlang/OTP, managing state across multiple processes requires moving data out of individual process heaps to avoid bottlenecks and data loss during process crashes. The primary decision is whether to use ETS (Erlang Term Storage) for raw speed or Mnesia for durability and distribution. Choosing the wrong one leads to either unacceptable latency in high-throughput systems or catastrophic data loss during node restarts.
Comparing Storage Options
The following table compares the core characteristics of ETS and Mnesia to help determine the appropriate tool for your specific constraints.
| Feature | ETS | Mnesia |
|---|---|---|
| Latency | Ultra-low (O(1) lookups) | Moderate (Transaction overhead) |
| Persistence | Volatile (RAM only) | Configurable (RAM or Disk) |
| Distribution | Local to one node | Distributed across cluster |
| Consistency | Eventual/Process-based | ACID Transactions |
| Querying | Key-based / Range | Complex (QLC / Match specs) |
Trade-offs and Decision Logic
Use ETS when your primary goal is a high-performance cache or a shared state that can be easily reconstructed. Because ETS is essentially a set of concurrent hash tables, it is the correct choice for session stores or rate-limiters where a node restart simply means a "cold start" for the cache.
Use Mnesia when the data is the "source of truth." If you need to ensure that a record written on Node A is immediately available on Node B, or if you need to perform atomic updates across multiple tables, Mnesia provides the necessary transactional integrity. Note that Mnesia is built on top of ETS; it adds a management layer for distribution and disk synchronization.
Implementation: The Hybrid Cache Pattern
For applications requiring both extreme read performance and durability, a hybrid approach is recommended: use ETS as a "hot" read cache and Mnesia as the persistent backing store.
In this pattern, reads hit the ETS table first. Writes are committed to Mnesia via a transaction and then mirrored to the ETS table to keep the cache warm.
%% Run on an Erlang node with Mnesia initialized
%% Required permissions: Node must be started with -s mnesia
-module(state_manager).
-export([init/0, get_value/1, set_value/2]).
init() ->
%% Create ETS table for fast reads
ets:new(hot_cache, [set, public, named]),
%% Load existing data from Mnesia to ETS
Data = mnesia:all(),
lists:foreach(fun({Key, Val}) -> ets:insert(hot_cache, {Key, Val}) end, Data).
get_value(Key) ->
case ets:lookup(hot_cache, Key) of
[{Key, Val}] -> Val;
[] -> nil
end.
set_value(Key, Val) ->
%% Ensure ACID durability first
mnesia:transaction(fun() ->
mnesia:write({user_data, Key, Val})
end),
%% Update the fast-access cache
ets:insert(hot_cache, {Key, Val}).
Validation and Risks
To verify this implementation, you can use timer:tc/3 to compare the latency of ets:lookup/2 against mnesia:read/3. You should observe that the ETS lookup is significantly faster, especially under concurrent load from multiple spawned processes.
Operational Risks:
- Memory Exhaustion: Both ETS and Mnesia (RAM_COPIES) consume heap-less memory. Monitor your node's memory usage to avoid OOM (Out of Memory) kills.
- Schema Evolution: Mnesia requires careful handling of schema changes during version upgrades. Changing a table definition across a live cluster can lead to synchronization errors if not managed via
mnesia:change_config/2. - Disk I/O: If using
disc_copies, ensure your disk throughput can handle the transaction log writes to avoid blocking the Erlang VM.
Rollback Procedure
Since this operation creates tables and modifies state, use the following to revert changes:
- Delete the ETS table:
ets:delete(hot_cache). - Delete the Mnesia table:
mnesia:delete_table(user_data). - If the Mnesia schema was created for this test, run
mnesia:delete_schema()to wipe the local database directory.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.