redis-client-kit for AI agents¶
One page holding everything a coding assistant needs to configure and drive redis-client-kit correctly, plus a map of where the rest of the documentation keeps the details it leaves out. Give an agent this page rather than the whole site.
| Package | redis-client-kit on PyPI, import root redis_client_kit |
| Requires | Python 3.10+, redis>=4.5.0,<9.0.0 (resolved at 8.1 today) |
| Install | pip install redis-client-kit · extras: settings, metrics, providers, all |
| Async | redis_client_kit.aio, re-exported from the root — returns redis.asyncio.Redis or RedisCluster |
| Sync | redis_client_kit.sync — same shape, different names, no await |
| Source | https://github.com/bedrock-python/redis-client-kit |
How to read this page¶
Every page of this site is also served as raw Markdown at its own URL with .md in place
of the trailing slash — this page is /agents.md, the configuration guide is
/guide/configuration.md — so anything the map below points at can be fetched as plain
text rather than scraped out of HTML. The Copy page control at the top of a page does
the same thing for a human with a chat window open. The one exception is the API
reference: its Markdown is a single instruction to a docstring renderer rather than the
API, so it carries neither the control nor a .md twin — read it as HTML, or read the
docstrings in the source.
Top to bottom before writing code. Rules that hold or break the code is the section correctness lives in — those are the things the library will not save you from. Every name used below is in the public API; if you need something not listed here, fetch the page the documentation map points at rather than guessing a method that sounds plausible.
Scope¶
It does build a configured redis-py client — single node or cluster, async or sync —
out of one settings object, wrap it in metrics when you ask for metrics, check its health,
close it without raising, and hand it to a Dishka container. The settings object can be
anything that satisfies a protocol; a Pydantic implementation with validated defaults comes
with the settings extra.
It does not wrap Redis commands. What comes back from the factory is a redis-py
client and everything after that call is redis-py's API — there is no kit-specific get,
set, pipeline, pubsub, lock, serializer, key builder or cache. It applies no key prefix,
it does not connect for you, it runs no background task, and it retries nothing itself:
retries are redis-py's Retry, configured from your settings.
Mental model¶
Four nouns and one direction of travel.
- Settings — an object satisfying
RedisSettingsProtocol: six attribute groups (connection,cluster,pool,retry,ssl,response) plushealth_check_interval. Your own dataclass, orBaseRedisSettingsfrom thesettingsextra. - The factory —
create_async_redis_client(settings, metrics=None)and its sync twin. It flattens the settings intoredis-pykeyword arguments, picks single or cluster mode fromsettings.cluster.enabled, and returns the client. Pass no metrics and you get a plainRedis/RedisCluster; pass metrics and you getInstrumentedRedis/InstrumentedRedisCluster, subclasses that time everyexecute_command. - Metrics — anything satisfying
RedisMetricsProtocol(record_command,record_error,record_pool_stats).RedisMetricsfrom themetricsextra is a Prometheus implementation of it. - Lifecycle —
check_*_redis_health(client)pings and returns a bool, and withwrite_key=writes that key as well;close_*_redis_client(client)closes and swallows. Neither raises.
Nothing here holds state of its own. The client is the state, and it is redis-py's.
Wiring¶
from redis_client_kit import (
check_async_redis_health,
close_async_redis_client,
create_async_redis_client,
)
from redis_client_kit.settings import BaseRedisSettings, RedisConnectionSettings
settings = BaseRedisSettings(
key_prefix="myapp", # required, and yours to apply
connection=RedisConnectionSettings(host="localhost", port=6379),
)
client = create_async_redis_client(settings) # no connection is opened here
try:
if not await check_async_redis_health(client):
raise RuntimeError("Redis is not answering")
await client.set("myapp:key", "value")
finally:
await close_async_redis_client(client)
The sync twin is the same text with create_redis_client, check_redis_health,
close_redis_client and no await.
Without the settings extra, hand the factory any object with the same attributes:
from dataclasses import dataclass, field
@dataclass
class Connection:
host: str = "localhost"
port: int = 6379
db: int = 0
client_name: str | None = None
protocol: int = 2
def get_password(self) -> str | None:
return None
@dataclass
class Settings: # groups omitted here have defaults of their own
connection: Connection = field(default_factory=Connection)
... # cluster, pool, retry, ssl, response
health_check_interval: int | None = 30
client = create_async_redis_client(Settings())
Every attribute the protocols name has to be there: the factory reads all of them and
raises AttributeError on the first one missing.
Settings¶
BaseRedisSettings (extra settings) is a pydantic-settings BaseSettings with
extra="forbid". It is grouped, not flat — the six group models go in as objects, and
key_prefix is required.
| Field | Type | Default |
|---|---|---|
connection |
RedisConnectionSettings |
all defaults |
cluster |
RedisClusterSettings |
all defaults |
pool |
RedisPoolSettings |
all defaults |
retry |
RedisRetrySettings |
all defaults |
ssl |
RedisSSLSettings |
all defaults |
response |
RedisResponseSettings |
all defaults |
key_prefix |
str |
required — never read by this library |
health_check_interval |
int | None, ge=0 |
30 |
metrics_enabled |
bool |
False — never read by this library |
| Group | Field | Default | Notes |
|---|---|---|---|
RedisConnectionSettings |
host |
"localhost" |
|
port |
6379 |
1 ≤ port ≤ 65535 |
|
password |
None |
SecretStr; read with get_password() |
|
db |
0 |
0 ≤ db ≤ 15, must be 0 in cluster mode |
|
client_name |
None |
CLIENT SETNAME value |
|
protocol |
2 |
RESP version, 2 or 3 |
|
RedisClusterSettings |
enabled |
False |
this alone selects cluster mode |
nodes |
None |
["host:6379", …]; falls back to connection.host:port |
|
require_full_coverage |
True |
||
read_from_replicas |
False |
||
RedisPoolSettings |
max_connections |
10 |
ge=1 |
socket_timeout |
5.0 |
seconds | |
socket_connect_timeout |
5.0 |
seconds | |
socket_keepalive |
True |
||
socket_keepalive_options |
None |
dict[int, int | bytes] |
|
RedisRetrySettings |
enabled |
False |
off hands redis-py Retry(NoBackoff(), 0), not nothing — rule 5 |
max_attempts |
0 |
0 means no retry even when enabled |
|
backoff_base |
1.0 |
seconds | |
backoff_cap |
10.0 |
seconds | |
RedisSSLSettings |
enabled |
False |
|
cert_reqs |
None |
"none", "optional", "required" |
|
ca_certs / certfile / keyfile |
None |
paths, PEM-validated when SSL is on | |
RedisResponseSettings |
decode_responses |
False |
commands return bytes by default |
encoding |
"utf-8" |
One model validator runs after construction and rejects three combinations:
cluster.enabledwith neithercluster.nodesnorconnection.host—ValueError.cluster.enabledwithconnection.db != 0—ValueError.ssl.enabledwithssl.cert_reqs is None—ValueError.
From the environment¶
BaseRedisSettings sets no env_prefix and no env_nested_delimiter, so out of the box
the names are the bare field names and a group is one JSON document:
Subclass it if you want the usual shape:
from pydantic_settings import SettingsConfigDict
class Settings(BaseRedisSettings):
model_config = SettingsConfigDict(env_prefix="APP_", env_nested_delimiter="__")
# APP_KEY_PREFIX=myapp APP_CONNECTION__HOST=redis.internal APP_CONNECTION__PORT=6380
The API¶
Everything in this table is importable from redis_client_kit itself.
| Name | Signature | Returns |
|---|---|---|
create_async_redis_client |
(settings, metrics=None) |
Redis | RedisCluster, instrumented when metrics is given |
create_redis_client |
(settings, metrics=None) |
the sync equivalent |
check_async_redis_health |
await (client, write_key=None) |
bool — never raises; write_key adds SET <write_key> 1 EX 60 after the ping |
check_redis_health |
(client, write_key=None) |
the sync equivalent |
close_async_redis_client |
await (client) |
None — shielded, 10 s timeout, never raises |
close_redis_client |
(client) |
None — never raises |
build_base_redis_kwargs |
(settings, asyncio=False) |
dict[str, object] of redis-py keyword arguments; asyncio=True for a redis.asyncio client |
build_redis_retry |
(settings, asyncio=False) |
redis.retry.Retry, or redis.asyncio.retry.Retry with asyncio=True — Retry(NoBackoff(), 0) when retries are off |
parse_redis_url_node |
(node) |
tuple[str, int] — ValueError on a node with no port |
AsyncRedisClient |
type alias | redis.asyncio.Redis | redis.asyncio.cluster.RedisCluster |
SyncRedisClient |
type alias | redis.Redis | redis.cluster.RedisCluster |
RedisSettingsProtocol |
protocol | what the factory reads |
RedisMetricsProtocol |
protocol | what instrumentation calls |
__version__ |
str |
The rest lives one import deeper.
| Module | Extra | Names |
|---|---|---|
redis_client_kit.aio |
— | AsyncRedisClient, InstrumentedRedis, InstrumentedRedisCluster, create_async_redis_client, check_async_redis_health, close_async_redis_client |
redis_client_kit.sync |
— | SyncRedisClient, InstrumentedRedis, InstrumentedRedisCluster, create_redis_client, check_redis_health, close_redis_client |
redis_client_kit.config |
— | RedisSettingsProtocol and its parts: RedisConnectionProtocol, RedisClusterProtocol, RedisPoolProtocol, RedisRetryProtocol, RedisSSLProtocol, RedisResponseProtocol |
redis_client_kit.protocols |
— | RedisMetricsProtocol |
redis_client_kit.utils |
— | the three exported helpers, plus mask_redis_kwargs(kwargs) for logging and WRITE_PROBE_TTL_S, the write probe's expiry in seconds |
redis_client_kit.settings |
settings |
BaseRedisSettings, RedisConnectionSettings, RedisClusterSettings, RedisPoolSettings, RedisRetrySettings, RedisSSLSettings, RedisResponseSettings |
redis_client_kit.metrics |
metrics |
RedisMetrics, REDIS_COMMAND_DURATION_BUCKETS |
redis_client_kit.providers |
providers |
AsyncRedisProvider(check_health_on_startup=True, provide_default_metrics=True) |
Each optional module raises ImportError at import time when its extra is missing, naming
the extra. The root package imports none of them.
Metrics¶
RedisMetrics(prefix=None) creates five Prometheus collectors on the default registry,
named redis_* or <prefix>_redis_*:
| Collector | Type | Labels |
|---|---|---|
redis_pool_size |
Gauge | — |
redis_pool_checked_out |
Gauge | — |
redis_commands_total |
Counter | command, status |
redis_command_duration_seconds |
Histogram | command |
redis_connection_errors_total |
Counter | error_type |
Buckets are REDIS_COMMAND_DURATION_BUCKETS — (0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05,
0.1, 0.5, 1.0, 5.0). They are not configurable through RedisMetrics; write your own
RedisMetricsProtocol implementation if you need different ones. command is the first
argument of execute_command, upper-cased — it is unbounded cardinality only if you send
unbounded command names.
Dishka¶
from dishka import Provider, Scope, make_async_container, provide
from redis_client_kit import AsyncRedisClient
from redis_client_kit.config import RedisSettingsProtocol
from redis_client_kit.metrics import RedisMetrics
from redis_client_kit.protocols import RedisMetricsProtocol
from redis_client_kit.providers import AsyncRedisProvider
from redis_client_kit.settings import BaseRedisSettings
class AppProvider(Provider):
scope = Scope.APP
@provide
def settings(self) -> RedisSettingsProtocol:
return BaseRedisSettings(key_prefix="myapp")
@provide
def metrics(self) -> RedisMetricsProtocol | None: # this exact annotation
return RedisMetrics(prefix="myapp")
container = make_async_container(AsyncRedisProvider(), AppProvider()) # this order
client = await container.get(AsyncRedisClient)
AsyncRedisProvider is Scope.APP and provides the client as an AsyncIterator, so the
container closes it on teardown. Two keyword-only constructor arguments decide what it
registers:
| Argument | Default | Effect |
|---|---|---|
check_health_on_startup |
True |
pings Redis before yielding the client, and raises when it does not answer; False registers the factory that yields immediately |
provide_default_metrics |
True |
provides RedisMetricsProtocol | None as None so a container without metrics resolves; False leaves that type to your own provider |
See rules 15 to 17.
Rules that hold or break the code¶
- The factory returns a
redis-pyclient, not a wrapper. Everything aftercreate_*_redis_clientisredis-py:await client.set(...),client.pipeline(),client.pubsub(). There is no kit API for commands, and no key prefixing —key_prefixis a value you carry, not behaviour you get. - Creating a client opens no connection, except for a sync cluster client:
redis.cluster.RedisCluster.__init__discovers slots eagerly and raisesRedisClusterExceptionright there when no node answers. Async cluster and both single clients connect on the first command. - Cluster mode is chosen by
settings.cluster.enabled, not by the function you call. With it on,dbis never sent to the server, andBaseRedisSettingsrefusesconnection.db != 0outright. - Every cluster node string needs an explicit port.
host:6379,[::1]:6379andredis://host:6379parse;redis://hostandhostraiseValueError. The scheme is parsed and then ignored —rediss://does not enable TLS,ssl.enableddoes. retry.enabled=Trueon its own retries nothing, and neither doesredis-py. The exponentialRetryis built only whenenabledandmax_attemptsare both truthy, andmax_attemptsdefaults to0. Otherwise the factory handsredis-pyRetry(NoBackoff(), 0)explicitly — never nothing, becauseredis-pygiven noRetryretries on its own since 6.0 (three times; ten since 8.0, with jittered backoff), which turns asocket_timeout=0.5failure into ten seconds or more. With retries off, the firstConnectionErrororTimeoutErroris the one you get. The flavour follows the client: the async factory handsredis.asyncio.retry.Retry, the sync factoryredis.retry.Retry, and both helpers build the sync class unless called withasyncio=True. A syncRetryon aredis.asyncioclient never retries — itscall_with_retrydoes not await, so the failure never reaches the loop.- Retries cover connection failures, not command failures.
redis-py'sRetrydefaults toConnectionError,TimeoutErrorandsocket.timeout; aResponseErrorfrom a bad command is raised on the first try. The delay ismin(backoff_cap, backoff_base * 2**failures)with no jitter, so the first retry waits exactlybackoff_base. key_prefixandmetrics_enabledare declared and never read.key_prefixis required byBaseRedisSettingsand used by nothing in this package;metrics_enabled=Truedoes not turn on instrumentation. Passingmetrics=to the factory does, and it is the only thing that does.BaseRedisSettingsis grouped and forbids extras.BaseRedisSettings(host="…")raisesValidationError: Extra inputs are not permitted. Passconnection=RedisConnectionSettings(host="…").health_check_intervalisredis-py's per-connection ping interval, not thecheck_*_redis_healthfunction.0andNoneboth disable it.- The health check never raises and never says "maybe", and by default it only
pings. It returns
Falseon any error and logs it. A clusterping()returns one entry per node, and the check isTrueonly when every node answered truthily. ButPINGis a liveness answer, not a readiness one: a read-only replica and a server atmaxmemoryundernoevictionboth replyPONGand refuse every write. Passwrite_key="myapp:health"and the check runsSET <write_key> 1 EX 60(WRITE_PROBE_TTL_S) after the ping, returningFalseonReadOnlyError,OutOfMemoryErroror anything else the write raises. The key is yours to name and yours to prefix —key_prefixis applied to nothing (rule 7) — and it is left to expire rather than deleted. It costs one more round trip under the samesocket_timeout, and on a cluster the write reaches only the node owning that key's slot, soTruethere means every node answered and one of them took a write. - Closing never raises either.
close_async_redis_clientshieldsaclose()and gives it 10 seconds (ACLOSE_TIMEOUT_S); a timeout or a broken close is a warning in the log, not an exception. OnlyCancelledErrorandKeyboardInterruptpropagate. - SSL certificate paths are read when the client is built, not when it connects.
With
ssl.enabled,build_base_redis_kwargsopens each configured PEM file and checks its header, footer and base64 body: a malformed file raisesValueError, a missing one raisesFileNotFoundError.ssl.cert_reqsis mandatory once SSL is on. - The protocols are not
@runtime_checkable.isinstance(settings, RedisSettingsProtocol)raisesTypeError. Subclass them for the type checker, or duck-type and let the factory'sAttributeErrorfind the gap. - Extras are enforced at submodule import.
import redis_client_kitnever needs pydantic, prometheus-client or dishka;from redis_client_kit.settings import …raisesImportErrornaming the extra when it is missing. Do not guard the root import. - Register
AsyncRedisProvider()before your own metrics provider, or turn its default off. It provides a defaultRedisMetricsProtocol | NoneofNone, and in Dishka the last provider to claim a type wins — put it second and your metrics are silently dropped, leaving an uninstrumented client.AsyncRedisProvider(provide_default_metrics=False)registers no default, so order stops mattering. Either way the annotation on your factory must be exactlyRedisMetricsProtocol | None;RedisMetricsProtocolis a different key. - The provider registers one client factory, chosen at construction.
AsyncRedisProvider()registersget_redis_with_health_check();AsyncRedisProvider(check_health_on_startup=False)registersget_redis()instead. They provide the same type, so registering both would leave only the second — the constructor picks one. - The provider's startup health check fails startup. It pings up to three times
with exponential backoff — 1 s, then 2 s — and raises
ConnectionErrorwhen Redis never answers, closing the client it built. ResolvingAsyncRedisClientis what triggers it, so that is where the error surfaces. UseAsyncRedisProvider(check_health_on_startup=False)when a missing Redis must not block startup. - A
RedisMetricsinstance owns global Prometheus names. Building a second one with the same prefix raises a duplicate-timeseriesValueErrorfrom the default registry. Build one per process and inject it. - Cluster clients record no pool statistics. Single-node clients, async and sync,
report
redis_pool_sizeandredis_pool_checked_outfrom the pool's own containers before every command;InstrumentedRedisClusterreports neither. Command counts, durations and error counts are recorded everywhere. - A uvloop closed-transport
RuntimeErrorreaches you asredis-py'sConnectionError.execute_commandtranslates it soredis-pycan retry or reconnect, and records it under the type you catch:redis_commands_total{status="error"}andredis_connection_errors_total{error_type="ConnectionError"}. Catchredis.exceptions.ConnectionError, notRuntimeError. Any otherRuntimeErroris re-raised unchanged and counted underRuntimeError.
Common mistakes¶
# WRONG — the flat constructor from an older version of this library
settings = BaseRedisSettings(host="localhost", port=6379, decode_responses=True)
# RIGHT — grouped, and key_prefix is required
settings = BaseRedisSettings(
key_prefix="myapp",
connection=RedisConnectionSettings(host="localhost", port=6379),
response=RedisResponseSettings(decode_responses=True),
)
# WRONG — retry that never retries, and a cluster node with no port
BaseRedisSettings(
key_prefix="myapp",
retry=RedisRetrySettings(enabled=True), # max_attempts is still 0
cluster=RedisClusterSettings(enabled=True, nodes=["redis://node1"]),
) # accepted here, ValueError when the factory parses the node
# RIGHT
BaseRedisSettings(
key_prefix="myapp",
retry=RedisRetrySettings(enabled=True, max_attempts=3, backoff_base=0.5, backoff_cap=5.0),
cluster=RedisClusterSettings(enabled=True, nodes=["node1:6379", "node2:6379"]),
)
# WRONG — a sync Retry on an async client never retries: its call_with_retry does not await
client = redis.asyncio.Redis(**build_base_redis_kwargs(settings), host="localhost", port=6379)
# RIGHT — asyncio=True builds redis.asyncio.retry.Retry, which is what the async factory does
client = redis.asyncio.Redis(**build_base_redis_kwargs(settings, asyncio=True), host="localhost", port=6379)
# WRONG — metrics_enabled does nothing, and the client is never instrumented
settings = BaseRedisSettings(key_prefix="myapp", metrics_enabled=True)
client = create_async_redis_client(settings)
# RIGHT
client = create_async_redis_client(settings, metrics=RedisMetrics(prefix="myapp"))
# WRONG — creating a client is not connecting to one
client = create_async_redis_client(settings)
log.info("Redis is up")
# RIGHT
client = create_async_redis_client(settings)
if not await check_async_redis_health(client):
raise RuntimeError("Redis is not answering")
# WRONG — readiness for a service that writes, from a check a read-only replica passes
if not await check_async_redis_health(client):
return Response("unready", status_code=503)
# RIGHT — ask for the write you are going to need
if not await check_async_redis_health(client, write_key="myapp:health"):
return Response("unready", status_code=503)
# WRONG — a per-request client, and a close that can take the request down with it
async def handler(settings):
client = create_async_redis_client(settings)
try:
return await client.get("key")
finally:
await client.aclose()
# RIGHT — one client per process, closed once, with the shielded helper
client = create_async_redis_client(settings) # at startup
...
await close_async_redis_client(client) # at shutdown
Errors¶
This library defines no exception classes of its own. Everything you catch is raised by
Python, by Pydantic or by redis-py.
| Raised by | What it is |
|---|---|
ValueError from parse_redis_url_node |
an empty node, a node with no port, or a port outside 1-65535 |
ValueError / FileNotFoundError from build_base_redis_kwargs |
a malformed or missing PEM file while ssl.enabled |
pydantic.ValidationError from BaseRedisSettings |
a field out of range, an unknown keyword, a missing key_prefix, or one of the validator's three checks |
AttributeError from the factory |
a settings object missing an attribute the protocols name |
redis.exceptions.* from the client |
everything at run time: ConnectionError, TimeoutError, ResponseError, RedisClusterException, ClusterDownError and the rest of redis-py's tree, all under RedisError |
builtin ConnectionError from AsyncRedisProvider |
Redis did not answer within the startup health check's three attempts — Python's ConnectionError, not redis.exceptions.ConnectionError, so it is not caught by except RedisError |
ImportError from an optional submodule |
the extra is not installed; the message names it |
check_*_redis_health and close_*_redis_client convert redis-py's errors into a
False and a log line respectively — they are the two places that swallow. A refused
write probe goes the same way: ReadOnlyError and OutOfMemoryError come back as False
with a warning in the log, not as an exception.
Documentation map¶
Fetch a page when the task is the one named beside it.
| Page | Read it when |
|---|---|
| Home | the shape of the library and what each extra adds |
| Quick start | the lifecycle helpers, the context-manager pattern, redis-py command examples |
| Configuration | implementing RedisSettingsProtocol without Pydantic |
| Advanced | writing a RedisMetricsProtocol, Dishka wiring, cluster and TLS deployment notes |
| API reference | an exact signature or docstring — HTML only, see above |
| Changelog | what changed between versions |