A Go backend that shortens URLs, redirects with caching, and stays safe under concurrent load.
go run ./cmdOpen http://localhost:8081 for the UI, or use curl:
curl -X POST http://localhost:8081/shorten \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'Client (HTML / curl)
↓
HTTP API (net/http)
↓
Rate Limiter → Cache → DB
↓
Short Code Generator (atomic-safe)
Data flow model:
Request
-> Rate limiter (drop if too fast)
-> Logger
-> Handler
POST /shorten -> validate URL -> generate code -> save DB -> cache it
GET /{code} -> check cache -> else DB -> cache it -> redirect
- REST APIs for shorten and redirect
- Collision-safe Base62 short codes (atomic counter)
- SQLite persistence with restart-safe ID recovery
- Token-bucket rate limiting (per client IP)
- In-memory LRU cache for hot redirects
- Health and readiness probes
- Load testing script with
hey - Graceful shutdown
Mainly: No collisions -> atomic counter + Base62, not random strings. Restart safe -> on startup, max ID is read from DB so codes never repeat. Cache -> redirects are read-heavy; LRU keeps hot URLs out of SQLite. Rate limit -> token bucket per IP stops abuse without blocking normal traffic.
- Client sends: {"url": "https://example.com"}
- Rate limiter checks if the IP is sending too many requests.
- Handler parses the JSON.
- Service validates the URL (must be http or https).
- Generator creates a unique short code using an atomic counter + Base62 (a, b, … 9, ba, …).
- Storage saves short_code → long_url in urls.db.
- Cache stores the mapping in memory.
- Response: {"short_code":"b","short_url":"http://localhost:8081/b"}
- User visits http://localhost:8081/b
- Handler extracts b from the path.
- Service checks cache first.
- If not cached, reads from urls.db and caches it.
- Handler sends HTTP redirect (307) to the long URL.
- Redirects are read-heavy, so the cache avoids hitting the DB on every click.
- On startup in main.go:
- Open urls.db
- Find the last short code used
- Resume the counter from there
- So after restart you don’t reuse old short codes.
| Method | Path | Description |
|---|---|---|
| POST | /shorten |
Create a short URL |
| GET | /{shortCode} |
Redirect to the long URL |
| GET | /health |
Liveness check |
| GET | /ready |
Readiness check (includes DB ping) |
| GET | / |
Simple web UI |
POST /shorten
{ "url": "https://example.com" }
Response
{
"short_code": "b",
"short_url": "http://localhost:8081/b"
}Install hey:
go install github.com/rakyll/hey@latestStep 1 - start server with rate limits raised (so the benchmark measures throughput, not 429s):
RATE_PER_SEC=10000 RATE_BURST=10000 go run ./cmdStep 2 - run the script in another terminal:
export PATH="$PATH:$HOME/go/bin"
chmod +x scripts/load_test.sh
./scripts/load_test.shLook for [307] on the redirect test and [200] on the shorten test. If you see mostly [429], the rate limiter is blocking the benchmark, use the env vars above.
Typical results on a local machine (with bench env vars):
- Redirect (cached): ~15k–25k req/s, p99 under 10ms
- Shorten (write): limited by SQLite writes (~500–1500 req/s)
The script uses -disable-redirects so hey only measures your server latency, not an external URL fetch.
Restart the server without the bench env vars and re-run the last section of the script to verify rate limiting returns 429 under burst traffic.
Configuration
- Token bucket rate limiter
- Refill rate: 20 requests/second
- Burst capacity: 40 requests
| Metric | Value |
|---|---|
| Requests | 5000 |
| Concurrency | 50 |
| Successful Redirects (307) | 43 |
| Rate-Limited Responses (429) | 4957 |
| p95 Latency | 5.7 ms |
| p99 Latency | 9.7 ms |
| Metric | Value |
|---|---|
| Requests | 500 |
| Concurrency | 20 |
| Successful Responses (200) | 1 |
| Rate-Limited Responses (429) | 499 |
| p95 Latency | 2.5 ms |
| p99 Latency | 3.4 ms |
Configuration
- Refill rate: 10000 requests/second
- Burst capacity: 10000 requests
| Metric | Value |
|---|---|
| Requests | 5000 |
| Concurrency | 50 |
| Throughput | ~17,931 req/sec |
| Success Rate | 100% |
| p95 Latency | 7.0 ms |
| p99 Latency | 13.9 ms |
| Metric | Value |
|---|---|
| Requests | 500 |
| Concurrency | 20 |
| Throughput | ~709 req/sec |
| Success Rate | 100% |
| p95 Latency | 81.2 ms |
| p99 Latency | 123.3 ms |
url-shortener/
├── cmd/main.go # wires everything together
├── internal/
│ ├── api/ # handlers, routes, logging middleware, HTTP layer
│ ├── cache/ # LRU cache
│ ├── health/ # /health and /ready
│ ├── ratelimit/ # token-bucket limiter
│ ├── shortener/ # ID generator and service logic
│ └── storage/ # Store interface and SQLite
├── scripts/load_test.sh # benchmarks
├── web/index.html # UI
└── README.md
go test ./...MIT




