# URL Shortener Follow-Ups: Failures, API Detail and Cost (2026)

> The production half of the URL shortener problem: clarifying questions, an API that is safe to retry, a failure table with arithmetic, consistency, alerts, cost, key rotation and the deep dives interviewers probe.

*Source: https://www.infowok.com/design-a-url-shortener-follow-ups/ · Sukhveer Kaur · Published October 24, 2026*

---

The core design is done and the diagram looks right. Then the interviewer asks what happens when the cache restarts at peak, how a retried `POST` avoids making two links, and what the design costs per month. None of this changes the boxes. All of it decides whether the design survives a bad Tuesday.

This is the second half of [Design a URL shortener](/design-a-url-shortener/), which builds the design itself. It holds the questions that fill the last fifteen minutes of the round.

<Prerequisites>

- [Design a URL shortener](/design-a-url-shortener/): number blocks, scrambled base62 codes, cache-aside reads and the 302 decision.
- [Caching strategies](/caching-strategies/) and [the CAP theorem](/cap-theorem-explained/) help with the failure and consistency sections.

</Prerequisites>

<Callout type="note" title="Remember from the core lesson">
Why does the design return 302 instead of 301, and why is the number scrambled before it is encoded? A 302 keeps every click, expiry and takedown under the service's control. The scramble stops anyone walking the codes in order.
</Callout>

<KeyTakeaways>

- State assumptions before drawing. An unstated assumption looks like a missing requirement.
- Make creates safe to retry with an idempotency key, answer a taken alias with 409, and page through links with a cursor.
- For each part, say what breaks, by how much, and what saves you. Arithmetic beats adjectives.
- Redirects choose availability. Alias creation chooses consistency. Say both, and why.
- Alert on the error budget, not on single errors, and watch the cache hit rate because it is the database's real load.
- Count clicks off the redirect path. Rotate the scramble key by versioning number ranges.

</KeyTakeaways>

<LessonOutcome>

Extend a working URL shortener for production: ask the clarifying questions, design a retry-safe API and data model, tabulate failures with numbers, state the consistency stance, set alerts and a cost estimate, rotate the scramble key safely, and answer the usual deep dives.

</LessonOutcome>

## Ask Before You Draw

Spend the first two minutes on questions. Where the interviewer shrugs, state an assumption out loud and move on.

| Question | Assumption if no answer |
|---|---|
| Anonymous creates, or accounts? | Accounts, so quotas and listing work |
| Custom aliases? | Yes, with a minimum length |
| Default expiry? | None, but a user can set one |
| Can a link be edited? | No, create and disable only |
| Clicks counted? | Yes, daily totals, a few minutes late |
| How many regions? | One first, a second as a follow-up |
| Is abuse in scope? | Yes, at creation and by report |

These are judgement calls, not facts. The point is that each one changes a later decision: accounts enable rate limits per user, and "no editing" is what makes 302 caching safe.

## API Detail That Interviewers Probe

The core lesson had two endpoints. A production contract needs more.

`POST /links` is not idempotent by definition: [RFC 9110 section 9.2.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2) lists only safe methods, `PUT` and `DELETE` as idempotent. A request that times out and is sent again can create two links. Two fixes:

