ETS (Erlang Term Storage) is an in-memory table facility for sharing Erlang terms between BEAM processes without routing every operation through a dedicated server. It is an optimization: GenServer or Agent can express the same state management, but ETS can remove a single-process bottleneck in suitable workloads. section 10.0 section 10.3
Use ETS effectively by understanding its ownership, access, and concurrency boundaries:
- Create tables with :ets.new/2; identify them by a reference or, with :named_table, by a global atom name. section 10.3.1
- Allow multiple processes to read and write concurrently; concurrent writes to the same row are safe, with the last write winning. section 10.3.1
- Remember that ETS copies terms into and out of separate table memory, so reads return snapshots and table data does not pressure the garbage collector. section 10.3.1
- Keep the owner process alive: terminating it reclaims the table, while otherwise an unreachable table continues occupying memory. section 10.3.1
- Prefer key-based operations for speed; use :ets.tab2list/1, traversal, or match patterns only when non-key queries require them. section 10.3.3
Removing the GenServer bottleneck
A key-value store can retain a GenServer solely as the supervised owner of a public, named ETS set. Callers invoke :ets.insert/2 and :ets.lookup/2 directly, allowing independent clients to operate simultaneously. In the demonstrated benchmark, the GenServer version reached about 953,182 operations per second, while the ETS version reached about 5,700,062; with 10,000 clients and repeated updates, the ETS version reached about 52,009,220 operations per second. section 10.3 section 10.3.2
Listing 10.8 Creating the ETS table (key_value/lib/ets_key_value.ex)
defmodule EtsKeyValue do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, nil, name: __MODULE__) #1
end
def init(_) do
:ets.new( #2
__MODULE__,
[:named_table, :public, write_concurrency: true]
)
{:ok, nil}
end
...
end
Shows the supervised owner pattern: initialize a named, public ETS table with write concurrency enabled. section 10.3.2
defmodule EtsKeyValue do
...
def put(key, value) do
:ets.insert(__MODULE__, {key, value}) #1
end
def get(key) do
case :ets.lookup(__MODULE__, key) do #2
[{^key, value}] -> value #3
[] -> nil #4
end
end
...
end
Shows direct caller-side insertion and lookup, including conversion of the set lookup result into a value or nil. section 10.3.2
Match patterns and process registries
A :bag table can store several rows under one key, such as appointments grouped by date. :ets.match_object/2 can then retrieve rows by value criteria, such as every appointment named "Dentist". ETS also supports process registries: a GenServer owns a public table while clients perform registration and lookup directly, leaving the server to manage lifecycle and cleanup. section 10.3.3 section 10.3.4
