Redis 📂 Core data structures · 2 of 2 22 min read

Redis Streams and Geospatial — Event Logs and Nearby Search

A practical Redis tutorial on streams and geospatial search. Learn append-only event logs with XADD and XREAD, sharing work with consumer groups, and finding nearby places with GEOADD and GEOSEARCH. See how geo is built on sorted sets. Includes animated diagrams, code, and a when-to-use guide.

Section 01

Two Special-Purpose Structures

A Ship's Logbook and a Map with a Compass
A ship keeps a logbook. Every event is written on a new line, with a time, and the old lines are never rubbed out. Anyone can open the book later and read from where they left off. That is a Redis stream.

The ship also has a map. Given your position, you can ask "what is within 2 km of me?" and get a list, nearest first. That is Redis geospatial search.

This tutorial covers both: streams for event logs, and geo for nearby search.
📜
Stream
append-only log
Events are added to the end, each with a unique ID. Nothing is edited. Readers move forward through the log and can replay old events.
👥
Consumer group
shared work
Many workers read one stream together. Each event goes to just one worker, so the work is shared and nothing is processed twice.
📍
Geospatial
nearby search
Store places by longitude and latitude, then find everything within a radius of a point, sorted by distance.

Section 02

Streams — An Append-Only Log

A stream is a log of events. XADD writes a new event at the end and returns its ID. The ID looks like 1758300000000-0: the time in milliseconds, a dash, and a sequence number. Because IDs always go up, the log stays in order forever.

Each XADD appends one entry at the tail. Entries are never changed. Readers move forward from an ID.

📜 A Stream Only Grows at the End
key: events (a stream) oldest newest 1758-0login 57 1759-0view 88 1760-0buy 57 1761-0login 12 XADD events * (append) Append-only: entries are only added, never edited. XREAD moves forward from an ID, so readers never miss an event.
# add events; * means "auto-generate the next ID"
XADD events * type "login" user 57
XADD events * type "purchase" user 57 item "book"
# -> each returns an ID like "1758300000000-0"

# how many events are in the log?
XLEN events

# read every event from the very start
XRANGE events - +

# read the newest events; $ means "only entries added after now"
XREAD BLOCK 5000 STREAMS events $
# waits up to 5 seconds for the next event, then returns it
OUTPUT of XRANGE events - +
1) "1758300000000-0" -> type "login" user "57" 2) "1758300000000-1" -> type "purchase" user "57" item "book"
🧠
The Entry ID Is a Timestamp

An ID like 1758300000000-0 is the millisecond time plus a counter for events in the same millisecond. IDs only ever increase, so the log is always in time order and a reader can say "give me everything after this ID".

Stream vs Pub/Sub vs List

📢 Pub/Sub
Fire and forget
Offline listeners miss messages
No history, no replay
Good for live-only signals
📜 Stream
Events are kept in the log
Late readers can catch up
Replay old events any time
Consumer groups share the work
⚠️
Cap the Stream So It Does Not Grow Forever

A stream keeps every event, so trim it. Add MAXLEN ~ 100000 to XADD to keep roughly the newest 100,000 entries and drop older ones. The ~ makes trimming faster and is almost always what you want.


Section 03

Consumer Groups — Sharing the Work

One reader is fine for a small log. But to process many events fast, use a consumer group. Several workers join the group and read the same stream. Redis hands each event to only one worker, so the work is split and nothing is done twice.

The group tracks who got what. Each new entry goes to one free worker. After a worker finishes, it sends XACK so the entry is marked done.

👥 One Stream, Many Workers
orders (stream) 1760-0 order 91 1761-0 order 92 1762-0 order 93 group: workers each entry → one worker worker-1 gets 1760-0 worker-2 gets 1761-0 worker-3 gets 1762-0
# create a group that starts at the end of the stream
XGROUP CREATE orders workers $ MKSTREAM

# a worker reads new entries meant for it ( > means "not yet delivered" )
XREADGROUP GROUP workers worker-1 COUNT 1 STREAMS orders >

# after finishing, acknowledge so it is not re-delivered
XACK orders workers 1760-0

# see entries delivered but not yet acked (stuck work)
XPENDING orders workers
✅
Nothing Is Lost, Even If a Worker Crashes

An entry stays "pending" until a worker sends XACK. If a worker dies before acking, the entry is still in the pending list, so another worker can pick it up with XCLAIM. This makes streams safe for real job processing.


