Redis Client Applications
The native Redis client supports typed values, pipelines, transactions, streams, and Pub/Sub. Use it to combine Redis access with computation in a standalone Codon application; the Redis server needs no Codon module.
This guide covers the common command families and the differences between batched commands, transactions, and replayable event consumption.
Before you begin: install the all-SDK distribution and run a development Redis server on localhost. The examples write keys; use a database without application data you need to keep.
Read, Compute, and Write
An ordinary Redis server needs no Codon module for client access. Save the example
below as client.codon. It writes two readings, computes their total in Codon,
and stores a summary hash. Use a development database: the example writes to
reading:a, reading:b, and codon:summary.
from exaloop.databases.redis import Redis
import sys
port = int(sys.argv[1]) if len(sys.argv) > 1 else 6379
with Redis(port=port) as client:
client.mset({"reading:a": 1.5, "reading:b": 2.5})
readings = client.mget(["reading:a", "reading:b", "reading:missing"], float)
total = 0.0
for value in readings:
if value is not None:
total += value
client.hset("codon:summary", {"total": str(total), "source": "Codon"})
print(client.hgetall("codon:summary", str))
With a prebuilt SDK and a Redis server listening on port 6379:
exaloop --with redis run -release client.codon
Use an individual SDK without the CLI
codon run -release -plugin /path/to/sdk/redis client.codon
The resulting hash contains a total of 4.0. Requesting float in mget()
converts the stored values for the calculation; the missing key becomes None
and is skipped.
Connections and Security
Redis() connects to the local server on port 6379. For a remote deployment,
provide ACL credentials from private configuration and enable verified TLS:
Redis(host="redis.example.com", ssl=True, ssl_ca_certs="/etc/ssl/redis-ca.pem").
Each concurrent worker needs a dedicated client. A with block closes it
deterministically; connection loss raises ConnectionError without retrying
writes whose outcome may be uncertain. Server command failures raise
ResponseError instead.
The connection reference lists all options.
Types and Materialization
Reads return bytes unless you ask for a type. For example,
client.get("count", int) reads an integer and client.hgetall("record", str)
reads a hash as text. Missing values use Optional[T], so they are distinct from
an empty string or zero.
Choose the Redis data structure that matches your operation:
| Data | Common writes | Common reads and materialized results |
|---|---|---|
| Strings and binary blobs | set, mset, append, setrange, incr, incrbyfloat | get -> Optional[T], mget -> List[Optional[T]], getrange -> bytes, strlen |
| Hashes | hset(key, mapping), hdel, hincrby | hget, hmget, hgetall -> Dict[T,T], hexists, hlen |
| Lists | lpush, rpush, lset, ltrim | lrange -> List[T], lpop/rpop -> Optional[T], llen, blpop -> Optional[Tuple[T,T]] |
| Sets | sadd, srem, spop | smembers/sunion/sinter/sdiff -> Set[T], sismember, scard |
| Sorted sets | zadd(key, member_to_score), zincrby, zrem | zrange -> List[T], zrange_withscores -> List[Tuple[T,float]], zscore -> Optional[float], zcard |
| Bitmaps and bitfields | setbit, bitop, bitfield | getbit, bitcount, bitfield -> List[Optional[int]] |
| HyperLogLog | pfadd, pfmerge | pfcount -> int (approximate cardinality) |
| Geospatial indexes | geoadd(key, member_to_lon_lat) | geopos -> List[Optional[Tuple[float,float]]], geodist -> Optional[float], geosearch -> List[T] |
| Streams and consumer groups | xadd, xdel, xtrim, xgroup_create, xack, xclaim | xrange, xread, xreadgroup, xpending, xlen |
Common Redis options keep their familiar names. For example, set() accepts
ex for an expiry in seconds and nx to write only if the key does not exist.
You can also use client[key] for string values; direct indexing raises
KeyError for a missing key.
Large Collections and Bulk Work
Use scan_iter(match="prefix:*", count=100) to walk a keyspace without a single
large KEYS request. Like Redis SCAN, it is not a snapshot and may return
duplicates while keys are changing. Split large reads and writes into batches
to avoid holding an entire workload in memory.
A nontransactional pipeline sends queued commands together and then reads all replies, avoiding one network round trip per command:
from exaloop.databases.redis import Redis
with Redis() as client:
with client.pipeline(transaction=False) as batch:
batch.set("batch:count", "10").incr("batch:count").get("batch:count")
replies = batch.execute()
assert replies[1].int() == 11
assert replies[2].as_type(int) == 11
for reply in replies:
reply.close()
Call execute() to send the queued work, then read the replies in command order.
The example sets a counter to 10, increments it, and reads back 11. Leaving the
block without execute() discards the queue.
Transactions
Without transaction=False, a pipeline uses MULTI/EXEC, so Redis executes
the queued commands together without interleaving other clients' commands.
This does not provide SQL-style rollback: a failed command does not undo earlier
writes in the transaction.
For a read/modify/write operation, use WATCH to detect a concurrent update. The example retries the whole operation up to three times:
from exaloop.databases.redis import Redis, WatchError
with Redis() as client:
for attempt in range(3):
try:
with client.pipeline() as transaction:
transaction.watch("balance")
with transaction.read("GET", "balance") as reply:
balance = 0 if reply.is_null else reply.as_type(int)
transaction.set("balance", balance + 1)
results = transaction.execute()
for result in results:
result.close()
break
except WatchError:
if attempt == 2:
raise
Keep the client's connection dedicated to this operation until the transaction
finishes. If a watched key changes, WatchError lets the application decide
whether to retry.
Streams and Pub/Sub
Use streams when consumers need stored events and acknowledgements. An entry
has an ID and a collection of fields; xread() groups the returned entries by
stream:
from exaloop.databases.redis import Redis
with Redis() as client:
entry = client.xadd("measurements", {"sensor": "A", "value": "12.5"})
rows = client.xread({"measurements": "0"}, count=100, T=str)
assert rows[0][0] == "measurements"
assert client.xlen("measurements") > 0
For consumer groups, create the group with xgroup_create(), read deliveries
with xreadgroup(), and acknowledge completed work with xack(). If you use a
blocking read, give the socket a longer timeout than the requested wait.
Pub/Sub is a simpler choice for live notifications that do not need replay. It uses its own connection, so publishing does not interrupt the subscriber:
from exaloop.databases.redis import Redis
with Redis() as client:
with client.pubsub() as subscription:
subscription.subscribe("events")
assert subscription.get_message(1.0).__val__().kind == "subscribe"
client.publish("events", b"updated")
message = subscription.get_message(1.0)
assert message is not None
assert message.channel == b"events" and message.data == b"updated"
The first message confirms the subscription; the next contains the published
event. get_message() returns None on timeout. A disconnected Pub/Sub consumer
misses events, so choose streams when recovery matters.
Commands Beyond the Convenience API
Use execute_command("COMMAND", *args) for a command without a convenience
method, including commands provided by an installed Redis module. It returns
a reply that you can read through typed accessors; see the
reply reference.
The client is synchronous and single-node. Cluster routing, Sentinel discovery, and connection pooling are not supported.
Next Steps
- Package the client with its runtime dependencies.
- Create a server command to combine a short sequence of operations inside Redis.
- Review the connection and reply reference for configuration and lower-level commands.