Architecture patterns for large-scale synchronized viewing events
Contents
Think of the kind of event where the official site says, “Everyone starts watching at 7:00 PM.” Tens of thousands of people gather on the site, start the same video at roughly the same moment, and the web UI changes in sync with playback.
How do you design that?
How this differs from a watch party
Services like Teleparty or Amazon Watch Party are built around a host. Everyone else follows the host’s playback position.
A large synchronized viewing event is different:
- tens of thousands of viewers
- no host
- all clients align to server time as an absolute reference
- the site runs visual effects tied to playback
That leads to a completely different architecture.
Common failure mode
Trying to do everything on one server is how these projects fail.
[Client] -> [Main server]
|- time sync API
|- effect data
|- comments
`- everything else
If tens of thousands of clients poll every few seconds for time synchronization, the request rate quickly climbs into the thousands or tens of thousands per second.
Recommended architecture
The key move is to split out the time server.
[Client] -> [Time server, e.g. Cloudflare Workers]
-> [CDN for cue sheets and assets]
-> [Main server for dynamic features only]
The role of the time server
It should be a tiny endpoint that does only two things:
- return current server time
- return event status such as running or paused
{
"serverTime": 1704067200000,
"status": "running",
"message": null
}
Edge platforms such as Cloudflare Workers or Vercel Edge Functions are a good fit because they are cheap, globally distributed, and low-latency.
Using status for incident control
If the time server also exposes status, it becomes an emergency broadcast channel.
If something breaks and the event has to pause, returning paused lets every client know immediately. Even if the main server is struggling, the time service can still coordinate state.
Client-side time synchronization
On first visit
- request the time server
- measure RTT
- calculate offset
const start = Date.now();
const res = await fetch(TIME_SERVER_URL);
const data = await res.json();
const rtt = Date.now() - start;
const serverTime = data.serverTime + (rtt / 2);
const offset = serverTime - Date.now();
After that, Date.now() + offset becomes the client’s best estimate of current server time.
Re-sync sparingly
Once the initial offset is known, most progression can be calculated locally.
- re-sync maybe once every five minutes
- do not stop the event if the time server is temporarily unreachable
- combine re-sync with status checks
At that cadence, even tens of thousands of clients only generate a modest request rate.
Cue sheet design
Define presentation timing as JSON and distribute it ahead of time through the CDN.
[
{ "time": 0, "action": "showOpening" },
{ "time": 560, "action": "startSong1" },
{ "time": 1200, "action": "showParticles" },
{ "time": 6800, "action": "discChange" }
]
The client scans the cue sheet against corrected server time and fires the relevant effect locally:
function checkCue(currentTime) {
const elapsed = currentTime - eventStartTime;
for (const cue of timesheet) {
if (cue.time <= elapsed && !cue.fired) {
cue.fired = true;
triggerAnimation(cue.action);
}
}
}
That means almost no runtime load on the origin server.
Practical choices for visual effects
Perfect sync is impossible
You will still see drift on the order of hundreds of milliseconds or even a few seconds because of:
- variable network delay
- differences in playback start timing
- device performance differences
Choose effects that tolerate drift
| Effect type | Tolerance |
|---|---|
| Background color slowly shifts | Good |
| Particles fall | Good |
| Text fades in | Good |
| Fireworks at an exact instant | Poor |
| Explosion plus flash at the same frame | Poor |
Short, sharply timed effects make drift obvious. Effects that feel acceptable even when they start slightly late are much safer.
Scaling summary
| Component | Placement | Why |
|---|---|---|
| Time server | Edge | Tiny, cheap, highly available |
| Cue sheet | CDN | Static and cacheable |
| Effect assets | CDN | Same |
| Comments and live dynamics | Main server | Keep origin focused on what must be dynamic |
The point is to strip as much load as possible away from the main server. If time sync and asset delivery live at the edge, the origin can focus on genuinely dynamic features.
Summary
- Split time sync into a separate service
- Fetch time once, then progress locally
- Distribute cue sheets in advance via CDN
- Design effects that tolerate drift
- Reuse the time server as an emergency state channel
The real shift is moving from “do everything on one server” to “separate responsibilities by function.”