Behind the BFCM dashboard: live metrics at scale
Vercel's Black Friday–Cyber Monday (BFCM) dashboard tracked real-time infrastructure metrics—deployments, requests, blocked traffic, and more—throughout the retail rush. Building it meant solving several data-intensive problems: keeping queries cheap, rendering a shared view for every visitor without hammering the database, and making numbers feel alive instead of jumping every few seconds. Here's how the engineering played out.
Polling architecture with a cache layer
The dashboard is a Next.js app built on a simple polling loop. The client asks the server for fresh metrics every 10 seconds; the server queries an internal database and returns the aggregates. All the underlying request, deployment, and firewall data flows into that database from Vercel's APIs and services.
Because the metrics of interest required scanning terabytes, the team needed to avoid long-running queries while keeping the experience responsive.
Cost: rolling windows plus a KV counter
The first approach—a straightforward query for total request count—degraded as traffic grew. Each request scanned an ever-larger dataset. The fix was to query a five-minute rolling window instead of the entire BFCM period. Counting records from just the last five minutes is far cheaper and faster, but it no longer yields the cumulative total the dashboard must show.
SELECT count() FROM requests WHERE timestamp > '2024-11-29 00:00:00'
SELECT count() FROM requests WHERE timestamp > now() - INTERVAL 5 MINUTE
To reconstruct the total, the team used the Upstash KV integration from the Vercel Marketplace. The server stores a running cumulative count in KV and adds the latest five-minute increment to produce the displayed number.
Speed: ISR to cut database calls
Every visitor sees identical data, so hitting the database per request is wasteful. With Incremental Static Regeneration (ISR), the page fetches data once and caches it. Adding two lines to page.tsx—declaring the page static and setting time-based revalidation—produced the desired behavior:
- If the cached page is newer than five seconds, serve it immediately.
- If it's older, return the cached version and regenerate a fresh copy in the background for the next visitor.
// Tell Next.js to render the page as a static page, despite having fetch calls
export const dynamic = 'force-static';
// Invalidate the cache after 5 seconds
export const revalidate = 5;
export default function Page() { ... }
This way the database is queried at most once per five-second window, regardless of traffic volume.
Frontend: simulating continuous growth
The polling interval of 10 seconds meant numbers updated in discrete jumps, which felt clunky. To make the counters look like they were constantly climbing, the team calculated each metric's rate of change. Since the previous count was already stored in KV, deriving the rate needed no new infrastructure.
export function getRateOfChange() {
const lastCount = getCountFromKv();
const newCount = fetchLatestCount(); // only from last 5 minutes
const rateOfChange = newCount / (Date.now() - lastCount.timestamp)
return rateOfChange;
}
The server passes both the current value and the rate to a client component, which uses requestAnimationFrame to increment the displayed number smoothly between polls.
'use client';
import { useEffect, useRef } from 'react';
export function Counter({ value, rateOfChange }) {
const ref = useRef(null);
useEffect(() => {
let id;
const increment = (ts) => {
if (!ref.current) return;
ref.current.textContent = value + rateOfChange * ts;
id = requestAnimationFrame(increment);
};
id = requestAnimationFrame(increment);
return () => {
cancelAnimationFrame(id);
};
}, [rateOfChange]);
return <span ref={ref}>{value}</span>;
}
Keeping the visual count honest
Frontend incrementation can drift from the backend's actual value. To correct that, the component continuously compares the displayed number with the latest server value. If the animation overshot, the rate slows down; if it lagged, the rate speeds up. That dynamic adjustment requires a few steps per tick: record the previous rate and value, compute the delta from the new value, adjust the animation speed, then update the refs for the next cycle.
'use client';
import { useEffect, useRef, useState } from 'react';
export function Counter({ value, rateOfChange }) {
const [rate, setRate] = useState(rateOfChange);
const lastMetric = useRef({
value,
timestamp: null,
});
useEffect(() => {
const percentageDiff = lastMetric.current.value / value;
setRate(rateOfChange * percentageDiff);
}, [value, rateOfChange]);
const ref = useRef(null);
useEffect(() => {
let id;
const increment = (ts) => {
if (!ref.current) return;
const { timestamp, value } = lastMetric.current;
lastMetric.current.timestamp = ts;
const lastTime = timestamp ?? ts;
const diff = (ts - lastTime) * rate;
const newValue = value + diff;
lastMetric.current.value = newValue;
ref.current.textContent = newValue;
id = requestAnimationFrame(increment);
};
id = requestAnimationFrame(increment);
return () => {
cancelAnimationFrame(id);
};
}, [rate]);
return <span ref={ref}>{value}</span>;
}
Performance: moving data fetching to the server
The dashboard is almost entirely React Server Components (RSCs); only the animation components need client-side execution. This simplifies data fetching considerably. In a conventional client-side setup, data would arrive only after the browser downloads and executes JavaScript, and the fetch itself would depend on the user's connection—and the app would need exposed API endpoints. With RSCs, the component calls getData directly on the server using async/await, with no client-side round trip or dedicated API route.
const [data, setData] = useState(null);
useEffect(() => setData(getData()), []);
export async function GET() {
return getData();
}
async function Statistics() {
const { value, rateOfChange } = await getData();
return <Counter value={value} rateOfChange={rateOfChange} />;
}
Initial page loads ship fully rendered components with all data included, and no extra client requests are made on first visit.
Summary
- Use rolling-window queries plus a KV store for cumulative totals.
- Leverage ISR so a shared page is regenerated at most once per interval.
- Compute a rate of change to render continual counter movement.
- Add dynamic rate correction so the animation converges on real values.
- Fetch data in server components to cut client round trips and API surface.
Cost, latency, accuracy, and feel don't have to be traded off against each other. The BFCM dashboard shows that each bottleneck can be removed in isolation, stacking the wins into one coherent architecture.



