Fikaride now lets people ride on the same road. Your laptop draws the world, and a server passes around small messages describing the riders nearby. The expensive part is still drawing the people and their bikes.

That matters because multiplayer should leave enough room for the ride itself. A cyclist appearing ahead shouldn't freeze the road while the game prepares their jersey.

A controlled multiplayer test on Coast Loop, with a live rider and a local pacer identified by their name tags.
Coast Loop during the local performance test. Four automated clients supplied real multiplayer connections; these are test riders, not a claim about how many people are using Fikaride. The name tags distinguish a live rider from a local pacer.

There are three kinds of named rider, with different jobs:

Rider What they do Where they exist
Live rider Another person's current ride Shared with nearby people in the same online room
Virtual rider A named character with their own pace, routes and riding schedule Available while the game has a healthy online roster
Pacer A training companion for your ride Local to you, including offline

Your pacers never appear in somebody else's ride. They aren't part of the bot roster, either.

Each riding browser opens one WebSocket connection. Think of it as an open conversation with the server: once connected, small messages can travel in either direction without starting a new request each time.

The first message supplies a public name and appearance. During a visible ride, the browser sends its distance along the route, position across the road, speed and cadence at most five times a second. Those multiplayer messages leave out account identifiers, power, heart rate, weight, FTP and ride history.

The server groups connections into small route rooms. Each room accepts up to 32 connections. If one fills, the client tries another of eight room numbers for that route. People in different rooms don't see each other. This first release has no room picker or friend-invite system, so a large group isn't guaranteed to land together.

Within a room, the server returns at most the four closest riders within 260 metres. It sends their appearance when they enter your nearby set; later messages only need their movement. Your computer doesn't receive a stream of everyone's position across every route.

Five updates a second would look jerky if the game simply jumped a rider between those positions. Instead, the browser keeps two samples and draws the movement between them. This is interpolation. It runs about a quarter of a second behind the received timeline, giving the next sample time to arrive. If an update is late, the browser estimates movement for a short, bounded period. Stale riders eventually stop and disappear.

Your own ride continues independently of those messages. Hiding the tab keeps the ride simulation going, while live position messages slow to at most one per second. Explicitly pausing or ending the ride removes your live presence. Public display names are chosen by riders, and positions are reported by their browsers; this is shared road presence, not a race result validated by a server.

Background riding needed a separate check. Chrome can delay hidden-tab timers to once a minute. An independent code review found that the original connection timeout mistook that quiet period for a broken connection. A simulation reproduced a loop that kept reconnecting without sending a position.

The corrected client waits for a reply only after sending a position, sends its first position immediately after joining, and also publishes after background trainer updates. Those updates use the same one-per-second limit. In the regression test, ten minute-spaced wakeups now send ten positions through one connection. That test simulates delayed timers; it is not a physical-trainer test. Without trainer events, a heavily throttled demo tab can still disappear from other riders' views between wakeups, even though its own ride catches up with elapsed time.

The room itself uses a Cloudflare Durable Object: a small coordinator that holds the room's current connections. There is no server physics loop advancing everybody's bike, and movement messages don't write ride history to a database. The implementation uses Cloudflare's hibernation interface, which can preserve connections while an idle object sleeps. An active room still processes messages and consumes resources; hibernation doesn't make a busy ride free. Cloudflare explains the connection lifecycle here.

The browser also has a drawing limit. At most four online visitors are drawn at once, with a lower limit at Low graphics. They take slots from the existing background-traffic budget, and real people take priority over scheduled characters. Your local pacers are separate from that visitor limit.

Preparing a new visitor turned out to need particular care. The game has to create geometry, fit the chosen body shape, prepare bike detail and compile shaders—the small programs the GPU uses to draw surfaces. Doing all that in one uninterrupted task can cause a hitch even when most frames are fast.

Fikaride prepares visitors one at a time and yields between stages so the ride can draw. Jerseys share shader logic while their colours and patterns remain individual. Shader preparation runs at background task priority where the browser supports it. That priority matters because work resumed after a yield can otherwise keep getting ahead of the next ride frame. Chrome documents how yielded tasks inherit their priority.

