Redis

CIHex.pmDocsLicense

Modern, full-featured Redis client for Elixir built on OTP.

RESP3 native. Cluster-aware. Client-side caching. Resilience built in. Zero required dependencies.

Installation

def deps do
[
{:redis, "~> 0.8.0", hex: :redis_client_ex}
]
end

The dependency and OTP application are named :redis; hex: :redis_client_ex points Mix at the differently named Hex package. All modules use the Redis namespace.

Connecting

# Basic
{:ok, conn} = Redis.start_link(port: 6379)
# URI
{:ok, conn} = Redis.start_link("redis://:secret@myhost:6380/2")
# With authentication
{:ok, conn} = Redis.start_link(host: "myhost", password: "secret")
# TLS with certificate and hostname verification
{:ok, conn} = Redis.start_link(
host: "myhost",
ssl: true,
ssl_opts: [
verify: :verify_peer,
cacerts: :public_key.cacerts_get(),
server_name_indication: ~c"myhost"
]
)

For backwards compatibility, ssl: true without ssl_opts uses verify: :verify_none. Configure peer verification for production connections.

Supervision

children = [
{Redis.Connection, port: 6379, name: :redis},
{Redis.Connection.Pool, pool_size: 10, port: 6379, name: :redis_pool}
]
Supervisor.start_link(children, strategy: :one_for_one)

Connection Pool

{:ok, pool} = Redis.Connection.Pool.start_link(
pool_size: 10,
port: 6379
)
Redis.Connection.Pool.command(pool, ["GET", "key"])

Commands, Pipelines, and Transactions

{:ok, "OK"} = Redis.command(conn, ["SET", "hello", "world"])
{:ok, "world"} = Redis.command(conn, ["GET", "hello"])
# Pipeline -- multiple commands in a single round-trip
{:ok, ["OK", "OK", "1"]} = Redis.pipeline(conn, [
["SET", "a", "1"],
["SET", "b", "2"],
["GET", "a"]
])
# Transaction -- atomic MULTI/EXEC
{:ok, [1, 2, 3]} = Redis.transaction(conn, [
["INCR", "counter"],
["INCR", "counter"],
["INCR", "counter"]
])

Optimistic Locking (WATCH)

Redis.watch_transaction(conn, ["balance"], fn conn ->
{:ok, bal} = Redis.command(conn, ["GET", "balance"])
new_bal = String.to_integer(bal) + 100
[["SET", "balance", to_string(new_bal)]]
end)

Watches keys, calls your function to read and compute commands, then executes in MULTI/EXEC. Automatically retries on conflict (default 3 attempts).

Typed Responses

Raw RESP values remain the default. Opt into stable, protocol-independent values for commands with known reply shapes:

{:ok, %{"name" => "Ada", "role" => "admin"}} =
Redis.command(conn, ["HGETALL", "user:1"], response: :typed)
{:ok, [%Redis.Response.StreamEntry{id: id, fields: fields}]} =
Redis.command_typed(conn, ["XRANGE", "events", "-", "+"])
{:ok, [hash, clients]} = Redis.pipeline_typed(conn, [
["HGETALL", "user:1"],
["CLIENT", "LIST"]
])

Typed mode parses HGETALL, CONFIG GET, set operations, INFO, CLIENT LIST/CLIENT INFO, and the range/read stream commands. Unknown commands pass through unchanged, including within mixed pipelines. See Redis.Response for the complete return types.

Auto-Pipelining

Opt in to automatic batching when many processes issue commands concurrently:

{:ok, conn} = Redis.start_link(
auto_pipeline: true,
auto_pipeline_window: 1, # collect commands for up to 1 ms
auto_pipeline_max_size: 1_000 # or flush when this size is reached
)
1..100
|> Task.async_stream(fn i ->
Redis.command(conn, ["SET", "key:#{i}", to_string(i)])
end, max_concurrency: 100)
|> Stream.run()

Each caller still receives only its own reply, with its own hooks, telemetry, timeout, and typed-response handling. Explicit pipelines, transactions, and no-reply operations act as ordering barriers and flush queued commands first. The feature is disabled by default. Redis.Connection.Pool passes the options to every physical connection, so each connection builds independent batches.

Command Builders

Pure functions that return command lists. Use them with any connection type.

