Module P-8·11 min read

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.

Written by Jatin Jain Saraf · Architect of SupraScan

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 commands
  • d — Module key type events
  • m — Key miss events (when a command targets a non-existent key — generates many events)
  • A — Alias for g$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 name
  • E — 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 & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.