notify-keyspace-events configuration, the event type matrix (key expiry, deletion, Set/List/Hash writes), subscribing to expiry for cache warming, and the critical limitation: keyspace notifications are at-most-once.
P-8 — Keyspace Notifications and Event-Driven Architectures
Who this module is for: You want to trigger application logic when a Redis key expires, is deleted, or is modified — without polling. Keyspace notifications let you subscribe to Redis's internal events via Pub/Sub. This module covers the configuration, the event type matrix, practical patterns, and the critical limitation that most documentation buries at the bottom.
What Keyspace Notifications Are
Keyspace notifications allow Redis to publish messages to Pub/Sub channels when specific events occur — a key expires, a key is written, a key is deleted. Your application subscribes to these channels and reacts to events.
This enables:
- Cache warming: re-populate a key when it expires
- Audit logging: record every mutation to sensitive keys
- Event-driven workflows: trigger downstream logic when a key is set
- Session expiry hooks: notify your app when a session token expires
Enabling Keyspace Notifications
By default, keyspace notifications are disabled (they add CPU overhead to every write operation).
CONFIG SET notify-keyspace-events "KEA"
Or in redis.conf:
notify-keyspace-events "KEA"
The Event Flag String
The value is a combination of flag characters:
Event type (what happened):
g— generic commands (DEL, EXPIRE, RENAME, COPY)$— String commands (SET, GETSET, SETRANGE, APPEND, INCR, etc.)l— List commands (LPUSH, RPUSH, LPOP, RPOP, LMOVE, etc.)s— Set commands (SADD, SREM, SPOP, etc.)h— Hash commands (HSET, HDEL, etc.)z— Sorted Set commands (ZADD, ZINCRBY, ZREM, etc.)x— Expired events (key expired by TTL)e— Evicted events (key evicted by maxmemory-policy)t— Stream commandsd— Module key type eventsm— Key miss events (when a command targets a non-existent key — generates many events)A— Alias forg$lshztdxea(every event class except key-miss events)
Channel type (where to publish):
K— Keyspace events: channel is__keyspace@{db}__:{key}; message is the event nameE— Keyevent events: channel is__keyevent@{db}__:{event}; message is the key name
You must specify at least one channel type (K or E) and at least one event type.
Common configurations:
"KEA" → both channel types, all events — maximum coverage, maximum overhead
"Kx" → keyspace events for key expiry only
"Ex" → keyevent events for key expiry only — recommended for most use cases
"Kg$" → keyspace events for generic + String commands
"Ex$z" → keyevent events for expiry + String + Sorted Set commands
The Two Channel Types
Keyspace Channels (K)
Channel: __keyspace@{db}__:{key}
Message: the event name (e.g., "expired", "set", "del")
Subscribe to a specific key to receive all events affecting it:
SUBSCRIBE __keyspace@0__:session:user:1001
# Receives:
# "set" when the key is created/updated
# "expire" when EXPIRE is called
# "expired" when the key's TTL causes automatic deletion
# "del" when DEL is called
Keyevent Channels (E)
Channel: __keyevent@{db}__:{event}
Message: the key name
Subscribe to a specific event type to receive it for all keys:
SUBSCRIBE __keyevent@0__:expired
# Receives the key name whenever ANY key expires:
# "session:user:1001"
# "rate:user:2002:window"
# "cache:product:999"
For most use cases (cache expiry hooks, session expiry), __keyevent@0__:expired is the right channel.
Practical Pattern: Cache Re-Population on Expiry
When a cached value expires, automatically re-populate it:
import Redis from 'ioredis';
const subscriber = new Redis();
const publisher = new Redis(); // separate connection for other operations
// Enable keyspace notifications (expired events only)
await publisher.config('SET', 'notify-keyspace-events', 'Ex');
// Subscribe to all expired events
await subscriber.subscribe('__keyevent@0__:expired');
subscriber.on('message', async (channel: string, expiredKey: string) => {
// Only handle keys that match our cache pattern
if (!expiredKey.startsWith('cache:')) return;
console.log(`Key expired: ${expiredKey}`);
// Re-populate the cache
const value = await fetchFromDatabase(expiredKey);
if (value !== null) {
await publisher.set(expiredKey, JSON.stringify(value), 'EX', 300);
}
});
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion