#2

URL Shortener

Early December 2025

PythonFastAPIPostgreSQLRedis

A snappy URL shortener service with analytics tracking, custom slugs, and click stats. Simple, fast, and reliable.

What is it?

A URL shortener with click tracking and geolocation analytics — custom slugs, 7-character short codes, redirect latency measured in milliseconds. What makes this project interesting isn't the happy path. It's what happened after launch: a ransomware attack that wiped the database.

The architecture

FastAPI + PostgreSQL for URL storage (self-managed on a GCP VM, not Cloud SQL), Redis for rate limiting, and a background queue for async click tracking with IP geolocation via ipwho.is. The queue caps at 1000 items — a lesson learned the hard way about unbounded queues under load.

The incident (January 6–8, 2026)

Three days after launch, the VM was used as a DDoS amplifier. Every redirect hit ipwho.is synchronously with no caching — under bot traffic, bandwidth spiked from 15KB/s to 1.15MB/s (9.2 Mbps). Node.js hit 130% CPU.

Worse: PostgreSQL port 5432 was open to 0.0.0.0/0 with default credentials (postgres/postgres). Attackers found it, deleted all URL data, and left a readme table demanding 0.0051 BTC. They also revoked superuser privileges — recovery required starting Postgres in --single user mode.

Redis at port 6379 was also public. The URL shortener data was gone. Not backed up.

Post-incident hardening

Everything in the current codebase is a direct response to that incident:

Origin allowlist middleware — unknown origins blocked unless they present an API key. Bad domain blocklist — bit.ly, known adult sites, gambling domains blocked at submission. Rate limiting — Redis setex 1s window, 10 req/s per IP; 1000 total req/24h triggers IP block. LRU geo cache — 10K IPs cached to prevent per-request ipwho.is calls (the root DDoS amplifier). Queue size limit — max 1000 items in the click queue to prevent memory exhaustion. New DB password, firewall rules closing ports 5432 and 6379 to public internet.

Short code generation

Each URL gets a SERIAL ID from PostgreSQL. That integer is base62-encoded (0-9, a-z, A-Z) to produce a 7-character short code. Base62 with 7 chars covers 3.5 trillion unique URLs — collision-free because the IDs are unique by definition. No hash collisions, no retry logic needed.

Key takeaways

  • Never expose PostgreSQL or Redis ports to 0.0.0.0/0 — always use firewall rules or VPC
  • Synchronous external API calls in redirect paths become DDoS amplifiers — always cache or queue
  • LRU cache for geolocation: reducing external API calls with bounded memory (10K entries)
  • Redis rate limiting: setex 1s window pattern, IP blocking after threshold
  • Database credentials hygiene: change defaults immediately, before the port is even opened
Try it live →Watch on YouTube →← all projects