Like many people, in 2020 I cycled through lots of new hobbies to keep myself busy, the one that stuck the most though was simracing. The sim of choice was iRacing, which exposes a rich telemetry feed, every car's position, lap times, fuel load, tire wear, session flags. But none of that data is visible to viewers watching a stream. So I wanted to make something that could help your stream chat feel like your actual pit crew.
The Pit Wall, named after the Formula One pitwall is a two-part system: a desktop app that reads live data out of iRacing and broadcasts it to a server, and a web app that receives that data and serves it to viewers as live standings dashboards and OBS overlays. The whole pipeline runs on a 1Hz clock for close updates quicker that stream delays.
This writeup focuses on how the data moves, from the game's memory, through the Electron client, across a WebSocket, and into a React UI updating in real time.

iRacing exposes session data through a shared memory interface that the iracing-sdk-js library wraps using two Node.js events.
SessionInfo fires once when a session starts (or when the driver roster changes). It's a YAML blob containing everything static: driver names, car numbers, car classes, track name, weather conditions, qualifying results.
Telemetry fires continuously at iRacing's update rate. Each frame contains the live state of every car on track:
NOTE: iRacing outputs telemetry must faster than the Pitwall updates, this was a tradeoff to minimize costs since this was a free to use tool.
The Electron app subscribes to both events and keeps an in-memory model of the full session state, merging the slow SessionInfo data with the fast Telemetry stream.
The Electron main process does three jobs: read from iRacing, maintain state, and broadcast to the server.
On each Telemetry tick the app updates its in-memory driver array. One non-obvious piece is fuel tracking: iRacing reports current fuel level but not per-lap consumption. The app tracks this itself by recording the fuel level at the start of each lap and computing the delta when the lap completes, building a rolling history that's used to project how many laps of fuel remain.
// rough shape of the per-driver state the app maintains
{
CarIdx: 3,
UserName: "Gabe Krahulik",
LapCompleted: 14,
FuelLevel: 2.34,
laps: [
{ lap: 13, fuelUsed: 0.41 },
{ lap: 14, fuelUsed: 0.39 }
]
}The main process sends two IPC messages to the renderer (the visible Electron window):
session_update - on SessionInfo changes, carries driver roster and track infotelemetry_update - on every telemetry tick, carries live car statesThe renderer uses these to show the driver their own session at a glance.
Once per second, the main process emits a standings event to pitwall.gabirmotors.com over a persistent Socket.io connection:
socket.emit("standings", JSON.stringify({
sessionInfo, // track, weather, flags, session type
sessionRacers, // all drivers with live telemetry
driverData, // the streamer's own fuel/tire/lap detail
options: {
channel, // Twitch channel name used as the room key
fuelIsPublic, // whether viewers can see the streamer's fuel data
password, // optional channel password, used to protect some pages
}
}));The channel name is the room key, every broadcast is associated with a specific Twitch channel, and viewers subscribe to that channel's feed while also getting a chat widget on the dashboard.

The server is a Next.js app with a Socket.io instance attached to the HTTP server via pages/api/socket.ts. There's no database, all state is kept in a plain in-memory array of active channels, this works since the tool is only required to work live, so no persistence is needed.
When a standings event arrives the server:
fastest_lap-{channel} event if one occurredViewer pages (both the full dashboard and the OBS overlays) follow the same connection pattern. On mount, the page gets /api/socket to ensure the Socket.io server is running, then subscribes to the channel's event stream:
useEffect(() => {
fetch("/api/socket").then(() => {
socket = io();
socket.on(`standings_update-${channel}`, (data: string) => {
const parsed = JSON.parse(data);
setDrivers(parsed.drivers);
setSession(parsed.session);
});
});
}, []);The second half of the viewer app is a set of purpose-built overlay pages designed to be dropped directly into OBS as browser sources.

Each overlay connects to the same Socket.io feed but renders only one piece of data:
| Overlay | What it shows |
|---|---|
standings/[channel] |
Full grid, position, name, gap, lap count |
relative/[channel] |
Cars within 3 relative positions of the streamer |
fuel/[channel] |
Streamer's fuel remaining and burn rate |
track/[channel] |
Track map with live car positions |
fastest-lap/[channel] |
Animated banner when a fastest lap is set |
shiftlights/[channel] |
RPM bar for visual shift point indication |
The whole stack is stateless by design. The Electron app is the source of truth, if anything downstream drops it reconnects and the 1Hz broadcast catches everything back up within a second.