Section 04

Geospatial — Nearby Search

Redis can store places by their location and answer "what is near me?". You add each place with its longitude and latitude, then search within a radius. Results come back sorted by distance.

📍
Longitude First, Then Latitude

GEOADD takes longitude before latitude. This trips up almost everyone. For Bengaluru (lat 12.97, long 77.59) you write GEOADD places 77.59 12.97 "...". Longitude, then latitude.

Search from a point with a radius. Places inside the circle are returned, nearest first. Places outside are ignored.

🗺️ Find Everything Within a Radius
Cafe Deli Pizza Chai Bakery Sushi Grill you GEOSEARCH places FROMLONLAT 77.60 12.97 BYRADIUS 2 km ASC → Deli, Cafe, Pizza, Chai
# add places: longitude, latitude, name  (long FIRST!)
GEOADD places 77.595 12.972 "cafe-mtr"
GEOADD places 77.610 12.980 "pizza-hub"
GEOADD places 77.601 12.965 "deli-corner"

# find places within 2 km of a point, nearest first, with distance
GEOSEARCH places FROMLONLAT 77.600 12.970 BYRADIUS 2 km ASC WITHDIST

# or search around an existing place
GEOSEARCH places FROMMEMBER "cafe-mtr" BYRADIUS 1 km ASC

# distance between two places, and a place's coordinates
GEODIST places "cafe-mtr" "pizza-hub" km   # -> "1.8"
GEOPOS places "cafe-mtr"
OUTPUT of GEOSEARCH ... WITHDIST
1) "deli-corner" 0.15 km 2) "cafe-mtr" 0.56 km 3) "pizza-hub" 1.80 km

Section 05

How Geo Works — It Is a Sorted Set

Here is the neat part. Redis geo is not a new structure. It turns each longitude and latitude into a single number called a geohash, and stores that number as the score in a sorted set. Places that are near each other get scores that are near each other, so a radius search is a fast score range.

🧭 From Coordinates to a Sorted-Set Score
1. Coordinates long 77.595 lat 12.972 GEOADD 2. Geohash number 3663485224812... (one 52-bit score) 3. Sorted set entry member: "cafe-mtr" score: the geohash
💡
Why This Matters

Because geo is a sorted set, your places also work with sorted-set commands, and a GEOSEARCH is really a clever score range. Nearby points have nearby scores, so Redis can find them without checking every place.


Section 06

Command Reference

CommandWhat it does
XADD key * f v ...Append an event, auto ID
XLEN keyCount events in the stream
XRANGE key - +Read a range of events
XREAD BLOCK ms STREAMS key $Wait for and read new events
XGROUP CREATE / XREADGROUP / XACKShare work across a group of workers
GEOADD key lon lat nameAdd a place (longitude first)
GEOSEARCH ... BYRADIUS r kmFind places within a radius
GEODIST / GEOPOSDistance between places / a place's coordinates
📜
Use a Stream when
You need a durable event log that late readers can catch up on: order events, activity logs, sensor readings, audit trails.
XADD, XREAD
👥
Add a Consumer Group when
Many workers must share the load and never process the same event twice. Background jobs, pipelines, safe queues.
XREADGROUP, XACK
📍
Use Geospatial when
You answer "what is near me?": nearby restaurants, drivers, stores, or friends, sorted by distance.
GEOADD, GEOSEARCH

Section 07

Golden Rules

🧩 Streams & Geospatial — Non-Negotiable Rules
1
Streams are append-only. Events are added at the end with rising IDs and never edited. Readers move forward from an ID, so nothing is missed.
2
Cap streams with MAXLEN. A stream keeps every event. Use MAXLEN ~ N on XADD so it does not fill your memory.
3
Use consumer groups to scale. Many workers, one stream, each event handled once. Always XACK when done, and reclaim stuck entries with XCLAIM.
4
Streams beat Pub/Sub for real work. Pub/Sub forgets messages the moment they are sent. A stream keeps them, so late or crashed readers can catch up.
5
Longitude before latitude. Every geo command takes longitude first. Getting this backwards puts your places in the wrong part of the world.
6
Geo is a sorted set. Coordinates become a geohash score. Nearby places have nearby scores, which is what makes radius search fast.
You have completed Core data structures. View all sections →