May 19, 2026·Karlo Hrvačić

Async Everything: Keeping Redirects Fast While Counting Millions of Visits

How we use Spring's TaskExecutor to fire-and-forget visit tracking, email sending, and cache warmup — keeping the redirect path under 5ms.

engineering
performance
async
spring

Every click on a short URL does two things: redirect the user to the destination, and record the visit. The redirect must be fast. The visit recording can wait. Here's how we separate them.

The Redirect Path

When a user visits hrva.cc/abc123, the backend checks Redis for cached URL data. If found and valid (not expired, not deactivated, under visit limit), the redirect response is returned immediately while the visit counter is incremented asynchronously in a background thread.

Async Visit Tracking

Spring's TaskExecutor runs the visit tracking in a separate thread. The method fireVisitTracking loads the URL entity from the database (it's already cached for the redirect, but we re-fetch the entity for the write), checks for duplicate IPs, increments the counter via url.onVisit(), and saves. If the visit exceeds the limit, the URL is automatically deactivated.

This means the redirect response time is independent of the database write latency. A slow database write doesn't slow down the redirect. The user gets their 302 response in milliseconds while the visit is recorded behind the scenes.

Async Email Sending

Email is another fire-and-forget operation. Welcome emails, password resets, expiry notifications, and malware alerts all go through @Async methods. The tryToSendEmail method attempts to send up to 3 times with exponential backoff, but the caller doesn't wait — the email is queued and delivered in the background.

Async Cache Warmup

On application startup, the CacheWarmup component loads the 20 most visited, 20 most recent, and 20 most recently accessed URLs into Redis. This runs asynchronously after the application context is ready, so the application starts serving requests immediately while the cache fills in the background.

What We Don't Make Async

Not everything should be async. URL creation, deletion, and schema migrations are synchronous — the caller waits for confirmation. Authentication is synchronous (you need to know if login succeeded before proceeding). The rule: if the user needs to see the result, it's synchronous. If it's bookkeeping that happens after the response, it's async.