One of the common issues when writing an online multiplayer game for Steam in Unreal Engine is the 9999 ping value in search results when browsing for sessions. You can see it just running the ShooterGame example on two different machines with the Steam subsystem enabled and a Steam client opened. When you search for sessions, you will see a 9999 ping value for all the results.
The 9999 value is not an Unreal Engine bug, nor it is a problem in your game code or configuration. It just depends on the Steam backend implementation. Here I’m going to explain everything in detail.
Where does the 9999 value come from
The 9999 value for the
PingInMs property of the class
FOnlineSessionSearchResult is just the
MAX_QUERY_PING value, which is the default value set for this property in the class constructor. You are reading this value in your search results just because Unreal Engine is not setting the value at all when parsing the session search results. To understand why, we need to look first at the Steamworks APIs for lobbies and game servers; than we will see how the Steam Online Subsystem is mapped on those APIs under the hood.
STEAM Matchmaking, Lobbies and Game Servers
The Steamworks API offers a set of interfaces for online games:
- Matchmaking & Lobbies – is the interface for creating lobbies and search for suitable games
- Game Server – is the interface for creating Game servers (more details below).
There isn’t any detailed documentation on the Steamworks API. The only documentation available can be found on the Steamworks website (you must be logged in to read the full documentation and download the SDK). The best documentation source is in the API interfaces source code itself: the Steam SDK and the Steamworks example distributed with the SDK.
In the Steamworks Space War example (an asteroid-like multiplayer game), you can create and search for lobbies and game servers.
In the Steam context, lobbies are server objects which have an owner and a list of user members. Users can join and leave lobbies, the owner can change and the lobby persists until the last user leaves it. Users in a lobby can communicate via multicast chat messages.
Lobbies can be searched via the Steam Matchmaking interface using the
When you parse the results from this search, you can retrieve lobby data such as Lobby owner and Lobby name. Unfortunately, no lobby ping information is available through the Steamwork API.
Steam Game Servers
If you create a Game Server in the Space War example, you will get directly in game, allowing for other users to find you and join the game. A game server is also created when users go from the lobby to the game.
If you run the Space War example on two machines with an open Steam client, you can make some experiments with Game servers:
- If the two machines are on the same LAN and you create a game server on the first machine, you can find the server from the second machine using the Find LAN Servers menu command (
SteamMatchmakingServers()->RequestLANServerListis called). In the search results you can see the value of the server ping.
- If you try to find the same game server using the Find Internet Servers menu command (
SteamMatchmakingServers()->RequestInternetServerListis called), you won’t find any result. This because the Server search works in two steps: first a server list is retrieved from the Steam server backend, then each Game server is queried directly through its QueryPort (set by the
SteamGameServer_Initfunction). If your game server is not binded to a public IP address, the query fails and you won’t find it.
Unreal Engine Steam Online Subsystem
The online subsystem for Steam in Unreal Engine maps the abstract Online Subsystem interfaces into the Lobby or the Game Server interfaces depending on the
bIsPresence session flag specified in the
FOnlineSessionSettings object passed to the CreateSession method of the
The Lobby-based implementation of the Steam Online Subsystem is defined in the
OnlineSessionAsyncLobbySteam.h / OnlineSessionAsyncLobbySteam.cpp Unreal Engine source files.
The GameServer-based implementation of the Steam Online Subsystem is defined int the
OnlineSessionAsyncServerSteam.h / OnlineSessionAsyncServerSteam.cpp Unreal Engine source files.
If you create a session with presence, which is the kind of session that the Shooter Game example is creating, then you will be able to find other sessions even if they have been created from machines with private IP addresses on LANs. But if you change the
bIsPresence flag to false when creating the session, you won’t find anything unless you are running the server on a public address.
So, unless you are writing a dedicated server for your game, you will need to stick to a session with presence.
Let’s have a look at the source code
Let’s see how Unreal Engine populates the session search results in both modes: Lobby and Server:
- Lobby – In Lobby mode (
bIsPresence = true) the session result data is populated in the
OnlineSessionAsyncLobbySteam.cppsource code file, inside the
ParseSearchResultmethod. Here only the Session property of the
FOnlineSessionSearchResultobject is written using the
PingInMsproperty is not written and remains at its 9999 default value.
void FOnlineAsyncTaskSteamFindLobby::ParseSearchResult(FUniqueNetIdSteam& InLobbyId)
FOnlineSessionSearchResult* NewSearchResult = new (SearchSettings->SearchResults) FOnlineSessionSearchResult();
if (!FillSessionFromLobbyData(InLobbyId, NewSearchResult->Session))
// Remove the failed element
SearchSettings->SearchResults.RemoveAtSwap(SearchSettings->SearchResults.Num() - 1);
- Game Server – in Server mode (
bIsPresence = false) the search result data (
FOnlineSessionSearchResult) is populated in the
OnlineSessionAsyncServerSteam.cppsource code file, inside the
ParseSearchResultmethod. Here you can see that the
PingInMsproperty of the
FOnlineSessionSearchResultobject is written based on the
gameserveritem_tdata obtained by the Steamworks SDK.
// Fill search result members
FOnlineSessionSearchResult* NewSearchResult = &NewPendingSearch->PendingSearchResult;
NewSearchResult->PingInMs = FMath::Clamp(ServerDetails->m_nPing, 0, MAX_QUERY_PING);
Adding ping values to presence session searches
We have seen that the missing values for pings in session search results are not an Unreal Engine issue but a feature derived from the Steamworks API.
There are two things you can do to deal with this problem:
- Don’t care about pings. Steamworks matchmaking documentation says that “The results are returned ordered by geographical distance”. Although this definition is based on a geographical distance, we can suppose that this distance can be tied to the roundtrip time (which is the ping) values. I don’t know exactly what’s going on under the hood of the Steam Matchmaking backend, but it is reasonable to think that a matchmaking algorithm must take into account low roundtrip times when ordering match results. So it could be fine to forget about pings in your results and thrust the matchmaking ordering.
- Implement ping queries inside the Steam Online Subsystem. This second solution is for people with a good understanding of the Unreal Engine internal module structure and extension capabilities. You can modify the Steam Online Subsystem module, or create another module based on the original one if you don’t want to modify the engine. The new code added should allow the game to query each lobby owner in the search results with a ping packet (containing a timestamp) and receive back the same packet in response. On the send (client) side you need to create an UDP server socket to receive the ping acknowledge packet, then send a ping packet to the Steam address of the lobby owner. On the lobby owner (server) side you need to receive and recognize the ping packet and send it back to the sender. Unreal Engine sockets are implemented on the Steam Subsystem using the Steam P2P interface, which is connectionless. The first packet will take very long (a second) to reach the destination, but after the first packet, the communication will be very fast. You have to send a burst of packets to the lobby owner and take the minimum ping after receiving the ping acknowledges packets.
Some code examples
In your GameSession class, you may have an handler to call after a session search completes. In this handler you can parse each session result and send some ping packet to each session’s owner, see below.
void MyGameSession::OnFindSessionsComplete(bool bWasSuccessful)
IOnlineSubsystem* const OnlineSub = IOnlineSubsystem::Get();
IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
// SearchSettings is the FOnlineSessionSearch object passed to the FindSessions method of the IOnlineSession interface
UE_LOG(LogOnlineGame, Verbose, TEXT("Num Search Results: %d"), SearchSettings->SearchResults.Num());
ISocketSubsystem* SS = ISocketSubsystem::Get();
for (int32 SearchIdx = 0; SearchIdx < SearchSettings->SearchResults.Num(); SearchIdx++)
const FOnlineSessionSearchResult& SearchResult = SearchSettings->SearchResults[SearchIdx];
Sessions->GetResolvedConnectString(SearchResult, GamePort, ConnectInfo);
TRACE("Connection String: %s", *ConnectInfo);
TSharedRef<FInternetAddr> Addr = SS->CreateInternetAddr();
// Creating client socket
FSocket* Socket = SS->CreateSocket(FName("SteamClientSocket"), FString("PingSocket"), true);
FString msg = FString::Printf(TEXT("Ping %f", FPlatformTime::Seconds());
Writer << msg; // Creating packet data
// Sending 10 ping packets
for(int32 i=0; i<10; i++)
Socket->SendTo(Writer.GetData(), Writer.Num(), BytesSent, Addr.Get());
These ping packets will be received by the
StatelessConnectHandlerComponent of the
USteamNetDriver which is inited in the
UNetDriver::InitConnectionlessHandler of the
UNetDriver base class.
// Add handling for the stateless connect handshake, for connectionless packets, as the outermost layer
TSharedPtr<HandlerComponent> NewComponent =
When the packet arrives, it will be processed by the
UIpNetDriver::TickDispatch. Since a connection is not established yet, this packet will be passed to the
wich will eventually call the
StatelessConnectHandlerComponent::IncomingConnectionless. This method will handle the initial connection handshake during the creation of a new connection. Here we have to manage our special Ping packets by sending them back to the client.