Reading — step 1 of 7
Learn
~3 min readETS, Applications, with
ETS (Erlang Term Storage) is BEAM's built-in concurrent in-memory key-value store. Used by libraries like Cachex, Phoenix.PubSub, and rate limiters. Programming Elixir treats it as essential for serious BEAM development.
Creating an ETS table
:ets.new(:my_table, [:set, :public, :named_table])
Options:
:set— keys are unique (most common); other options::bag,:duplicate_bag,:ordered_set:public— any process can read/write:protected— owner can write, others read (default):private— only the owner can access:named_table— refer to it by atom (otherwise need the returned reference)
The owning process is whoever called :ets.new/2. If that process dies, the table is destroyed.
Basic operations
# Insert (overwrites for :set):
:ets.insert(:my_table, {"alice", 30})
:ets.insert(:my_table, {"bob", 25})
# Lookup — returns a list of matches:
:ets.lookup(:my_table, "alice")
# [{"alice", 30}]
# Multiple records:
:ets.lookup(:my_table, "missing")
# []
# Delete:
:ets.delete(:my_table, "alice")
# Match — pattern-based query:
:ets.match(:my_table, {:"$1", :"$2"})
# [["bob", 25]]
# Match with conditions:
:ets.match_object(:my_table, {:"_", :_})
# [{"bob", 25}]
# Counter:
:ets.update_counter(:my_table, "counter", 1, {"counter", 0})
:ets.update_counter/4 is atomic — perfect for high-throughput counters without locks.
Use cases
Application caches:
defmodule MyCache do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def init(:ok) do
:ets.new(:my_cache, [:set, :public, :named_table])
{:ok, nil}
end
def get(key) do
case :ets.lookup(:my_cache, key) do
[{^key, value}] -> {:ok, value}
[] -> :error
end
end
def put(key, value) do
:ets.insert(:my_cache, {key, value})
end
end
Much faster than going through a GenServer for every operation — ETS reads/writes are concurrent.
Rate limiting:
def allow?(user_id) do
count = :ets.update_counter(:rate_limits, user_id, 1, {user_id, 0})
count <= 100
end
Phoenix.PubSub uses ETS to track subscribers. Cachex builds on ETS.
Performance
- Reads: nanoseconds, completely concurrent
- Writes: also fast; can configure for high concurrency with
:write_concurrency - No serialization overhead — terms are stored directly
- Memory-only; cleared on VM restart (no persistence)
Caveats
- ETS data is COPIED in/out by default — terms are duplicated when read
- Owner process death destroys the table (unless transferred via
:ets.give_away) - No built-in TTL — use a separate expiry mechanism or a library like Cachex
- Limited query language — use Match Specifications or :ets.fun2ms for complex queries
When to use ETS vs alternatives
- ETS — concurrent in-process key-value store; fast, but data lost on VM restart
- GenServer state — when state is small and access is naturally serial
- Mnesia — distributed/persistent ETS-like store (heavyweight)
- External (Redis, Postgres) — when persistence + cross-node distribution matters
Common mistakes
- Using public ETS without coordination — race conditions for non-atomic ops. Use update_counter, update_element, or wrap in a GenServer.
- Putting huge values into ETS — copies on every read; expensive. Reference larger data via IDs.
- Forgetting owner process death cleanup — table dies with its owner. Make a long-lived process the owner.
- Pattern matching ETS contents naively —
:ets.matchis a separate query API; learn match specifications for production code. - Treating ETS as durable — it isn't. For persistence, write to disk or use a database.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…