Home/Design Principles

Frontend System Design

Real-Time Dashboard at Scale

Design a real-time dashboard handling millions of events, thousands of concurrent users, and live updates without impacting performance.

23.4K events/sec98.7K concurrent users1,345 dashboardsp99 < 2s
1

Requirements Gathering

Functional

  • Live metrics & real-time charts
  • Alert notifications
  • User management
  • Search & filtering
  • Historical data
  • Export reports

Non-Functional

  • Page load < 2 seconds
  • Updates every second
  • 10K+ concurrent users
  • Responsive UI
  • Fault tolerant
  • Accessibility (WCAG 2.1)
2

High-Level Architecture

Backend APIs

REST / GraphQL

Initial data, user management, historical queries

WebSocket Server

Real-time updates

Live metrics push, alert notifications, presence

Frontend Dashboard

React Application

State management, rendering, caching, charts, tables

REST/GraphQL handles initial data loads and mutations. WebSocket carries live updates. The frontend maintains a unified state store fed by both channels — never duplicate sources of truth.

3

Frontend Folder Structure

src/
  pages/          # Route-based pages
  components/     # Reusable components
  features/       # Feature-based modules
  services/       # API calls, clients
  hooks/          # Custom React hooks
  store/          # Global state (Redux/Zustand)
  utils/          # Utility functions
  layouts/        # Layout components
  charts/         # Chart components
  websocket/      # WebSocket logic

Feature-based modules own their own components, hooks, and local state — reducing cross-feature coupling.

4

Dashboard Component Hierarchy

Dashboard

├─ Header

├─ Sidebar

├─ MetricsSection

│ ├─ RevenueCard

│ ├─ UsersCard

│ └─ OrdersCard

├─ ChartsSection

│ ├─ RevenueChart

│ ├─ TrafficChart

│ └─ CPUChart

├─ AlertsPanel

└─ ActivityFeed

5

State Management Strategy

Global Client State — Redux / Zustand

authuserPreferenceswebSocketStatusalerts
// Zustand store slice
const useUIStore = create((set) => ({
  webSocketStatus: 'disconnected',
  alerts: [],
  setStatus: (s) => set({ webSocketStatus: s }),
  addAlert: (a) =>
    set((state) => ({ alerts: [...state.alerts, a] })),
}));

Server State — TanStack Query

  • use useQuery for data fetches
  • use useInfiniteQuery for tables
  • avoid Storing server responses in Redux
const { data } = useQuery({
  queryKey: ['dashboard', userId],
  queryFn: fetchDashboard,
  staleTime: 5_000,
  refetchInterval: 30_000,
});
6

Real-Time Updates — WebSocket Design

Bad — Multiple connections

Each chart component opens its own WebSocket. Results in high overhead, race conditions, and dropped messages.

// ❌ Each component connects independently
function RevenueChart() {
  const ws = new WebSocket('/ws');
  ws.onmessage = (e) => setData(parse(e));
}
function TrafficChart() {
  const ws = new WebSocket('/ws'); // duplicate!
}
Good — Single service + fan-out

One singleton WebSocket service. Distributes events to the state store; components subscribe only to what they need.

// ✓ Singleton service, fan-out via store
class WebSocketService {
  connect() {
    this.ws = new WebSocket('/ws');
    this.ws.onmessage = ({ data }) => {
      const event = JSON.parse(data);
      store.dispatch(handleLiveEvent(event));
    };
  }
}
7

Handling 1000+ Updates / Second

Naive — dispatch per event
// ❌ 1000 dispatches/sec → UI freezes
socket.on('metric', (data) => {
  dispatch(updateMetric(data)); // every single event
});
Better — buffer + flush
// ✓ Buffer updates, flush every 1 sec
const buffer = [];
socket.on('metric', (data) => {
  buffer.push(data);
});

setInterval(() => {
  if (buffer.length === 0) return;
  dispatch(batchUpdateMetrics(buffer.splice(0)));
}, 1000); // one re-render per second

Batching reduces React reconciliations from 1000/sec to 1/sec. Pair with startTransition to keep interactions responsive while background updates are pending.