alias Redis.Commands.{String, Hash, JSON, Search}
String.set("key", "value", ex: 60, nx: true)
#=> ["SET", "key", "value", "EX", "60", "NX"]
Hash.hset("user:1", [{"name", "Alice"}, {"age", "30"}])
#=> ["HSET", "user:1", "name", "Alice", "age", "30"]
JSON.set("doc", %{name: "Alice", scores: [1, 2, 3]})
#=> ["JSON.SET", "doc", "$", "{\"name\":\"Alice\",\"scores\":[1,2,3]}"]
Search.create("idx:users", :json,
prefix: "user:",
schema: [
{"$.name", :text, as: "name"},
{"$.age", :numeric, as: "age", sortable: true}
]
)

21 command modules are available: String, Hash, List, Set, SortedSet, Stream, Key, Server, JSON, Search, Script, Geo, Bitmap, HyperLogLog, Bloom, Cuckoo, TopK, CMS, TDigest, TimeSeries, and PubSub. See the docs for full coverage.

Cluster

{:ok, cluster} = Redis.Cluster.start_link(
nodes: [{"127.0.0.1", 7000}]
)
Redis.Cluster.command(cluster, ["SET", "mykey", "myvalue"])
Redis.Cluster.command(cluster, ["GET", "mykey"])
# Cross-slot pipelines are split, fanned out, and reassembled
Redis.Cluster.pipeline(cluster, [
["SET", "key1", "a"],
["SET", "key2", "b"],
["GET", "key1"],
])
#=> {:ok, ["OK", "OK", "a"]}
# Transactions require same-slot keys (use hash tags)
Redis.Cluster.transaction(cluster, [
["SET", "{user:1}.name", "Alice"],
["SET", "{user:1}.email", "alice@example.com"]
])
#=> {:ok, ["OK", "OK"]}

Sentinel

{:ok, conn} = Redis.Sentinel.start_link(
sentinels: [{"sentinel1", 26379}, {"sentinel2", 26379}],
group: "mymaster",
read_preference: :prefer_replica,
password: "secret"
)
# Writes use the primary; eligible reads prefer replicas
Redis.Sentinel.command(conn, ["SET", "key", "value"])
Redis.Sentinel.command(conn, ["GET", "key"])

Sentinel refreshes both primary and replica topology and reconnects on failover. The existing role: :primary | :replica option remains available for a fixed-role connection.

Primary/Replica Routing

For a manually managed replica set, configure the primary and optional replicas directly:

{:ok, redis} = Redis.ReplicaSet.start_link(
primary: {"redis-primary", 6379},
replicas: [{"redis-replica-1", 6379}, {"redis-replica-2", 6379}],
read_preference: :prefer_replica
)
Redis.ReplicaSet.command(redis, ["SET", "key", "value"]) # primary
Redis.ReplicaSet.command(redis, ["GET", "key"]) # replica, then primary fallback

Omit :replicas to discover online replicas from INFO REPLICATION. Read-only commands are derived from Redis COMMAND metadata; unknown commands, writes, mixed pipelines, and transactions stay on the primary.

Pub/Sub

{:ok, ps} = Redis.PubSub.start_link(port: 6379)
Redis.PubSub.subscribe(ps, "events", self())
receive do
{:redis_pubsub, :message, "events", payload} ->
IO.puts("Got: #{payload}")
end

Command Monitoring

Redis.Monitor uses a dedicated connection to stream structured command events. Subscribers are automatically removed when their process exits.

{:ok, monitor} = Redis.Monitor.start_link(port: 6379)
:ok = Redis.Monitor.subscribe(monitor, commands: ["SET", "DEL"])
receive do
{:redis_monitor, %Redis.Monitor.Entry{} = entry} ->
IO.inspect(entry)
end

Redis MONITOR exposes live traffic and carries a performance cost. Protect access to it and use it deliberately in production.

Phoenix.PubSub Adapter

Drop-in Redis adapter for Phoenix.PubSub. Enables cross-node broadcasting for Phoenix, LiveView, and any PubSub-based feature.

children = [
{Phoenix.PubSub,
name: MyApp.PubSub,
adapter: Redis.PhoenixPubSub,
redis_opts: [host: "localhost", port: 6379]}
]

Requires phoenix_pubsub (optional dependency).

Streams Consumer

High-level consumer group abstraction over Redis Streams. Define a handler, start the consumer, and messages are delivered with automatic acknowledgement and recovery of pending messages from crashed consumers.

