#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?

I built this as a proper URL shortener, not just a toy redirect app. It supports custom slugs, tracks clicks, and records analytics like location data. On the surface it looks like a simple product, but under the hood it taught me a lot about request paths, caching, security, and what happens when you deploy something too early without locking it down properly.

The technical side was useful, but the bigger reason this project matters to me is that it went wrong in a very real way. A few days after launch, the service got hit badly. Rebuilding it after that taught me more than the original version ever could.

The architecture

The stack was pretty direct. FastAPI handled the HTTP API, PostgreSQL stored the shortened URLs and metadata, and Redis handled rate limiting plus some lightweight fast-path logic. When someone created a short URL, I stored the original target and generated a code. When someone visited that short code, the backend looked it up and issued a redirect.

Analytics made the redirect flow more interesting. I wanted geolocation data, but I learned very quickly that you should not do slow third-party work directly inside a hot redirect path. So the safer design became: respond fast, queue extra work, and keep memory bounded. I added a background queue for geo lookups and capped its size so traffic spikes could not grow it forever.

The incident (January 6–8, 2026)

This is where I learned the difference between a project that works and a project that survives the internet. At the time, every redirect was making a synchronous call to `ipwho.is` with no caching. That meant every bot hit amplified into extra network traffic and extra latency. Under load, bandwidth jumped hard and the redirect path became much more expensive than it should have been.

The worse problem was security. PostgreSQL was exposed publicly on port 5432, and I had left the default credentials in place. Redis was exposed too. Attackers got in, wiped the data, and left a ransom note asking for BTC. No backups. No excuses. That was entirely on me, and it forced me to take deployment hygiene seriously after that.

Post-incident hardening

After the incident, I rebuilt the service with a much more defensive mindset. I locked down network exposure, added proper firewall rules, removed the bad default credentials problem, and made sure internal services were not just sitting open to the public internet. That alone changed the whole posture of the app.

I also fixed the request path itself. Geo lookups moved behind caching and queueing. I added an LRU cache so repeated IP lookups would not keep hitting the external API. I added Redis-backed rate limiting and IP blocking for abusive patterns. I added a queue cap so analytics traffic could degrade gracefully instead of eating memory forever. The current version is basically a list of lessons learned the hard way.

Short code generation

I kept short code generation intentionally boring because boring is reliable. PostgreSQL gives me a unique integer ID for each row. I take that integer and base62-encode it using digits, lowercase letters, and uppercase letters. That gives a compact short code without needing random collision checks.

The nice thing about this approach is that uniqueness comes from the database itself. I am not generating random strings and hoping they are free. I am taking a unique integer and turning it into a shorter representation. That means no collision retries, no extra lookup loop, and a very predictable path from insert to final short URL.

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