How I Built a Real-Time LeetCode Stats Dashboard with 2-Minute Polling
2026-08-02 · 7 min read
Most portfolio sites hardcode their LeetCode stats. Mine refreshes every two minutes from a live API. Here's exactly how I built it — the architecture decisions, the edge cases, and the parts that surprised me.
The problem with direct client-side fetching
The obvious approach — calling the LeetCode API from the browser — runs into CORS immediately. LeetCode's official API doesn't expose public CORS headers, and third-party wrappers like alfa-leetcode-api are deployed on free Render instances that spin down after inactivity, meaning the first request after a cold start might take 20–30 seconds.
Hitting that from a client component means your entire UI stalls while waiting.
The server-side proxy pattern
The fix is a Next.js API route acting as a proxy. The browser calls /api/leetcode (same origin, no CORS), and the route handler calls the upstream API server-side with a 6-second timeout and a hardcoded fallback:
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 6000);
const [solvedRes, profileRes] = await Promise.all([
fetch(`${BASE}/${USERNAME}/solved`, { signal: controller.signal }),
fetch(`${BASE}/${USERNAME}`, { signal: controller.signal }),
]);
clearTimeout(timer);
If either call fails or times out, the route returns the last-known accurate stats with isFallback: true — so the UI always has something meaningful to show.
Cache-Control: the secret ingredient
The route returns Cache-Control: public, s-maxage=120, stale-while-revalidate=60. This means:
- A CDN (Vercel Edge, Cloudflare) caches the response for 2 minutes
- After 2 minutes, it serves stale data while regenerating in the background
- The browser never waits for a cold API call
Client-side polling at the same cadence
The component sets up a setInterval matching the server cache duration:
const POLL_MS = 2 * 60 * 1000;
useEffect(() => {
fetchStats(true);
intervalRef.current = setInterval(() => fetchStats(false), POLL_MS);
return () => clearInterval(intervalRef.current);
}, [fetchStats]);
The UI distinguishes between the initial load (full skeleton) and background refreshes (spinning indicator) so the user never sees a jarring blank state.
The Live/Offline badge
One detail I'm proud of: a badge that shows whether the data came from the live API or the fallback. The API route returns isFallback: boolean in every response, and the component maps that to a green LIVE badge or a red OFFLINE badge with a WiFi icon.
It also shows "Updated X minutes ago" by computing the delta from the fetchedAt timestamp every 15 seconds client-side — cheap and accurate without an extra API call.
What I'd improve next
The free Render deployment that hosts alfa-leetcode-api cold-starts unpredictably. The right long-term fix is a self-hosted scraper with a Redis cache sitting in front of it, so warm data is always sub-10ms. That's on the roadmap.