Skip to main content

How to Build a URL Shortener in Redis

URL shorteners turn long URLs into compact codes that redirect to the original destination. Behind every short link is a simple lookup: given a code like abc123, return the full URL. Redis excels at this because the core operation is a key-value lookup, and Redis handles millions of these per second with sub-millisecond latency.

This guide covers two approaches: basic shortening for simple redirect services, and shortening with click tracking for services that need analytics. Both use Redis strings as the foundation, with the second approach adding counters and sorted sets for visit data.

Which Redis data types we will use

String is used in three ways in this implementation:

  1. As the URL mapping where the key is the short code and the value is the destination URL. One GET command returns the redirect target. This is Redis at its simplest: pure key-value storage with O(1) lookups.
  2. As an ID counter to generate unique short codes. The INCR command atomically increments a counter and returns the new value. Convert that number to base62 (a-z, A-Z, 0-9) and you have a unique, compact short code with no collision checks needed.
  3. As a click counter for simple click totals. One INCR per click gives you a running total without the overhead of storing individual click records.

Hash stores URL metadata when you need more than just the destination. Creation timestamp, creator ID, expiration date, or custom slugs all fit naturally as hash fields. You can update individual fields without rewriting the entire record.

Sorted Set tracks click timestamps for analytics. Each click becomes a member with its timestamp as the score. This lets you query "clicks in the last hour" or "clicks between Monday and Friday" efficiently. The sorted set keeps click history ordered by time automatically.

The simplest URL shortener needs two operations: create a short code that maps to a URL, and look up a URL by its short code. A Redis string handles both. The key is the short code, the value is the destination URL. Creating a link is SET, looking it up is GET.

You need to generate unique short codes. Common approaches include incrementing a counter and encoding it in base62, generating random strings, or hashing the URL. The examples below use a counter-based approach with INCR because it guarantees uniqueness without collision checks. The counter gives you a number, and you convert it to a short string using base62 (a-z, A-Z, 0-9).

The lookup is a single GET command. If the key exists, redirect to the URL. If not, return a 404. This is about as fast as any database operation can be.

# Generate a unique ID using a counter
INCR shorturl:counter
> 1000001

# Store the mapping (short code -> URL)
# The code "abc123" would come from base62-encoding the counter
SET shorturl:abc123 "https://example.com/very/long/path/to/page"
> OK

# Look up a short code
GET shorturl:abc123
> "https://example.com/very/long/path/to/page"

# Check if a code exists before redirecting
EXISTS shorturl:abc123
> 1 (exists)

EXISTS shorturl:invalid
> 0 (does not exist)

Adding analytics: counting clicks and tracking visits

Most URL shorteners need analytics: how many times was a link clicked, when, and from where. Redis can track this alongside the redirect lookup with minimal additional latency. The key insight is that you can increment counters and record visits without blocking the HTTP redirect.

For each click, you want to increment a total counter and record the timestamp. A simple counter uses INCR on a separate key. For time-series data, a sorted set works well: the score is the timestamp, and the member is a unique click ID or the timestamp itself. This lets you query clicks within a time range using ZRANGEBYSCORE.

To avoid slowing down redirects, use pipelining or fire-and-forget writes. The redirect can return immediately after the GET, while the analytics writes happen asynchronously. For high-traffic links, consider batching clicks in memory and flushing periodically.

# Store URL with metadata using a hash
HSET shorturl:abc123 url "https://example.com/page" created_at 1699900000
> 2 (fields set)

# On each click: increment counter
INCR shorturl:abc123:clicks
> 1

# Record click timestamp in sorted set (score = timestamp)
ZADD shorturl:abc123:clicklog 1699900060 "1699900060:click1"
> 1

# Get total clicks
GET shorturl:abc123:clicks
> "42"

# Get clicks in the last hour
ZRANGEBYSCORE shorturl:abc123:clicklog 1699896460 1699900060
> ["1699896500:click1", "1699898000:click2", ...]

# Get click count for a time range
ZCOUNT shorturl:abc123:clicklog 1699896460 1699900060
> 15

# Get URL and increment click in one round trip (pipeline)
GET shorturl:abc123
INCR shorturl:abc123:clicks

Choosing an approach

Use basic shortening when you only need redirects and do not care about analytics. It is the simplest possible implementation: one key per short code, one GET per redirect. Memory usage is minimal and performance is optimal.

Use click tracking when you need to know how your links perform. The additional writes add negligible latency when pipelined, and the sorted set gives you flexible time-range queries. Consider setting a TTL on the click log to prevent unbounded growth, or periodically aggregate old data into daily/weekly counters.

Both approaches scale well. Redis handles millions of keys without issue, and the operations are O(1) for basic lookups or O(log N) for sorted set operations. For extremely high traffic links, consider using Redis Cluster to distribute the load, or add a caching layer in front of Redis for the most popular short codes.