24 July 2026
A night chasing a Steam connection bug, and the one-line cause
Friends couldn't join each other's games for weeks. We spent a long night fixing real networking bugs, none of which were the cause. The actual problem was a single line left behind by an earlier fix for a bug we never had.
For a few weeks, hosting a session and having a friend actually join it had been a coin flip. Sometimes it timed out. Sometimes it looked like it was about to connect and then died anyway. We finally sat down and ran it to ground in one long night, and the ending is the sort that makes you laugh and reach for your keyboard to throw it at the same time.
This is a game post rather than an infrastructure one, so it’s a change of pace from the Horde posts. If you write Steam multiplayer in Unreal, though, some of the traps here will be familiar.
Symptoms
If you’re here from a search, this is what it looks like. Two players, both on Steam, both patched, both with Steam running. One hosts, the other tries to join, by invite or through the session browser. Every attempt ends the same way, about ten seconds in:
Timed out attempting to connect
The lobby or session search reports a sentinel ping of 9999ms. If you’re also falling back to the wrong net driver, you may see socket noise like this earlier in the log:
Cannot get information on an invalid socket handle
SO_BROADCAST failed
But the log line that actually pins this bug is one that should be there and isn’t. When the engine goes to create a net driver for hosting, it prints this unconditionally, before anything Steam-specific happens:
LogNet: InitBase GameNetDriver ...
If your “Host” action logs its travel request and loads the level, but that InitBase GameNetDriver line never appears, the engine isn’t failing to create a net driver. It’s never trying to. That absence is the fingerprint.
30-second check: grep your host’s log for InitBase. If it’s missing, skip straight to The fix below, everything else on this page is a longer list of things that turned out not to be it. If InitBase is there and connections still fail, this specific bug isn’t yours; the rest of this post won’t help and you’re more likely looking at NAT/relay routing, a missing PacketHandlerComponents entry (see below), or an actual Steamworks App ID mismatch.
The fix
If the game mode you travel to when a player starts hosting sets bUseSeamlessTravel = true, remove it:
// On your host / frontend game mode:
// bUseSeamlessTravel = true; <- delete this
Seamless travel is for swapping the map inside an already-connected session. It never opens a fresh listen socket, so starting a host through it means the game never begins listening at all, silently and with no error to catch. Only two things in the whole engine call UWorld::Listen(): the plain-travel path in UEngine::LoadMap, and UGameInstance::EnableListenServer (for promoting an already-running standalone game to a listen server without any travel at all). If your host flow doesn’t go through one of those two, it isn’t going to open a socket.
Keep seamless travel on your in-session and lobby game modes, where a real session and real connections already exist to preserve. Those were never the problem.
That’s the whole fix. The rest of this is how a night of chasing everything else led there, if you want it.
A long night of real fixes
Here’s the uncomfortable part. We found and fixed a genuine pile of networking bugs that night, and not one of them was the cause.
The very first thing we tried was dropping a steam_appid.txt next to the executable, on the strength of an early log read that looked like the Steam OSS was failing to initialize at boot, the classic symptom of launching a Steam game outside of Steam. Turned out we’d read the wrong log file; the OSS was initializing fine in the run that actually mattered. Harmless, but it cost us twenty minutes and taught us nothing. Left it in place anyway, since it’s a no-op when Steam’s already running the game.
Ten seconds is a suspicious number to start from, because it’s almost exactly Steam’s default initial connection timeout. That sent us looking at relay routing and handshake timing for a good while, on the theory that the connection was being attempted and just not landing in time. We even got as far as considering patching the SteamSockets plugin to raise k_ESteamNetworkingConfig_TimeoutInitial past its ~10-second default, before talking ourselves out of it: a longer timeout doesn’t help if no traffic is arriving in the first place, it just delays the same failure. Never implemented, and a good thing too, given what the actual cause turned out to be.
The one that looked most promising, and that I’d have sworn was the fix, turned out to be a red herring on closer inspection. Our NetDriverDefinitions had a driver class path missing its leading /Script/:
DriverClassName="SteamSockets.SteamSocketsNetDriver"
The theory was that the bare form fails to resolve and Unreal silently falls back to plain IpNetDriver, a net driver that has no idea what a Steam ID is. And right after we added the prefix, the log signature did change, from invalid-socket and SO_BROADCAST errors to an actual SteamSocketsNetDriver being constructed. Case closed, we thought.
Except it wasn’t, and this is the bit worth slowing down on. When we later traced the resolver properly, the bare Module.Class form should have worked all along. StaticLoadClass goes through ResolveName2 in UObjectGlobals.cpp, which has a short-package-name lookup (FPackageName::FindScriptPackageName, backed by a map that RegisterShortPackageNamesForUObjectModules fills at boot for every registered module) built specifically to resolve strings like SteamSockets.SteamSocketsNetDriver without the prefix. So the missing /Script/ almost certainly wasn’t the blocker. The log change more likely came from one of the config restores below landing in the same pass, and we credited the wrong line for it. We left the explicit prefix in because it’s the unambiguous form either way, but I’m no longer willing to call it a fix.
While we were in there we also chased a module-load-order theory: USteamSocketsNetDriver::IsAvailable() gates on FSteamSocketsModule’s bEnabled flag, which latches once at module startup based on whether the Steam OSS happened to already be alive at that exact moment. A genuinely real race condition, mechanically speaking, and worth knowing about if you’re chasing something similar. Just not what we had, the module was enabled fine, every time, in every log we checked.
Then we kept finding more, mostly config that had been quietly deleted during earlier experimentation and never restored:
- A missing
[PacketHandlerComponents]entry with the Steam auth component, without which two separate Steam accounts can’t complete the low-level handshake at all. Same-machine testing hides this, because it’s the same account both ends. - The handshake version console variables, gone, so the two peers had nothing to agree on.
bAllowP2PPacketRelaydropped, so traffic couldn’t fall back to Steam’s relay when a direct connection wasn’t possible.- A
DriverClassNameFallbackpointing at a class that doesn’t exist anywhere in the engine. We traced that one to a transposed name in Epic’s own Steam Sockets documentation, copied in faithfully. - Smart quotes and a
/Scripts/typo (extra s) in another ini value, clearly pasted from somewhere that auto-formats text.
One config change nearly got second-guessed back in. We’d deleted an entire [SocketSubsystemSteamIP] block, on the assessment that it belonged to an unused legacy raw-IP P2P subsystem WH doesn’t touch, the actual system in play here is SteamSockets/SDR, a different thing despite the similar name. Later a deprecation warning turned up: bAllowP2PPacketRelay has moved from OnlineSubsystemSteam to SocketSubsystemSteamIP, which briefly looked like proof we’d deleted something load-bearing. Traced the warning to its source (FSocketSubsystemSteam::Init()) and confirmed it fires unconditionally from that legacy plugin regardless of whether anything uses it, a false alarm on the re-check, and the original removal was correct.
We also chased things that turned out to be nothing: an App ID we thought might be a leftover from a sibling project, a VPN theory to explain why two players in the UK were being routed through a relay cluster in Dubai, and a Steamworks partner-site setting that it turns out plain client-to-client P2P doesn’t need.
The one that stung most was the login check. The game waited for Steam login to succeed before letting you host, which is sensible, except the privilege check it waited on, GetUserPrivilege for Steam, is a no-op stub that reports success immediately and verifies nothing. So the gate we thought was protecting us did nothing. We wrote an actual engine-plugin patch to poll Steam’s real login state instead, on the theory that hosting was racing ahead of login.
That patch is where the night turned, but not because it worked. It didn’t. What mattered was that even thirty seconds into hosting, with login long since settled, the listen socket still never opened. That killed the entire “it just needs a moment to settle” idea, and forced us to stop inferring from symptoms and go look at what the engine was actually doing.
What the engine was actually doing
With the timing theory dead, we went looking for direct evidence at the net-driver level. There’s a log line, LogNet: InitBase GameNetDriver, that the engine prints unconditionally the instant it tries to create any net driver for hosting. It fires before anything Steam-specific, before anything that could go wrong with sockets or relays.
It never appeared. Not once. The “Host” travel request logged and processed, the level loaded, and the engine never even attempted to create a net driver.
That’s a very different bug from the one we’d spent the night on. Every fix so far had been aimed at making the net driver work. The net driver was never being created.
The actual cause
Buried in a frontend game mode constructor, from an earlier debugging pass on this same symptom months ago:
// A plain (non-seamless) travel tears down this world's net driver before the new one
// is created, and SteamSockets doesn't purge its stale per-world socket state on that
// teardown (Epic UE-239936), so the new listen attempt sees leftover state and falls
// back to plain IpNetDriver. Seamless travel keeps the net driver alive instead.
bUseSeamlessTravel = true;
I’d written that. Months earlier I’d seen hosting fail with a symptom that looked like a silent fallback to IpNetDriver, pinned it on UE-239936, and reached for a reasonable-sounding workaround: use seamless travel so the net driver never gets torn down and recreated. As far as I can now tell that diagnosis was wrong, and the earlier symptom was some other config problem long since fixed, not the engine issue the comment names. But the exact cause of a months-old failure is beside the point. What matters is that the workaround was aimed at a bug I didn’t have.
The trouble is that seamless travel doesn’t fix that. It sidesteps it by never opening a listen socket at all. UWorld::SeamlessTravel exists to swap the map underneath an already-connected session while keeping existing players attached. It has no code path that bootstraps a fresh listen server from a standalone, not-yet-networked game. We checked every call site: UWorld::Listen() is called in exactly two places in the whole engine: the plain-travel path in UEngine::LoadMap, and UGameInstance::EnableListenServer, which promotes an already-running standalone game to a listen server directly, no travel involved. Nowhere in the seamless-travel machinery does either appear. If your project calls EnableListenServer instead of traveling to start a host, the same silent-no-listen symptom can happen for an unrelated reason, worth ruling out separately.
So every single time a player hit “Host” from the main menu, a state with no existing session for seamless travel to preserve, the game quietly never opened a listening socket. Not a wrong one. None. No error either, because the code path that would have tried and failed was never reached. The workaround for the original bug had turned into a wall that made the game look broken in exactly the same way, for a completely different reason, indefinitely.
Deleting that one line put hosting back on a plain travel, the only path that calls Listen(). Friend joined instantly.
What I took from it
Most of the fixes from earlier in the night weren’t wasted. The config restores were real, and cross-account hosting wouldn’t have worked with any of them still broken. The /Script/ prefix, on the honest retelling, fixed nothing and we only thought it did. But none of them could ever have appeared to work whilst the net driver wasn’t being created in the first place, which is why the night felt like bailing a boat that had no bottom.
The thing I’ll actually carry forward is simpler. If a workaround for a scary-sounding engine bug doesn’t make the symptom go away, don’t file it under “mostly working, needs more tuning”. Go and find out whether it does anything at all. We spent hours re-litigating relay routing and login timing because a short comment, written in good faith months ago, told us confidently where not to look. The comment wasn’t lying. It was just wrong about which bug it was describing, and that was enough.
If you’re setting up SteamSockets in Unreal and hosting silently does nothing, check that whatever handles your “Host” transition actually reaches a plain travel. And if you’ve hit UE-239936 for real, I’d genuinely like to compare notes, because we went looking for it and it wasn’t what we had.
FAQ
Does UWorld::SeamlessTravel call Listen() in Unreal Engine?
No. Seamless travel swaps the map underneath an already-connected session and explicitly keeps the existing net driver alive across the transition, that’s the entire point of it. It has no code path that opens a new listen socket. If a standalone, not-yet-networked game travels through SeamlessTravel expecting to become a listen server, it never will, and it won’t log an error either, because the code that would try and fail is never reached.
Why does my Steam session search show a ping of 9999ms?
That’s Steam’s sentinel value for “no response,” not a real (if bad) measurement. It means nothing came back at all, consistent with the host never having opened a listen socket in the first place, rather than a slow or lossy connection.
Hosting works with two instances on the same PC but not with a real friend over Steam. Why?
Same-machine testing (two editor instances, or an editor instance plus a packaged build) usually shares state that two separate Steam accounts don’t. Most commonly that’s a missing [PacketHandlerComponents] entry for the Steam auth component, which two distinct accounts need to complete the low-level connection handshake. If same-PC hosting works but cross-account doesn’t, check that config block before anything else.
Does DriverClassName="SteamSockets.SteamSocketsNetDriver" need a leading /Script/ to resolve?
No, or at least not for the reason it’s easy to assume. StaticLoadClass resolves through ResolveName2 in UObjectGlobals.cpp, which has a short-package-name lookup (FPackageName::FindScriptPackageName) built specifically to turn bare Module.Class strings into their long form at boot. A bare SteamSockets.SteamSocketsNetDriver should resolve on its own. If yours doesn’t, don’t stop at adding the prefix as the fix, it may paper over a different problem, the same way it did for us.
Is this the same thing as Epic’s UE-239936 (stale SteamSockets state on non-seamless travel)?
Not necessarily, and worth checking before you assume it is. UE-239936 is about stale per-world socket state surviving a net driver teardown on a plain travel. What’s described on this page is a listen socket never being opened at all, because a seamless travel was used to start a fresh host. The fingerprint (LogNet: InitBase GameNetDriver present or absent) tells you which one you’re actually looking at.
What should a “Host” button actually do in Unreal with Steam Sockets / OnlineSubsystemSteam?
Travel to your hosting map with a plain (non-seamless) ServerTravel or map load, the bUseSeamlessTravel flag on that game mode should be false or unset. Save seamless travel for transitions that happen after a session already exists (lobby ↔ in-session), where there’s an existing net driver and existing connections actually worth preserving.
Do I need a steam_appid.txt file for Steam P2P / SteamSockets to work?
Only if you’re launching the packaged game directly, outside of Steam, during development, it’s how the Steamworks SDK figures out which app you are without Steam having launched you itself. If Steam launched the process (which is the normal case once you’re actually testing hosting/joining), it’s a no-op either way, so it’s very unlikely to be your actual blocker even though it’s an easy first guess when Steam-related logs look wrong.
What does “bAllowP2PPacketRelay has moved from OnlineSubsystemSteam to SocketSubsystemSteamIP” mean?
That warning comes from FSocketSubsystemSteam::Init(), part of a separate, legacy raw-IP Steam P2P subsystem (SocketSubsystemSteamIP) that’s distinct from the SteamSockets/SDR system most current Unreal projects actually use. It fires unconditionally from that plugin regardless of whether your project references it at all. If you’re on SteamSockets, it’s almost certainly not telling you anything you need to act on.
USteamSocketsNetDriver::IsAvailable() returns false, what causes that?
It gates on FSteamSocketsModule’s internal bEnabled flag, which is set once at module startup based on whether the Steam OSS was already initialized at that exact moment, a real module-load-order race in principle. Worth checking your module’s own boot-order logs if you hit this, but confirm it’s actually happening before treating it as your cause; it’s an easy theory to reach for that doesn’t always pan out.