- **Idempotency key.** The client sends an `Idempotency-Key` header, and the server stores the first response under `(owner_id, key)`. [Stripe](https://docs.stripe.com/api/idempotent_requests) may prune keys once they are 24 hours old and compares the parameters of a retry with the original. The header is still an [Internet-Draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/), not an RFC.
- **Deterministic alias.** Derive the code from the owner and the normalised long URL, so the same request always lands on the same row.

| Case | Status | Note |
|---|---|---|
| Created | 201 | Body has `code` and `short_url` |
| Retry with same key | 201 or 200 | Stored first response |
| Alias taken | 409 | [Conflict with current state](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.10) |
| Malformed URL | 400 or 422 | Validate before storing |
| Not logged in | 401 | Blocked destination: 403 |
| Over quota | 429 | Add `Retry-After` ([RFC 6585](https://www.rfc-editor.org/rfc/rfc6585#section-4)) |

For `GET /links?limit=20&cursor=...`, use a cursor over `(owner_id, created_at, code)`, not an offset. Assume 20 links a page: page 500 makes an offset query skip 10,000 rows. A cursor seeks straight to the next row. Limit creates with [a rate limiter](/design-a-rate-limiter/).

## Data Model and Indexes

Assume a 500-byte row, as in the core lesson.

```text
links(code PK, long_url, host,
      owner_id, created_at,
      expires_at, status)
idx_owner(owner_id,
  created_at DESC, code)
idx_host(host)    -- takedown
idx_expiry(expires_at)
idem(owner_id, key PK,
  response, created_at)
```

- `code` serves the redirect. `idx_owner` serves the listing cursor.
- `host` disables every link to one bad domain in one statement.
- `idx_expiry` lets cleanup skip a table scan.

Do not index `long_url`: nothing queries by it, and each index adds write cost.

## What Breaks, By How Much

Every number here is an assumption carried over from the core lesson: 120 writes a second and 12,000 reads a second at peak, a 90 percent cache hit rate, and replicas that serve about 2,000 reads a second each.

<Callout type="tip" title="Predict first">
The cache cluster restarts at peak. How much load reaches the database, and how does it compare with three replicas? Answer, then read the first table row.
</Callout>

| Part | What happens | Arithmetic | What saves you |
|---|---|---|---|
| Cache down | Every read misses | 12,000 against 3 x 2,000 = 6,000 a second: twice capacity, against 1,200 a second normally | Coalescing, local cache, load shedding |
| Counter store down | No new blocks | Runway = block size / per-instance rate | Bigger blocks, alert on blocks left |
| Primary down | Writes fail until failover | Assume 30 s: 30 x 120 = 3,600 failed creates. Assume 1 s of lag: up to 120 links lost | Replica reads, retries, semi-sync |
| One replica down | Capacity falls | 2 x 2,000 = 4,000, still above the 1,200 normal | Spare replica |

![A cache restart at peak: all 12,000 reads a second miss and reach three replicas that serve about 6,000, causing timeouts; coalescing misses, a short local cache and a cap on database reads stop it](./design-a-url-shortener-follow-ups-stampede.svg)

### Cache down is the stampede

Assume one hot code gets 2,000 reads a second and a database fetch takes 5 ms. That puts 2,000 x 0.005 ≈ 10 fetches in flight for that one key. With request coalescing it is one. Also keep a short local cache in each server and cap concurrent database reads, so overload becomes a few slow answers instead of an outage.

### Counter down is a sizing question

Assume 4 instances share 120 writes a second, so 30 each.

- Assume a block of 1,000: it lasts ≈ 33 seconds at peak.
- Assume a block of 10,000: it lasts ≈ 333 seconds, or 5.5 minutes.
- Assume a crash wastes at most one block: 10,000 of 3.52 trillion codes.

 Alert when an instance holds under half a block.

### Primary down can lose links

It loses data if replication is asynchronous. [Sentinel's documentation](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/) says acknowledged writes are not guaranteed to survive a failover. For links, that means a user holds a code that later returns 404. Because writes are only 120 a second, a semi-synchronous write path is affordable. State the RPO (about zero for links) and RTO (about 30 seconds, an assumption) you chose.

<Callout type="warning" title="Failed-over counters repeat numbers">
A failed-over counter can reissue numbers it already handed out. The `UNIQUE` constraint on `code` rejects the duplicate and the service takes a fresh number.
</Callout>

## Consistency: Two Answers, Not One

The honest stance is split by operation. In [CAP terms](/cap-theorem-explained/):

| Operation | Choice | Why |
|---|---|---|
| Redirect read | Available, may be stale | A broken link breaks pages. Staleness is bounded by the cache TTL |
| Alias create | Consistent | A conditional insert on the primary. During a partition it fails instead of giving one alias to two users |
| Takedown | Fast, not instant | Stale for at most the shorter of TTL and edge `max-age` |

In PACELC terms, with no partition, reads trade consistency for latency (replicas and caches), while alias creation pays latency for consistency. This is design judgement, so say it as a choice you made, not a law.

## Watch the Right Numbers

Pick a few signals and give each a threshold.

- **p99 redirect latency** at the read service, against the 100 ms target assumed in the core lesson.
- **Cache hit rate.** The database load is `(1 - hit rate) x reads`. Assume 90 percent: it is 1,200 a second. At 80 percent it doubles to ≈ 2,400. Alert at 85 percent (an assumption), where the load is 1,800, well inside the 6,000 limit.
- **Assumed redirect availability SLO** of 99.9 percent. Assume 4,000 reads a second over 2.5 million seconds: ≈ 10 billion redirects a month, so the budget is 10 million failures, or 43.2 minutes of full outage in a 30-day month.
- **Replication lag, blocks left per instance, abuse reports waiting.**

Page on budget burn, not on single errors. The [Google SRE Workbook](https://sre.google/workbook/alerting-on-slos/) recommends pairing a long and a short window: for a 99.9 percent SLO, a burn rate of 14.4 over one hour (confirmed over five minutes) pages, because it consumes 2 percent of the budget. Treat its numbers as a starting point to tune.

## What It Costs

Prices change, so these are dated and the rest are labelled assumptions. Assume the ≈ 10 billion redirects a month from above.

| Item | Arithmetic | Result |
|---|---|---|
| Edge requests | Cloudflare Workers paid plan, checked 2026-10-24: $5 a month, 10 million included, $0.30 per extra million. (10,000 - 10) x $0.30 + $5 | about $3,000 a month, request charge only |
| Egress | Assume 400 bytes per 302 and $0.09 per GB. 10 billion x 400 B = 4 TB | about $360 a month |
| Cache memory | Assume the hottest 1 percent of 6 billion links, 600 bytes each: 60 million x 600 B | about 36 GB |

The lesson is the shape, not the totals. Edge serving bills every redirect, so use it for the hot codes and let origin caches take the rest. [Cloudflare's page](https://developers.cloudflare.com/workers/platform/pricing/) also bills CPU time separately, so read it before quoting a total.

## Deep Dive: Counting Clicks at Scale

Assume 4,000 clicks a second and 50 bytes per event: 200 KB a second, about 17 GB a day. Writing a database row per click would put a write on the hot path.

![Click counting: the redirect emits an event without waiting, a durable log holds it, an aggregator sums clicks per code per 10 seconds, and a store keeps one row per code per minute](./design-a-url-shortener-follow-ups-analytics.svg)

- **Emit and move on.** If the log is slow, drop the event. A redirect never waits for analytics.
- **Aggregate before writing.** Assume a link with 2,000 clicks a second: it becomes one write every 10 seconds per code.
- **Expect duplicates.** Logs deliver at least once, so dedupe on an event ID where counts feed billing. Charts can tolerate small error.

See [event-driven architecture](/event-driven-architecture/) and [Kafka vs RabbitMQ](/kafka-vs-rabbitmq/) for the log choice.

## Deep Dive: Abuse and Takedown at Scale

At creation, check the destination against a reputation service such as [Google Web Risk](https://docs.cloud.google.com/web-risk/docs/overview), which says its lists can miss risky sites and flag safe ones. So add layers:

1. **Rate limit and quota** per user and per address.
2. **Re-scan popular links** on a schedule, hottest first, because a page can turn bad later.
3. **Take down by host.** `idx_host` disables every link to one domain in one statement.
4. **Purge every layer:** mark the row disabled, delete the cache entry, purge the edge.

The worst-case exposure after a takedown is the cache TTL or the edge `max-age`, whichever is shorter. Assume 60 seconds for both and say so. A 301 would make this unbounded, which is why the core lesson chose 302.

## Rotating the Scramble Key

The key is a secret, so sooner or later someone asks how to change it. Existing links survive, because a redirect looks up the stored code and never reverses the scramble.

The one place that does reverse it is the cross-region trick from the core lesson, which decodes a code to a number to find its home region. That is what depends on the key.

![Rotating the key: tag number ranges with a key version, issue new blocks under the new key, route cross-region clicks by trying both keys and then asking other regions, and retire the old key last](./design-a-url-shortener-follow-ups-rotation.svg)

Rotation protects codes made after the change. It cannot un-leak codes already issued under a leaked key. Moving from 6 to 7 characters works the same way: codes are strings, so old ones stay valid.

## Practice: Size the Block

<Exercise title="Survive a ten-minute counter outage" time="10 min">

Assume 4 instances, 120 writes a second at peak, and a counter store that can be down for 10 minutes. Work out:

1. The block size each instance needs.
2. The most codes wasted if all four crash.
3. One alert that gives warning.

<Hint>

Per-instance rate is 120 / 4. Runway is block size divided by that rate.

</Hint>

<Expected title="One reasonable answer">

```text
rate     = 120 / 4 = 30 per second
10 min   = 600 s
block    >= 30 x 600 = 18,000
choose   = 20,000 (a round number)
wasted   <= 4 x 20,000 = 80,000
share    = 80,000 / 3.52e12 ~ 2e-8
```

A block of 20,000 covers the outage with some margin, and a crash of every instance wastes about two billionths of the 7-character space. Alert when any instance holds under 10,000 numbers, which at 30 a second is about five minutes of warning. The trade-off is that bigger blocks waste more on a crash, which costs almost nothing here.

</Expected>

</Exercise>

## The 45-Minute Plan

| Minutes | Do |
|---|---|
| 0 to 5 | Questions, assumptions, requirements, out of scope |
| 5 to 12 | Entities, API with idempotency and errors, estimate |
| 12 to 20 | Simple design, then the read path and cache |
| 20 to 32 | Deep dives: codes, redirect status, one failure table |
| 32 to 40 | Consistency stance, abuse and takedown, regions |
| 40 to 45 | Observability, cost, recap, follow-ups |

This is a suggestion. Cut cost before cutting the failure table.

## What Each Level Should Show

| Level | Expected in the follow-up round |
|---|---|
| Mid-level | Names the cache stampede and the 409 for a taken alias when prompted |
| Senior | Brings the failure table and idempotent creates unprompted, with numbers |
| Staff+ | Splits consistency by operation, rotates the key safely, sets SLO burn alerts, prices the edge |

<InInterview>

Expect one-line follow-ups. Answer with a trade-off, not a tool name:

| They ask | Answer in one line |
|---|---|
| Client retries a create? | Idempotency key, stored response |
| Two users want one alias? | Conditional insert, loser gets 409 |
| Cache restarts? | Coalesce, local cache, cap database reads |
| Counter store dies? | Block size sets the runway |
| Primary dies? | Promote a replica, state RPO and RTO |
| A link goes viral? | Replicate the hot entry, edge absorbs it |
| Count clicks? | Log, aggregate, write per window |
| Phishing link reported? | Disable, purge every layer, ban the host |
| Leaked scramble key? | Rotate by range version, new codes only |
| How do you know it works? | Latency, hit rate, SLO burn |

</InInterview>

## Check Yourself

<CheckYourself>

<Question q="Why is a database row per click a bad idea on the redirect path, and what replaces it?">

It adds a write to the hottest path and a slow database can slow every redirect. Emit an event to a log, aggregate per code per window, and write the totals, so the redirect never waits.

</Question>

<Question q="The hit rate falls from 90 to 80 percent. What happens to database load?">

It doubles from 1,200 to 2,400 reads a second at peak, because load is (1 - hit rate) times reads. That is why the hit rate gets an alert before the database does.

</Question>

<Question q="Why can the scramble key be rotated without breaking old links?">

Redirects look up the stored code and never reverse the scramble. Only cross-region routing decodes a code, so it tries the new key, then the old, then asks other regions.

</Question>

<Question q="Why does alias creation choose consistency while redirects choose availability?">

A stale redirect costs one slightly old answer, bounded by the TTL. Two users holding the same alias is a correctness bug, so the conditional insert fails during a partition rather than risk it.

</Question>

</CheckYourself>

<Callout type="tip" title="Learning tip">
Cover the table in the failure section and rebuild it from memory with the numbers. Do it again tomorrow. The arithmetic, not the part names, is what interviewers listen for.
</Callout>

## In One Picture

```text
Ask first
  accounts? aliases? regions?
API
  idempotency key, 409, 429,
  cursor paging
Failures
  cache x2 over capacity;
  block size = runway; RPO/RTO
Consistency
  reads available,
  alias consistent
Watch
  p99, hit rate, SLO burn
Cost
  edge bills every request
Rotate
  version the number ranges
```

## Conclusion

The follow-up answer is everything around the read-path machine: a create that is safe to retry, a failure table with numbers, a split consistency stance, budget-based alerts, an honest cost line and a way to change the key. State the assumption each time and the answers stay short.

**Which follow-up would catch you out?** Tell us in the comments.

<NextSteps>

- **Back to the core design:** [design a URL shortener](/design-a-url-shortener/) for number blocks, caching and 302.
- **The box guarding creates:** [design a rate limiter](/design-a-rate-limiter/) and [rate limiting explained](/rate-limiting-explained/).
- **Next problem:** [news feed design](/design-a-news-feed/), another read-heavy system.

</NextSteps>