8

Rendering Optimization

  • React.memoSkip re-renders for stable props
  • useMemoMemoize expensive calculations
  • useCallbackStable function references for children
  • Code Splittinglazy() + Suspense per route/feature
  • VirtualizationOnly render visible rows/cells
  • Key propsStable keys prevent full subtree remounts
// Avoid inline functions — new ref every render
// ❌ <Chart onHover={() => setHovered(id)} />
// ✓
const handleHover = useCallback(
  () => setHovered(id), [id]
);
<Chart onHover={handleHover} />
9

Chart & Table Optimization

Charts

50k pointsDon't render all data points
Last 1k ptsWindow to visible range
DownsampleLTTB algorithm → 500 representative pts

Tables

60k rowsNever render all rows to DOM
react-windowOnly visible rows rendered
import { FixedSizeList } from 'react-window';

<FixedSizeList
  height={600}
  itemCount={60_000}
  itemSize={40}
  width="100%"
>
  {({ index, style }) => (
    <Row style={style} data={rows[index]} />
  )}
</FixedSizeList>
11

Data Fetching Strategy

Initial Load — Parallel Requests

// Fire all requests in parallel, not waterfall
const [summary, settings, history, filters] =
  await Promise.all([
    fetchDashboardSummary(),
    fetchUserSettings(),
    fetchHistoricalData({ range: '7d' }),
    fetchFilterOptions(),
  ]);

Caching Strategy

High freshness5 secLive metrics, alerts
Medium freshness30 secUser counts, summaries
Low freshness60 secConfig, filter options

Set staleTimeper query key — don't apply a single TTL globally.

12

Error Handling & Resilience

WebSocket Connection States

LoadingConnectedReconnectingDisconnectedOfflineError

Exponential Backoff Reconnect

1s → 2s → 4s → 8s → 16s → …

function reconnect(attempt = 0) {
  const delay = Math.min(1000 * 2 ** attempt, 30_000);
  setTimeout(() => {
    ws = new WebSocket(WS_URL);
    ws.onerror = () => reconnect(attempt + 1);
    ws.onopen  = () => { attempt = 0; }; // reset
  }, delay);
}

Show user-friendly status banners. Always expose a manual “Retry” button — never leave users staring at stale data silently.

13

Security Considerations

  • AuthenticationCookie / JWT / SSO — prefer HttpOnly cookies; never localStorage for tokens
  • AuthorizationRole-based access (Admin, Operator, Viewer) enforced server-side, not just in UI
  • XSS ProtectionSanitize all user input; avoid dangerouslySetInnerHTML; use DOMPurify when HTML is needed
  • CSPContent-Security-Policy headers — restrict script/connect sources
  • No secrets on clientNever expose API keys, internal URLs, or sensitive config in browser bundle
14

Monitoring & Observability

Core Web Vitals

LCPINPCLS

Track

  • JS errors & unhandled promise rejections
  • WebSocket disconnects & reconnect rate
  • API failures & slow queries (p95, p99)
  • User-perceived rendering time per chart
SentryDatadogNew RelicGrafana
15

Scaling to Millions of Events

Frontend Scaling

  • Code splitting — lazy load heavy chart libs
  • Virtualization — react-window for all lists
  • Chart downsampling — LTTB / min-max
  • Memoization — React.memo + useMemo
  • Efficient state updates — batch + transition

Backend / Platform Scaling

  • WebSocket clustering via Redis pub/sub
  • Event aggregation before push
  • Horizontal scaling behind load balancer
  • CDN for all static assets
  • gzip / Brotli compression
  • Database sharding by tenant

Goal

Handle 10× more data without compromising UX — each optimization must be measurable, not just theoretical.

16

Senior Engineer Mindset

Junior Engineer Asks

“How do I build this chart?”

Senior Engineer Asks

“How do I keep this dashboard responsive when it receives 1 million events per minute, serves 100,000 users, and continues to work during partial failures?”

Great dashboards are not built with more components, but with the right architecture, performance optimizations, and resilience patterns applied at every layer.