Once a visitor leaves the nearby set, their model is released. The renderer retains at most eight visitor models, including ones being prepared. People joining throughout a long ride should not leave an ever-growing collection of invisible bikes behind.

So what does it cost?

A controlled test kept the same view, lighting, four local pacers and graphics settings, then changed only the live-rider conditions. The player stayed at the same place to keep scenery streaming out of the comparison. Each steady sample lasted 20 seconds.

Condition Median application work per frame 95th percentile
Multiplayer off, before 8.0 ms 9.2 ms
Connected, nobody nearby 8.0 ms 9.2 ms
Four live riders nearby 11.0 ms 12.3 ms
Those four riders farther away 7.8 ms 8.8 ms
Multiplayer off again 7.9 ms 9.1 ms
A bar chart showing unchanged median application work for an empty connection, an increase with four nearby riders, and a return to baseline when they move away.
Bars show median application work; the orange marks show the 95th percentile. This measures CPU-side simulation, scene preparation and drawing submission. It does not measure the complete GPU or screen-display time.

The empty connection's cost was too small to distinguish in this sample. Four nearby avatars added about 3 milliseconds per frame, roughly 38% more application work than the initial baseline. That is a meaningful cost. The test sustained about 60 FPS because it had room for that work; a computer already near its limit may need lower graphics settings.

This comparison starts with no ambient traffic, so the four visitors add four models. In an already populated scene, visitors can replace background riders instead. That changes the comparison, but it doesn't make rendering a person free.

The separate 15-second arrival window reached four visible live riders with a worst frame gap of 33.4 milliseconds. No browser long tasks over 50 milliseconds were recorded. A 33-millisecond gap can still be felt; this result means the test avoided the much larger arrival stalls found during development, not that every frame arrived perfectly.

With four nearby riders, the steady messages averaged approximately 0.35 KB/s sent and 1.63 KB/s received: about 2 KB/s combined. Those are application payloads, excluding connection setup, WebSocket and network headers, encryption overhead and the game's initial assets. They're also one measured case, not a worst-case bandwidth guarantee.

The renderer's tracked resources returned from 573 geometries and 84 textures with the visitors present to the original 401 geometries and 76 textures after they left. That is useful evidence for cleanup in this test. These are object counts, not a measurement of GPU memory in bytes or a long-duration leak test.

Virtual riders use a different shortcut. Each has a persistent identity, preferred routes, riding days, a start window and a target duration. A shared schedule and a compact elevation-based travel-time table determine where they should be. They can be resting or riding another route, and they don't move themselves to wherever you happen to be.

The client checks the online roster every 30 seconds. If it hasn't changed, the server returns a small “not modified” response. That avoids repeatedly downloading and rebuilding the same roster. Only nearby scheduled riders become drawable models. These are simulated characters, clearly labelled “virtual,” rather than recorded human rides.

Losing the connection removes roaming virtual riders and background road traffic. There is no saved roster that silently turns them into offline bots. Pacers remain yours and keep working offline; the desktop build doesn't open multiplayer connections or request the hosted roster.

The current tradeoff is deliberate: a few nearby people, a small stream of movement, and bounded preparation when someone arrives. The measurements show where the cost sits. Sending positions is cheap in this test. Putting another detailed rider on the road is where the laptop does more work.

The review also caught two quieter problems: an error logged during ordinary socket disconnections, and an admin editor that could become inaccessible if a saved roster referred to a retired route or appearance option. Regression tests now cover both. The editor preserves the original roster for its owner to repair and review before saving. These checks improve recovery; they do not establish how the service performs with every room full.


Measurement notes: Apple M4 Max, Chrome 155, WebGPU, High graphics, 1280 × 800 at device pixel ratio 1, Chrome's 2× CPU slowdown, 60 FPS render cap. Local game modules and a real local Worker/Durable Object exchanged WebSocket messages. Daylight and cast were fixed; movement was frozen after startup to hold the scene still. Ambient traffic and scheduled virtual riders were disabled to isolate live riding. These results do not measure internet latency, a full 32-rider room, weaker laptop GPUs, battery use or sustained thermal behaviour. CPU throttling does not reproduce those conditions. Released code: df0b01d. Download the measurements or individual frame timings.