Skip to content

How the game talks to the platform

A Trackmania 2020 server doesn't call the backend directly. When you cross the finish line, the news travels as a message, dropped onto a queue and picked up whenever the backend is ready. That queue is Redis Streams, the one road between the game servers and the platform.

The controller next to each server, kontrol, watches the game. When something happens it drops a message; the backend reads it and writes it down. Neither side talks to the other directly.

Messages have names

Every message has a short name that says what happened: a player connected, a player finished, a server checked in, a map started. They group into families: player messages, server messages, map messages, and a few more for admin actions, leaderboards, and notifications.

Each kind of message has exactly one handler on the backend. A "player finished" message becomes a finish; a server's regular check-in proves it's still alive. (Finishes aren't the same as records. See Records vs Finishes for why.)

Most messages flow one way, game → backend. A few need an answer ("give me this map's records," "give me the leaderboard") and those carry a return address, so the backend can reply to the exact server that asked.

Following a finish

sequenceDiagram
    participant TM as Trackmania server
    participant K as kontrol
    participant R as Redis Streams
    participant API as Backend
    participant C as Website / Discord bot

    TM->>K: you crossed the line
    K->>R: publish a "finished" message
    R->>API: backend picks it up
    API->>API: write the finish to the database
    API-->>C: rankings on the website, live status in the admin panel

Why a queue instead of a phone call

The game servers and the backend live on their own schedules. Servers restart, redeploy, and drop offline; the backend deploys whenever it deploys. A queue lets them ignore each other's downtime: kontrol drops the message and moves on, the backend picks it up when it's back. Messages wait until they're read.

No finish is ever lost

This is the real payoff of doing it with a queue, and a big part of why the whole thing is built this way. Your finish isn't a fragile phone call that's gone if nobody picks up. It's a durable message that sits on the queue until the backend reads it.

  • If the backend is down or mid-deploy when you cross the line, your finish just waits in the queue. When the backend comes back, it works through the backlog and catches up. Nothing is dropped, the system heals itself.
  • If a finish ever does slip through, it's still recoverable. The controllers keep a log of every finish, so an admin can find it and add it back by hand from the admin panel's insert-finish tool.

Either way, the record of your run survives. That's the quiet advantage of an event-based system: every event is a message you can re-read or replay, not a moment that's gone the instant it happens. A direct call would give you one shot; a queue gives you a second one.

The catch: nothing forces the two sides to agree

Here's the part that bites. A message is just text. The backend checks its shape, not its meaning. There's no shared safety net spanning the two codebases: one side writes fields by name, the other reads fields by name, and nothing forces them to match.

A renamed field fails silently

Rename a field on one side and forget the other, and nothing errors. The message still arrives and still looks valid. The value just quietly goes missing. No crash, no log, no red flag. It only turns up later as wrong numbers on the website. So the two sides have to change together, or not at all.

The bigger picture (who owns what, and how the pieces connect) is in the Architecture overview.