defmodule MyApp.OrderHandler do
@behaviour Redis.Consumer.Handler
@impl true
def handle_messages(messages, _metadata) do
for [stream, entries] <- messages, [id, fields] <- entries do
IO.puts("#{stream} #{id}: #{inspect(fields)}")
end
:ok
end
end
children = [
{Redis.Connection, port: 6379, name: :redis},
{Redis.Consumer,
conn: :redis,
stream: "orders",
group: "processors",
consumer: "proc-1",
handler: MyApp.OrderHandler}
]

Produce messages from anywhere:

Redis.command(conn, ["XADD", "orders", "*", "item", "widget", "qty", "5"])

Scale by adding more consumers with different :consumer names -- Redis distributes messages across the group automatically.

JSON Documents

High-level API for RedisJSON. Maps in, maps out, with atom or list path syntax instead of JSONPath strings.

# Store and retrieve documents
Redis.JSON.set(conn, "user:1", %{name: "Alice", age: 30, tags: ["admin"]})
{:ok, %{"name" => "Alice", "age" => 30}} = Redis.JSON.get(conn, "user:1", fields: [:name, :age])
# Nested updates
Redis.JSON.put(conn, "user:1", [:address, :city], "NYC")
# Merge (like PATCH)
Redis.JSON.merge(conn, "user:1", %{status: "online", last_seen: "2026-04-03"})
# Atomic operations
{:ok, 31} = Redis.JSON.incr(conn, "user:1", :age, 1)
# Array operations
{:ok, 4} = Redis.JSON.append(conn, "user:1", :tags, "moderator")
{:ok, "moderator"} = Redis.JSON.pop(conn, "user:1", :tags)

For raw RedisJSON commands, see Redis.Commands.JSON.

High-level search API over RediSearch. Define indexes with keywords, push documents as maps, search with Elixir filter expressions instead of raw query strings.

# Create an index
Redis.Search.create_index(conn, "movies",
prefix: "movie:",
fields: [
title: :text,
year: {:numeric, sortable: true},
genres: :tag
]
)
# Add documents as maps
Redis.Search.add(conn, "movies", "movie:1", %{
title: "The Dark Knight",
year: 2008,
genres: "action,thriller"
})
# Search with Elixir filters
{:ok, results} = Redis.Search.find(conn, "movies", "dark knight",
where: [year: {:gt, 2000}, genres: {:tag, "action"}],
sort: {:year, :desc},
limit: 10
)
#=> %Redis.Search.Result{total: 1, results: [%{id: "movie:1", "title" => "The Dark Knight", ...}]}
# Aggregation
{:ok, results} = Redis.Search.aggregate(conn, "movies",
group_by: :genres,
reduce: [count: "total"],
sort: {:total, :desc}
)

Filters compile to RediSearch query syntax automatically:

ElixirRediSearch
name: "Alice"@name:Alice
age: {:gt, 18}@age:[(18 +inf]
age: {:between, 18, 65}@age:[18 65]
city: {:tag, "NYC"}@city:{NYC}
city: {:any, ["NYC", "LA"]}@city:{NYC|LA}

Numeric strings are auto-coerced to integers/floats by default. For raw RediSearch access, see Redis.Commands.Search.

Session Store

Drop-in Plug session store backed by Redis with configurable TTL.

plug Plug.Session,
store: Redis.PlugSession,
key: "_my_app_session",
table: :redis,
signing_salt: "your_salt",
ttl: 86_400

Requires plug (optional dependency).

Client-Side Caching

{:ok, cache} = Redis.Cache.start_link(port: 6379)
Redis.Cache.command(cache, ["SET", "key", "value"])
# Cache miss -- fetches from Redis
{:ok, "value"} = Redis.Cache.get(cache, "key")
# Cache hit -- served from ETS, 197x faster than network
{:ok, "value"} = Redis.Cache.get(cache, "key")
# When another client modifies "key", Redis pushes invalidation
# and the next call fetches the new value automatically

Resilience

{:ok, conn} = Redis.Resilience.start_link(
port: 6379,
retry: [max_attempts: 3, backoff: :exponential],
circuit_breaker: [failure_threshold: 5, reset_timeout: 5_000],
coalesce: true,
bulkhead: [max_concurrent: 50]
)
# Same API, with all resilience patterns composed
Redis.Resilience.command(conn, ["GET", "key"])

Features

License

MIT