"The P2P join handshake has a fixed ~3-second connect budget"
▸ SYMPTOM
A joiner enters a Steam lobby, then gets the literal "Connection timed out." after roughly three seconds — regardless of how long the game code is willing to wait. Adding a longer client-side timeout in game code has no effect.
▸ CAUSE
The P2P join handshake is host-initiated: the host sends ServerInfo when it observes the new lobby member, so the joiner is passive until the host acts. After entering the Steam lobby, the joiner's engine-side budget is only about 3 seconds (30 polls at 100 ms intervals) before it surfaces "Connection timed out."
Other visible constants on the same path: a 5-second lobby-host wait and a 120-second connected-timeout (for the post-connect phase). But the critical window is the initial ~3-second connect budget — any host-side delay in observing the new member or sending ServerInfo (including poisoned pairwise transport state) exhausts it fast.
The key constraint: no game-side code can lengthen this window. The budget is in the engine's connection handshake, not in any game-accessible parameter.
▸ FIX
Since the connect window is fixed and non-extensible:
-
Join recovery must be a fresh retry attempt, never a longer client-side wait. A retry mints a new connection attempt with a fresh budget. Wire the client watchdog as a retry/recover mechanism, not a way to buy the engine more connect time.
-
Minimize host-side handshake latency. Any delay between the host observing the new lobby member and sending
ServerInfoeats directly into the ~3-second budget. Keep the host's member-observation path fast and synchronous. -
Pre-validate before connecting. Run lobby metadata checks (protocol version, build stamp) before
Networking.Connect, not during the handshake — every millisecond of game-side validation during the connect window is wasted budget.
▸ WHY IT WORKS
Each fresh retry resets the ~3-second engine budget from zero. By keeping the host responsive and moving all game-level validation outside the connect window, the full budget is available for the actual transport-level handshake. Combined with fresh-query retry (to sidestep poisoned pairs), this makes join recovery automatic and bounded rather than open-ended.
- Published.