# F1ReplayTiming: a past F1 session rebuilt as a live-timing experience

> F1ReplayTiming is a Next.js and FastAPI web app that takes Formula 1 session data, processes it once and serves it back as a replay with a timing tower, GPS car positions, telemetry and race control messages. Live timing is still marked beta, and the replay player was recently rewritten, which is the part to test before you rely on it.

**adn8naiagent/F1ReplayTiming** — Visualisation of real-time track data and telemetry synced to F1 replays.

- Repository: https://github.com/adn8naiagent/F1ReplayTiming
- Stars: 938 · Forks: 59
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/adn8naiagent-f1replaytiming

## A replay that behaves like the live timing screen

F1ReplayTiming is a web app for watching Formula 1 sessions with real timing data, car positions on track and driver telemetry, both live during race weekends and as replays of past sessions. The description of the project says the same thing in one line, and it is a narrower promise than the feature list suggests.

The feature set is what a timing tower does. A leaderboard shows position, gap to leader, interval, last lap time, sector indicators for qualifying and practice, tyre compound and age, tyre history, pit stop count with a live pit timer, grid position changes, a fastest lap indicator, investigation and penalty status, and sub-one-second interval highlighting. Race control messages cover steward decisions, investigations, penalties, track limits and flag changes, shown on a draggable overlay on the track map with optional sound notifications. Track status flags cover green, yellow, Safety Car, Virtual Safety Car and red flag conditions.

The track map places cars from GPS telemetry, updating every half a second with smooth interpolation, with marshal sector flags and toggleable corner numbers. Telemetry covers speed, throttle, brake, gear and DRS for 2025 and earlier seasons, plotted against track distance, with a moveable side panel for three or more driver comparisons. Playback runs from 0.5x to 20x with skip buttons for five seconds, thirty seconds, one minute and five minutes, lap jumping, a progress bar and a red flag countdown.

The user is a fan or an analyst with a specific session in mind, and the sessions supported are races, qualifying, sprint qualifying and practice from 2024 onwards.

## FastF1 fetches, the app reconstructs

The architecture section describes three moving parts. The frontend is Next.js with React and Tailwind CSS, built as static files and served by the backend. The backend is FastAPI in Python, serving the API, WebSocket endpoints and the frontend from a single port. The data source is FastF1, used during data processing only.

That last qualifier is the important architectural fact. FastF1 is not called at request time by the browser; it runs when a session is processed. The README says session data is processed once and stored locally, or in Cloudflare R2 for persistence, and that you can either pre-compute data in bulk ahead of time or let the app process sessions on demand when you select them.

So the flow is: pick a session, the backend fetches the underlying data through FastF1, processes it, writes the result to storage, and the frontend plays back the stored result. On a cold instance the first selection of a session therefore has a wait, and the .env.example is explicit that a session not covered by the background precompute is fetched on demand the first time you click it, with the usual one to three minute wait.

The README states that everything runs as a single container on one port, so the frontend and backend are the same service, with no cross-origin configuration, no CORS and no separate URLs to manage. For a self-hosted project that removes an entire class of deployment friction, and it is the reason the reverse proxy section is as short as it is.

## The static replay player replaced the websocket one

The release notes and the environment file both point at the same recent change. Version v2.2 is titled optimised processing and playback, v2.1.3 covers storage management and bug fixes, and v2.1.2 fixed track map rendering and aligned the Docker port configuration.

The environment variable that captures it is REPLAY_PLAYER. Its default is static, which plays replays in the browser from small compressed files, while websocket uses the previous player, which streams frames from the server. The comment is explicit that you should set it to websocket to go back to the previous player if you have problems with the new one, and that it can also be overridden per tab with a query parameter.

This is a genuinely useful escape hatch and also a warning. The default path is a recently written player, and the fallback is one query parameter or one line in .env away. Anyone deploying this should test the static player on the browsers they care about before assuming it works, because the previous implementation is still there and still supported.

The other side of the same change is the backend. The Dockerfile exposes port 8000, sets PORT and STATIC_DIR as environment variables, and starts the service with uvicorn against main:app. The container always listens on 8000 so that reverse proxies have a stable target, and the compose file comments that PORT sets the published host port only, overriding any PORT in .env that would otherwise move the port the proxies point at.

## Running it with Docker Compose

The self-hosting guide asks for Docker and Docker Compose. You clone the repository, copy the example environment file and start the stack:

```bash
cp .env.example .env
```

The README says the defaults work out of the box and that you should change PORT if you want a different port. Then:

```bash
docker compose up
```

After that you open http://localhost:8000, select any past session and it is processed on demand.

The compose file is short, and the ports mapping explains the split between the published host port and the container port:

```yaml
    ports:
      - "${PORT:-8000}:8000"
    env_file: .env
```

Two named volumes are declared for persistence, f1data mounted at /data and f1cache at /data/fastf1-cache. The second one is the FastF1 cache, which is worth understanding: it is the on-disk copy of what the upstream library has already downloaded, so keeping it means a reprocessed session does not re-fetch everything from the source.

The build itself is a two stage Dockerfile. The frontend is built with node:20-alpine and npm ci, producing static output in /app/out, and the production stage is python:3.11-slim with the system packages numpy and pandas need plus libheif-dev for HEIC image support. The backend requirements are installed from backend/requirements.txt, the frontend output is copied into /app/static, and the data directory is created with the FastF1 cache path inside it.

## Configuration lives entirely in .env

The README is explicit that all configuration is managed through the .env file and that there is no need to edit docker-compose.yml. The variable list in the README is worth reading in full, because it describes the two storage modes and the optional features.

