A conference, a dozen tables, one tablet per team and a giant screen at the back of the room. The question starts, the countdown begins and halfway through, someone's phone drops off the wifi. When it comes back, the screen is already on the next phase. What does that team see? If the answer is "whatever it showed before it disconnected", the game is badly built.
That's what I learnt building live games for events: in real time, the hard part isn't pushing messages over WebSockets. It's deciding who's in charge and what happens when a message doesn't arrive.
The server is the referee
The clients in these games (tablets, phones, the big screen) decide nothing. They draw the state they receive and send intentions: "this team answers option B". Everything else lives in Laravel: which phase the game is in, when it started, which answers have come in and how many points each team has.
A game is a small state machine: video, question, reveal, ranking, wait. Time-based transitions are calculated by the server on its own clock, not by a timer in anyone's browser. The step from question to reveal fires when time runs out or when every team has answered, with a short grace period so it doesn't cut off abruptly.
One detail I like: when the video that introduces each question ends, the timer starts exactly at that instant, even if the transition runs a few milliseconds later. I store the theoretical start, not the real one. Nobody gains or loses time because of when a request happened to be processed.
Anti-cheating without fuss
With an authoritative server, most cheating gets cut off almost by itself:
The correct answer never reaches the client while the question is open. The snapshot includes the field, but as
nulluntil the reveal phase.Speed points are calculated from the seconds remaining on the server's clock, not from anything the client claims.
A team can only answer once per question, and that is checked server-side.
$puntos = $correcta
? (int) round(1000 * ($restante / max(1, $partida->duracion_pregunta)))
: 0;The faster you get it right, the more you score. And "faster" is measured by the one party with no interest in lying.

One team picks a power-up against another table while the other answers a question. Both screens depend on the same server state.
Reverb as the fast lane, not the only lane
Laravel Reverb does the broadcasting. Whenever something relevant changes, the server fires an event carrying the whole game state to a per-game channel:
class PartidaActualizada implements ShouldBroadcastNow
{
public function broadcastOn(): Channel
{
return new Channel('partida.'.$this->partidaId);
}
public function broadcastWith(): array
{
return $this->estado;
}
}Notice I don't send "phase changed to ranking". I send the whole snapshot: phase, start time, question, ranking, how many teams have answered. A client that reconnects doesn't have to rebuild anything from a sequence of events it missed: the next message brings it up to date. The events are idempotent almost by design.
Still, a WebSocket can fail. That's why broadcast() in the game service is wrapped in a try/catch that only writes a warning to the log. The state is already saved in the database before it's broadcast. If Reverb goes down, the game doesn't break; the fast lane just stops being fast.
The safety net: a boring poll
The second leg is the least glamorous and the one that has saved me the most. Besides listening to the channel, every client requests GET /state every 2.5 seconds. That endpoint returns exactly the same snapshot that goes out over Reverb, after applying any pending time-based transitions.
const POLL_MS = 2500;
channel.listen('.PartidaActualizada', (data: Estado) => aplicar(data));
const id = setInterval(fetchState, POLL_MS);Both paths end up in the same aplicar function. If the WebSocket message arrives first, great, the screen changes instantly. If it got lost, the client catches up on its own within two and a half seconds. There's no special "recovery" logic: the initial state, the state after a dropout and every normal update are the same code.
On the Echo side I also set short timeouts to detect dead connections and force a reconnect when the state turns unavailable or failed. That's the first line of defence; the poll is the second.
The clock: don't trust the device's
One problem shows up as soon as you have ten different tablets: they don't all agree on the time. If you compute the countdown with Date.now(), every team sees a different timer.
The fix is small. Every snapshot includes server_time. The client stores the difference between that value and its local clock and adds it whenever it needs "the time now". The countdown is computed against the phase start time sent by the server and redrawn every 100 ms. It doesn't matter if a tablet runs fast or slow: they all see the same time.
What I take from it
If you're building anything real-time, my recipe after several editions is this:
One owner of the state, the server, saved before it's broadcast.
Events carrying the full state, not diffs.
A cheap poll as a safety net, running the same code as the event.
The server's clock for anything to do with time.
A broadcast failure must never break the game logic.
It isn't the most sophisticated architecture, and that's exactly why it holds up in a packed room on conference wifi. You can see all of this running on the live games page.
If you have an event in mind and want people to take part for real, tell me about your project and we'll see which format fits.

