Design a URL Shortener
By the Syncfusion DevTutorials team · Last updated
Design QuickBite's link shortener end to end: requirements, traffic and storage estimates, code length, 301 vs 302, base62 codes from ID ranges, caching, and a design sandbox.
QuickBite sends many links every day.
Every order confirmation includes a tracking link. Restaurants share their menu links on social media. These URLs are often long and difficult to share.
Example:
https://quickbite.app/restaurants/spice-garden-koramangala/menu?ref=sms&campaign=diwali-2026
A URL shortener converts that long URL into a shorter link such as qbite.link/3xK9pQ.
When someone opens the short link, they are redirected to the original URL.
At first, this sounds simple.
Store a short code, store a long URL and look it up later.
That simplicity is what makes URL shorteners a classic system design problem. The idea is small enough that you can focus on the design process.
The main idea: a URL shortener is a read-heavy lookup system. Every code must be unique, and redirects must be fast and reliable.
What You Will Learn
In this case study, you will:
- Write the requirements for a URL shortener.
- Estimate traffic, storage and code length.
- Design the APIs and choose the correct redirect type.
- Compare different code generation approaches.
- Design the high-level architecture.
- Build and test the design under real traffic.
This lesson builds on Estimation, Load Balancing, Caching, Data Storage, Partitioning and Rate Limiting.
Step 1: Requirements
Before designing the system, define what it must do.
What It Must Do (Functional)
- Create short links from long URLs.
- Redirect users from short URLs to long URLs.
- Count link clicks.
- Support link expiration.
- Return link information when requested.
- Handle large numbers of redirects.
How Well It Must Do It (Non-Functional)
- Redirects should complete within 100 ms at p99.
- The service should remain highly available.
- Every short code must be unique.
- Codes should be difficult to guess.
- The system should scale as traffic grows.
- Redirects should remain reliable during failures.
Out of Scope
This lesson does not include:
- Custom aliases
- User accounts
- Editing existing links
The focus is URL shortening and redirection.
Step 2: Estimate
Before designing the architecture, estimate traffic.
Assume QuickBite creates:
- 1 million new links per day
- 100 clicks per link
- Peak traffic is 5 times average traffic
Divide by the 86,400 seconds in a day, then multiply by 5 for the peak:
| Traffic | Average | Peak |
|---|---|---|
| New links | 11.6 a second | 58 a second |
| Redirects | 1,157 a second | 5,787 a second |
What to notice:
- Writes are very small.
- Redirects are much larger.
- Reads are about 100 times more common than writes.
- This is a read-heavy system.
- Caching becomes very important.
Storage
Suppose:
- Each link requires about 500 bytes.
- Data is stored for 5 years.
- Three copies of the data are kept.
1 million links a day for 5 years is 1.825 billion links. At 500 bytes each, that is 0.91 TB for one copy, or 2.74 TB with 3 copies.
What to notice:
- Storage requirements are reasonable.
- A single database can hold the data.
- Traffic is a bigger challenge than storage.
How Long Should the Code Be?
Short codes use Base62 characters: 0-9, a-z and A-Z. This gives 62 possible characters.
Every new character multiplies the number of possible codes by 62.
The chart shows how much of each code length 1.825 billion links would use up.
5 characters give 916 million codes, which run out in about 2.5 years. 6 characters give 56.8 billion, and 7 give about 3.5 trillion.
What to notice:
- Five characters are not enough.
- Six characters are sufficient.
- Seven characters provide room for future growth.
- Longer codes are harder to guess.
QuickBite chooses 7-character Base62 codes.
Estimation helps make design decisions. Here it shows that redirects need optimization, storage is manageable and 7-character codes provide enough capacity.
Step 3: The API
Only two APIs are required.
| API | Purpose |
|---|---|
POST /links | Create a short link |
GET /{code} | Redirect to the original URL |
Create Link
| Part | Value |
|---|---|
| Request | POST /links |
| Input | Long URL, expiry date (optional) |
| Response | 201 Created, short code 3xK9pQ |
Use Link
| Part | Value |
|---|---|
| Request | GET /3xK9pQ |
| Response | 302 Found with Location: https://... |
| Code missing or expired | 404 Not Found |
Link creation should also be rate limited to prevent abuse.
301 or 302?
There are two common redirect responses.
301 Moved Permanently
- Browser may cache the redirect.
- Future clicks may skip the shortener.
302 Found
- Browser asks the shortener every time.
- Every click can be counted.
Because click counting is a requirement, QuickBite uses 302 Found.
What to notice:
- 301 reduces traffic.
- 302 preserves click tracking.
- Requirements matter more than saving requests.
Step 4: Generating Short Codes
The most important design decision is how to generate codes.
Option 1: Hash the URL
Hash the long URL and keep the first few characters.
Advantages:
- Simple
- Same URL gets the same code
Problem:
- Different URLs can produce the same short code.
- Collision checks are required.
As the number of URLs grows, collisions become likely.
The chart shows the chance that at least two links get the same 7-character code.
With 1 million links there is already a 13% chance of a clash. By 10 million links a clash is certain.
Option 2: Counter + Base62
Assign every new link a unique number.
The first links get 1, 2, 3 and so on. Then convert the number into Base62. Step through one conversion:
Here is the same idea in code:
A counter turns into a short code
const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
function toBase62(n) {
if (n === 0) return "0";
let code = "";
while (n > 0) {
code = ALPHABET[n % 62] + code;
n = Math.floor(n / 62);
}
return code;
}
for (const n of [1, 61, 62, 125, 1_000_000_000]) {
console.log(`${String(n).padStart(10)} -> ${toBase62(n)}`);
}
Expected output:
1 -> 1
61 -> Z
62 -> 10
125 -> 21
1000000000 -> 15FTGg
Advantages:
- Every code is unique.
- No collision checks.
- Fast writes.
The challenge is the counter.
A single counter becomes a bottleneck.
ID Ranges
Instead of requesting one number at a time:
- Server A receives IDs 1-1000
- Server B receives IDs 1001-2000
Each server creates codes locally. Step through two servers sharing the work:
Here is the same idea in code:
Each server takes a block of 1,000 numbers
let nextBlockStart = 1_000_000_000;
function takeBlock() {
const block = { next: nextBlockStart, end: nextBlockStart + 999 };
nextBlockStart += 1000;
return block;
}
const serverA = takeBlock();
const serverB = takeBlock();
const newId = (server) => server.next++;
console.log(`Server A: ${newId(serverA)}, ${newId(serverA)}`);
console.log(`Server B: ${newId(serverB)}, ${newId(serverB)}`);
console.log(`Trips to the range service for 2,000 links: 2`);
Expected output:
Server A: 1000000000, 1000000001
Server B: 1000001000, 1000001001
Trips to the range service for 2,000 links: 2
What to notice:
- Very few calls are needed.
- No collisions occur.
- Writes remain fast.
Option 3: Random Codes
Generate a random 7-character code.
Before saving:
- Check if the code exists.
- If it exists, generate another code.
- Save when a free code is found.
Advantages:
- Simple
- Difficult to guess
Disadvantages:
- Requires a database check.
- Collisions must be handled.
QuickBite's Choice
QuickBite selects Counter + ID Ranges + Base62.
Because:
- Codes are unique by design.
- No collision checks are needed.
- Code generation is fast.
- The system scales easily.
Step 5: The High-Level Design
There are two different paths.
Redirect Path
This is the busy path.
Around 5,800 redirects happen every second during peak traffic.
Flow:
- User opens short URL.
- Request reaches the load balancer.
- App server checks the cache.
- On a cache miss, read from the database.
- Store the result in cache.
- Return a 302 redirect.
- Send click events to a queue.
Why use a queue?
Because counting clicks should never slow down redirects.
Step through one redirect, first as a cache miss and then as a hit:
Create Path
This path is much smaller.
Around 60 requests per second at peak.
Flow:
- User creates a short link.
- Request passes through a rate limiter.
- App server receives an ID from its local range.
- Convert ID to Base62.
- Save the mapping in the database.
Data Model
A simple table is enough.
| Field | Example |
|---|---|
| code | 3xK9pQ |
| long_url | https://quickbite.app/... |
| created_at | timestamp |
| expires_at | timestamp |
Lookups happen using the code.
The code is the primary key.
If growth becomes very large, partition data by code.
Cache Size
Most clicks occur on recently created links.
Older links receive very little traffic.
Suppose:
- 10 million links are clicked each day.
- The hottest 20% receive most traffic.
10 million links × 20% × 500 bytes = about 1.0 GB of cache for the hot links.
What to notice:
- Hot links fit comfortably in memory.
- Cache greatly reduces database traffic.
- Redirect performance improves significantly.
Step 6: Build It on the Canvas and Test It
Open the design sandbox.
The starting design includes:
- 1 load balancer
- 2 app servers
- 1 database
- No cache
- No queue
The simulator generates:
- Link creation traffic
- Redirect traffic
- Infrastructure failures
| Check | Passes when |
|---|---|
| Peak load | p99 at most 100 ms and under 0.1% errors |
| Lose any one machine | Errors under 1% and p99 under 250 ms |
| Cost | At most $1,500 per month |
| Requirements | 302 redirects and unique code generation |
Build a scalable URL shortener
Run the starting design first.
Then add:
- Cache
- Queue
- ID range service
- Additional servers
Compare the results.
What you will probably find:
- One load balancer creates a single point of failure.
- Redirect traffic overloads the database without caching.
- Database writes for click tracking slow down redirects.
- Caching improves performance dramatically.
- Queues simplify click tracking.
- ID ranges eliminate code-generation bottlenecks.
With only one cache node, the "Lose 1 cache node" check sends most requests to the database at once, and p99 jumps to about 366 ms. Keep a second cache node, or give the database enough copies to cope without the cache.
A design that passes:
- 2 load balancers
- 4 app servers
- 2 cache nodes
- 2 database copies
- ID range service
- Queue
This design costs about $1,360 per month and keeps p99 latency around 22 ms.
Common Mistakes
| Mistake | Wrong | Correct | Why It Happens |
|---|---|---|---|
| Ignoring estimates | Start designing without numbers | Estimate traffic first | Numbers drive architecture |
| Hash collisions | Assume hashes never collide | Handle collisions or avoid them | Collisions are underestimated |
| Wrong redirect type | Use 301 for analytics | Use 302 | Browser caching hides clicks |
| Single counter | One central counter | ID ranges | Central counters become bottlenecks |
| Sequential codes | Easy-to-guess URLs | Scramble IDs before encoding | Predictable code generation |
| No cache strategy | Depend entirely on cache | Plan for cache failures | Cache outages are often ignored |
Practice
What to Remember
- A URL shortener is a read-heavy lookup system.
- Redirect traffic is much larger than create traffic.
- Estimate first before designing.
- Use 7-character Base62 codes.
- Use 302 redirects when clicks must be counted.
- Counter + ID ranges guarantees unique codes.
- Cache should handle most redirect traffic.
- Click tracking should happen through a queue.
- Test designs against peak load, failures and cost.
Further Reading
These are the sources this lesson was checked against:
- System Design Primer: Design Pastebin.com (or Bit.ly): a worked short-link design, with estimates and base62 codes.
- MDN: 301 Moved Permanently: how browsers treat a permanent redirect.
- MDN: 302 Found: the temporary redirect used here.
- RFC 9110, HTTP Semantics: Redirection 3xx: the standard that defines these status codes.
- Wikipedia: Birthday problem: the maths behind hash clashes.
Next: Rate Limiter Service, where QuickBite turns rate limiting into a shared service that protects all APIs.