PORT defaults to 8000 and DATA_DIR to /data. STORAGE_MODE defaults to local, where data is stored on disk, while r2 reads pre-computed data from Cloudflare R2, which needs R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY and R2_BUCKET_NAME, the last defaulting to f1timingdata. AUTH_ENABLED defaults to false, with AUTH_PASSPHRASE required when it is turned on. OPENROUTER_API_KEY enables the photo sync feature. AUTO_PRECOMPUTE defaults to race+qual and decides which session types are fetched in the background on race weekends, with off, race, race+qual and all as the documented options. RETENTION_SWEEP_ENABLED defaults to true and stops the instance running the daily retention sweep, which matters when several instances share one bucket and only one should delete old sessions.

Two of those deserve a note. The passphrase authentication exists because the app can be publicly hosted, and the README puts it as optional rather than default, so a public deployment means turning it on yourself. And the precompute window runs every thirty minutes from Friday to Monday and downloads matching sessions as soon as F1 publishes data, which is what turns a live weekend into a fast replay rather than a queue of on-demand processing jobs.

## Reverse proxying, with one caveat about websockets

Because the app serves the frontend, the API and the WebSocket endpoints from a single port, the proxy configuration is short. The README gives three examples: Traefik with a router rule and a letsencrypt certificate resolver, nginx with a location block, and a Cloudflare Tunnel one-liner.

The nginx example shows the part that is easy to get wrong:

```nginx
server {
    server_name f1.example.com;

    location / {
        proxy_pass http://localhost:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
```

The proxy_http_version 1.1 with the Upgrade and Connection headers is what makes the WebSocket connection survive the proxy. Without them the HTTP requests work and the live socket does not, which is the kind of failure that looks like an application bug.

The README's key points list confirms the shape of the deployment: a single service on a single port with no separate backend URL to configure, WebSocket and HTTP on the same origin so TLS termination works without extra work, and no CORS headers needed because the frontend and the API are the same service. There is also a trailing mention of a NEXT_PU value that the cleaned README cuts off, which is the kind of leftover that suggests the public documentation has parts that the repository itself is more precise about.

## Live timing, lap analysis and photo sync are the beta edges

Two features are labelled Beta in the feature list and one is dependent on an external service.

Live timing connects to live F1 sessions during race weekends with real-time data from the F1 SignalR stream, with a broadcast delay slider and automatic detection of post-session replays. The label is on the README, and a live path depends on an upstream stream that the project does not control, so its reliability is a property of that stream as much as of this app.

Lap Analysis is also Beta and is limited to race replays. It compares lap times for up to two drivers with a line chart and lap-by-lap history, with pit stop and safety car periods highlighted.

The photo sync feature is the third edge, and it is the one with a cost attached. Broadcast sync matches the replay to a recording of a session, either by uploading a screenshot of the timing tower using AI vision, or by manually entering gap times. The AI path needs OPENROUTER_API_KEY, so it sends an image to an external model provider.

The alternative for that feature is the manual route, entering gap times by hand. For a single session that is a reasonable amount of typing. For a backlog it is not, and the cost of the automated path is that a screenshot of a timing tower leaves your machine.

## Conclusion

F1ReplayTiming suits a fan or an analyst who wants to study a past Formula 1 session frame by frame, with real timing towers, track maps and driver telemetry, on infrastructure they control. It does not suit anyone who needs broadcast-accurate official timing, since the app rebuilds its own timing view from FastF1 data, and it does not suit a deployment that must stay on the previous player without testing, since the replay player was replaced in v2.2. Before adopting, run it with docker compose up on the default port 8000, pick one session from 2024 onwards and let it process on demand, and set REPLAY_PLAYER to websocket if the static player misbehaves in your browser.

## FAQ

### How do I self-host F1ReplayTiming?

With Docker and Docker Compose: clone the repository, copy the example environment file with cp .env.example .env, then run docker compose up. The README says the defaults work out of the box, and you open http://localhost:8000 and select a past session to be processed on demand.

### Which F1 sessions does F1ReplayTiming support?

The README states support for races, qualifying, sprint qualifying and practice sessions from 2024 onwards. Telemetry covers speed, throttle, brake, gear and DRS for 2025 and earlier.

### What are the storage options in F1ReplayTiming?

STORAGE_MODE defaults to local, storing computed session data on disk in DATA_DIR, which defaults to /data. Setting it to r2 reads pre-computed data from Cloudflare R2 and requires R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY and R2_BUCKET_NAME.

### Can I restrict access to a public F1ReplayTiming instance?

Yes, by setting AUTH_ENABLED=true and providing AUTH_PASSPHRASE. The README lists passphrase authentication as an optional way to restrict access when publicly hosted, and it is disabled by default.

### What happens if the new replay player misbehaves?

The REPLAY_PLAYER variable defaults to static, which plays pre-chunked replay files in the browser. Setting it to websocket returns you to the previous player that streams frames from the server, and the player can also be overridden per tab with a query parameter.

## Sources

- [adn8naiagent/F1ReplayTiming on GitHub](https://github.com/adn8naiagent/F1ReplayTiming)
- [Issues](https://github.com/adn8naiagent/F1ReplayTiming/issues)
- [License: MIT](https://github.com/adn8naiagent/F1ReplayTiming/blob/main/LICENSE)
- [README](https://github.com/adn8naiagent/F1ReplayTiming/blob/main/README.md)
- [Releases](https://github.com/adn8naiagent/F1ReplayTiming/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/adn8naiagent-f1replaytiming
