redis_replica_selector

Latency-aware Sentinel replica selection for read-only Redis traffic.

Why this exists

The cluster’s replicas are not interchangeable: as of 2026-07-11 the master (10.10.0.3) and one replica (10.10.0.10) sit ~42ms from the app host while the other replica (10.10.0.2) is ~300ms away. redis-py’s Sentinel.slave_for rotates replicas randomly per NEW connection and pins each pooled connection to whichever replica it landed on — so naive replica reads get stuck on the 300ms node ~half the time, making them slower than just reading the master.

This module provides:

  • ReplicaSelector — a per-process singleton owning a background probe task that periodically times PING against the Sentinel-discovered master and replicas (EWMA-smoothed), then elects the lowest-RTT healthy replica, excluding any replica slower than exclusion_factor × the master’s RTT. When the elected replica changes, idle pooled connections on registered pools are disconnected so they re-run node selection instead of staying pinned to the old choice.

  • LatencyAwareSentinel — a Sentinel subclass whose filter_slaves returns only the elected replica (or an empty list, which triggers redis-py’s built-in fall-through to the master address inside rotate_slaves). Master discovery (master_for) is unaffected.

Consistency contract for anything routed through these read clients: reads may be seconds stale (replication lag is normally <1s). Locks, idempotency claims/markers, stream consumer-group operations, version counters and any read-your-own-write flow MUST stay on the master client.

class redis_replica_selector.ReplicaSelector(service_name, connection_kwargs, *, probe_interval=30.0, exclusion_factor=3.0, probe_timeout=2.0)

Bases: object

Elects the nearest healthy replica via periodic RTT probes.

One instance per process is shared by every latency-aware client (see get_shared_selector()). The probe task is started lazily from the first filter_slaves call that runs inside a live event loop.

Parameters:
  • service_name (str)

  • connection_kwargs (dict[str, Any])

  • probe_interval (float)

  • exclusion_factor (float)

  • probe_timeout (float)

attach_sentinel(sentinel)

Give the selector a Sentinel to discover topology through.

Return type:

None

Parameters:

sentinel (redis.asyncio.sentinel.Sentinel)

register_pool(pool)

Register a replica connection pool for eviction on re-election.

Return type:

None

Parameters:

pool (Any)

ensure_probe_task()

Start the probe loop if an event loop is running and it isn’t yet.

Return type:

None

choose(alive)

Pick the elected replica from Sentinel’s alive list, or None.

None means “no suitable replica” — callers translate that to an empty filter result, and redis-py falls through to the master. Before the first probe completes there is no RTT data, so this returns None (reads go to master) rather than gambling on a potentially 7x-slower node.

Return type:

Optional[Tuple[str, int]]

Parameters:

alive (Sequence[Tuple[Any, Any]])

class redis_replica_selector.LatencyAwareSentinel(*args, selector, **kwargs)

Bases: Sentinel

Sentinel whose replica discovery returns only the elected replica.

filter_slaves is the single sync choke point redis-py funnels every replica-pool connection decision through; returning one element makes selection deterministic, and returning [] reuses redis-py’s built-in master fallback inside rotate_slaves. master_for behavior is completely unchanged.

Parameters:
filter_slaves(slaves)
Return type:

Sequence[Tuple[Any, Any]]

Parameters:

slaves (Iterable[Mapping])

redis_replica_selector.get_shared_selector(service_name, connection_kwargs)

Return the process-wide ReplicaSelector, creating it once.

All latency-aware clients in a process share one selector so the RTT probe runs once per probe interval regardless of how many read clients exist.

Return type:

ReplicaSelector

Parameters: