Skip to main content

How to Build Typing Indicators in Redis

The "someone is typing..." indicator is a small detail that makes chat feel alive. When you see those three dots or a friend's name appear, you know a message is coming. Building this feature seems simple at first, but the edge cases add up quickly. What happens when someone starts typing, gets distracted, and never sends? What if multiple people type at once? What if a user closes their browser mid-sentence?

Redis handles these problems elegantly because of its built-in key expiration. You can set a typing indicator that automatically disappears after a few seconds of inactivity, without needing cleanup jobs or garbage collection. This guide covers three approaches: simple expiring keys for basic one-on-one chat, sorted sets for group chats where several people might type at once, and Pub/Sub for real-time updates to notify clients instantly when typing status changes.

Which Redis data types we will use

String with expiration is the simplest way to track typing status. Set a key when a user starts typing and give it a short TTL, say 3 seconds. If the user keeps typing, refresh the TTL. If they stop, the key expires automatically. Other clients check for the key's existence to know if someone is typing.

Sorted Set tracks multiple typers in a single room efficiently. Each typing user becomes a member with their last keystroke timestamp as the score. To find who is currently typing, query for members with recent timestamps. This avoids creating many separate keys for busy group chats and makes cleanup straightforward.

Pub/Sub pushes typing notifications to connected clients in real time. Instead of clients polling to check if someone is typing, the server publishes typing events and clients receive them instantly. This reduces latency and Redis load, but clients that disconnect briefly might miss updates.

The simple approach: one key per typing user

For one-on-one conversations or simple chat rooms, an expiring string key works perfectly. When a user types, set a key like typing:room:123:user:456 with a 3-second expiration. Each keystroke refreshes the TTL. When the user stops typing for 3 seconds, the key vanishes on its own.

The receiving client checks for typing indicators by looking up the key. If it exists, show "User is typing...". If not, hide the indicator. This polling approach is simple but adds latency since you only discover typing status when you check.

Debouncing on the client side prevents flooding Redis with updates. Instead of writing to Redis on every keystroke, wait until the user pauses for 300 milliseconds, or throttle updates to once per second. This dramatically reduces write volume while keeping the indicator responsive.

# User starts typing, set indicator with 3 second expiration
SET typing:room:123:user:456 1 EX 3
> OK

# User keeps typing, refresh the expiration
SET typing:room:123:user:456 1 EX 3
> OK

# Another user checks if anyone is typing
GET typing:room:123:user:456
> "1" (user 456 is typing)

# After 3 seconds of no keystrokes, key expires automatically
GET typing:room:123:user:456
> (nil) (user stopped typing)

# User sends message, explicitly clear the indicator
DEL typing:room:123:user:456
> 1

Tracking multiple typers in group chats

When many users might type simultaneously, checking individual keys becomes inefficient. A sorted set stores all typing users for a room in a single key, with each user's last activity timestamp as their score. To find current typers, query for users with timestamps within the last few seconds.

This approach scales better for active group chats. One ZRANGEBYSCORE returns all typing users instead of checking many individual keys. You also get automatic ordering by who started typing most recently, which is useful for displaying "Alice, Bob, and 3 others are typing."

The tradeoff is that sorted sets do not expire individual members. You need to clean up stale entries by removing users with old timestamps. Do this when checking for typers, or run a periodic cleanup. The query itself filters out old entries, so stale data only wastes a small amount of memory until cleanup runs.

# User starts typing, add to sorted set with current timestamp
ZADD typing:room:123 1699900060 user:456
> 1

# Another user starts typing
ZADD typing:room:123 1699900062 user:789
> 1

# First user keeps typing, update their timestamp
ZADD typing:room:123 1699900065 user:456
> 0 (updated existing member)

# Get users typing in the last 3 seconds (current time is 1699900066)
ZRANGEBYSCORE typing:room:123 1699900063 +inf
> ["user:456", "user:789"]

# Clean up users who stopped typing (older than 3 seconds)
ZREMRANGEBYSCORE typing:room:123 -inf 1699900063
> 0 (removed stale entries)

# User sends message, remove from typing set
ZREM typing:room:123 user:456
> 1

Pushing updates with Pub/Sub

Polling works but adds latency. The receiving user only sees "typing" when their next poll happens. For real-time feel, use Redis Pub/Sub to push typing events to connected clients immediately. When a user starts or stops typing, publish an event to the room's channel. All subscribed clients receive it instantly.

Combine Pub/Sub with the storage approaches above. The sorted set or string keys remain the source of truth for who is typing. Pub/Sub just notifies clients that something changed. When a client reconnects after a brief disconnect, they can check the stored state to catch up on any events they missed.

The tradeoff is complexity. Your server needs to maintain WebSocket connections to clients and bridge them to Redis Pub/Sub subscriptions. Clients that disconnect and reconnect might miss typing events during the gap. For most chat applications, this brief inconsistency is acceptable since typing indicators are ephemeral anyway.

# When user starts typing, publish to the room channel
PUBLISH typing:room:123 '{"user":"456","typing":true}'
> 2 (number of subscribers who received the message)

# When user stops typing or sends message
PUBLISH typing:room:123 '{"user":"456","typing":false}'
> 2

# Clients subscribe to receive typing events
SUBSCRIBE typing:room:123
> Reading messages... (push "typing:room:123" '{"user":"456","typing":true}')

Choosing an approach

Use expiring string keys for simple one-on-one chats or small rooms where you know the participant list. The automatic expiration handles cleanup perfectly, and checking a few keys is cheap enough.

Use sorted sets for group chats where many users might type simultaneously. The single key per room scales better than many individual keys, and you get all typers in one query. Remember to clean up stale entries periodically.

Use Pub/Sub when you need real-time updates and already have WebSocket infrastructure. Combine it with one of the storage approaches so clients can recover state after reconnecting. The added complexity is worth it for chat applications where instant feedback matters.

All three approaches handle the hard part automatically: users who abandon their typing without sending. The key expires, the timestamp ages out, or the stop event fires. No zombie typing indicators haunting your chat rooms.