Using ClientTravel with Steam Loads Incorrect Level

I’m trying to get a basic multi-player setup working with UE and Steam. I am using UE 5.8 with C++, Online Subsystem, Steam Sockets and Seamless Travel. I’ve started with the third person shooter project, created three maps (LVL_Title, LVL_Transition, LVL_Lobby), enabled seamless travel in the game mode, created my own multiplayer session subsystem, added the maps to the packaged build list, and setup the maps in the project settings. I’m packaging the application when I test, I have 2 machines each logged into seperate steam accounts set to the same download region, and I am creating my own custom session name when I create the session.

I create the session in my subsystem by calling ServerTravel passing the map location to the lobby level and passing the listen parameter. This works because I then load the lobby level, travel to that level and my character can move around. Below is the code I use to setup the server:

void UMultiPlayerSessionsSubsystem::CreateSession(int32 NumPublicConnections, const FString& MatchType)
{
	if (!SessionInterface.IsValid()) return;

	if (auto ExistingSession = SessionInterface->GetNamedSession("Brandon_Test"); ExistingSession != nullptr)
	{
		SessionInterface->DestroySession("Brandon_Test");
	}
	
	// Store the delegate in a Handle so we can remove it later from the delegate list
	CreateSessionCompleteDelegateHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(
		CreateSessionCompleteDelegate);
	
	LastSessionSettings = MakeShareable(new FOnlineSessionSettings());
	LastSessionSettings->bIsLANMatch = Online::GetSubsystem(GetWorld())->GetSubsystemName() == "NULL";
	LastSessionSettings->NumPublicConnections = NumPublicConnections;
	LastSessionSettings->bAllowJoinInProgress = true;
	LastSessionSettings->bAllowJoinViaPresence = true;
	LastSessionSettings->bShouldAdvertise = true;
	LastSessionSettings->bUsesPresence = true;
	LastSessionSettings->bUseLobbiesIfAvailable = true;
	LastSessionSettings->Set(FName("MatchType"), MatchType, EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
	LastSessionSettings->BuildUniqueId = 1;

	if (const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); !SessionInterface->
		CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), "Brandon_Test", *LastSessionSettings))
	{
		SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegateHandle);
		
		// broadcast our own custom delegate
		MultiPlayerOnCreateSessionComplete.Broadcast(false);
	}
}

After this is called the following code runs where ServerTravel is called with the listen parameter:

void UMenu::OnCreateSession(const bool bWasSuccessful)
{
	if (bWasSuccessful)
	{
		if (UWorld* World = GetWorld())
		{
			World->ServerTravel("/Game/Maps/LVL_Lobby?listen");
		}
	}
	else
	{
		if (GEngine)
		{
			GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Red, TEXT("Failed to create session"));
		}
	}
}

After this my first machine is connected to the lobby level and is waiting for a connection from another client. After that I have a couple of functions that call find sessions and join sessions which are both listed below:

void UMultiPlayerSessionsSubsystem::FindSessions(const int32 MaxSearchResults)
{
	if (!SessionInterface.IsValid()) return;
	
	FindSessionsCompleteDelegateHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate);
	
	LastSessionSearch = MakeShareable(new FOnlineSessionSearch());
	LastSessionSearch->MaxSearchResults = MaxSearchResults;
	LastSessionSearch->bIsLanQuery = Online::GetSubsystem(GetWorld())->GetSubsystemName() == "NULL";
	LastSessionSearch->QuerySettings.Set(SEARCH_LOBBIES, true, EOnlineComparisonOp::Equals);

	if (const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); !SessionInterface->
		FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), LastSessionSearch.ToSharedRef()))
	{
		SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegateHandle);
		
		MultiPlayerOnFindSessionsComplete.Broadcast(TArray<FOnlineSessionSearchResult>(), false);
	}
}

Since I’m using a custom name for my session only 1 item is ever found here and it is sent to the following method:

void UMenu::OnFindSessions(const TArray<FOnlineSessionSearchResult>& SessionResults, bool bWasSuccessful)
{
	if (MultiPlayerSessionsSubsystem == nullptr) return;
	
	for (auto Result : SessionResults)
	{
		FString SettingsValue;
		Result.Session.SessionSettings.Get(FName("MatchType"), SettingsValue);
		if (SettingsValue == MatchType)
		{
			MultiPlayerSessionsSubsystem->JoinSession(Result);
			return;
		}
	}
}

Here JoinSession is called on the subsystem which runs this code, the code setting bUseLobbiesIfAvailable and bUsesPresence came from a suggestion I found in this forum as a way to potentially fix the problem I’m having:

void UMultiPlayerSessionsSubsystem::JoinSession(const FOnlineSessionSearchResult& SessionResult)
{
	if (!SessionInterface.IsValid())
	{
		MultiPlayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
		return;
	}
	
	JoinSessionCompleteDelegateHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate);
	
	FOnlineSessionSearchResult& NewSession = const_cast<FOnlineSessionSearchResult&>(SessionResult);
	FOnlineSessionSettings& SessionSettings = NewSession.Session.SessionSettings;
	SessionSettings.bUseLobbiesIfAvailable = true;
	SessionSettings.bUsesPresence = true;

	if (const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); !SessionInterface->
		JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), "Brandon_Test", SessionResult))
	{
		SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegateHandle);
		
		MultiPlayerOnJoinSessionComplete.Broadcast(EOnJoinSessionCompleteResult::UnknownError);
	}
}

After this I finally call ClientTravel with the address retrieved from Steam using the online subsystem:

void UMenu::OnJoinSession(const EOnJoinSessionCompleteResult::Type Result)
{
	if (const IOnlineSubsystem* Subsystem = Online::GetSubsystem(GetWorld()))
	{
		const IOnlineSessionPtr SessionInterface = Subsystem->GetSessionInterface();
		if (SessionInterface.IsValid())
		{
			FString Address;
			SessionInterface->GetResolvedConnectString("Brandon_Test", Address);

			if (APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController())
			{
				PlayerController->ClientTravel(Address, TRAVEL_Relative, true);
				UE_LOG(LogOnlineSession, Log, TEXT("Called client travel to address: %s"), *Address);
			}
		}
	}
}

Initially I thought ClientTravel was throwing an error and just returning to the title map, but if you look through the logs you’ll see that it purposefully loads the title map and never attempts to call the lobby map (I removed the actual ID in the steam address in the logs).

[2026.07.02-13.02.29:706][232]LogOnlineSession: Warning: ******JOINSESSION Brandon_Test SUCCESSFUL******
[2026.07.02-13.02.29:706][232]LogOnlineSession: STEAM: Using P2P Data for Connection Serialization
[2026.07.02-13.02.29:708][232]LogWorld: SeamlessTravel to: /Game/Maps/LVL_Title
[2026.07.02-13.02.29:710][232]LogOnlineSession: Called client travel to address: steam.REDACTED:7777
[2026.07.02-13.02.29:781][235]LogWorld: BeginTearingDown for /Game/Maps/LVL_Title
[2026.07.02-13.02.29:783][235]LogViewport: Display: Viewport MouseLockMode Changed, DoNotLock -> LockOnCapture
[2026.07.02-13.02.29:783][235]LogViewport: Display: Viewport MouseCaptureMode Changed, NoCapture -> CapturePermanently
[2026.07.02-13.02.29:783][235]LogViewport: Display: Player bShowMouseCursor Changed, True -> False
[2026.07.02-13.02.29:793][235]LogWorld: UWorld::CleanupWorld for LVL_Title, bSessionEnded=false, bCleanupResources=true
[2026.07.02-13.02.29:796][235]LogSlate: InvalidateAllWidgets triggered.  All widgets were invalidated
[2026.07.02-13.02.29:802][235]LogAudio: Display: Audio Device (ID: 1) registered with world 'LVL_Transition'.
[2026.07.02-13.02.29:826][235]LogAudio: Display: Audio Device unregistered from world 'LVL_Title'.
[2026.07.02-13.02.29:832][235]LogRenderer: Forcing update for all mesh draw commands: SkyLight real-time capture change
[2026.07.02-13.02.29:847][235]LogUObjectHash: Compacting FUObjectHashTables data took   0.48ms
[2026.07.02-13.02.29:848][235]LogChaosDD: Creating Chaos Debug Draw Scene for world LVL_Transition
[2026.07.02-13.02.29:850][235]LogStreaming: Display: FlushAsyncLoading(81): 0 QueuedPackages, 1 AsyncPackages
[2026.07.02-13.02.29:860][235]LogStats:     SeamlessTravel FlushLevelStreaming  -  0.000 s
[2026.07.02-13.02.29:861][235]LogWorld: Bringing World /Game/Maps/LVL_Transition.LVL_Transition up for play (max tick rate 0) at 2026.07.02-23.02.29
[2026.07.02-13.02.29:861][235]LogWorld: Bringing up level for play took: 0.000685
[2026.07.02-13.02.29:861][235]LogWorld: Sending NotifyLoadedWorld for LP: LocalPlayer_2147482468 PC: BP_ThirdPersonPlayerController_C_2147482391
[2026.07.02-13.02.29:861][235]LogNet: Verbose: NotifyLoadedWorld Begin
[2026.07.02-13.02.29:861][235]LogNet: Verbose: NotifyLoadedWorld End
[2026.07.02-13.02.29:861][235]LogWorld: StartLoadingDestination to: /Game/Maps/LVL_Title
[2026.07.02-13.02.29:862][235]LogAppleController: [HandleMouseConnected] Handling physical mouse connection...
[2026.07.02-13.02.29:862][235]LogAppleController: [HandleMouseConnected] Handling physical mouse connection...
[2026.07.02-13.02.29:862][235]LogAppleController: [HandleMouseConnected] Handling physical mouse connection...
[2026.07.02-13.02.29:917][241]LogWorld: BeginTearingDown for /Game/Maps/LVL_Transition
[2026.07.02-13.02.29:918][241]LogWorld: UWorld::CleanupWorld for LVL_Transition, bSessionEnded=true, bCleanupResources=true
[2026.07.02-13.02.29:918][241]LogSlate: InvalidateAllWidgets triggered.  All widgets were invalidated
[2026.07.02-13.02.29:918][241]LogAudio: Display: Audio Device (ID: 1) registered with world 'LVL_Title'.
[2026.07.02-13.02.29:922][241]LogAudio: Display: Audio Device unregistered from world 'LVL_Transition'.
[2026.07.02-13.02.29:943][241]LogUObjectHash: Compacting FUObjectHashTables data took   0.30ms
[2026.07.02-13.02.29:944][241]LogChaosDD: Creating Chaos Debug Draw Scene for world LVL_Title
[2026.07.02-13.02.29:944][241]LogStreaming: Display: FlushAsyncLoading(83): 1 QueuedPackages, 0 AsyncPackages
[2026.07.02-13.02.29:957][241]LogLoad: Game class is 'BP_ThirdPersonGameMode_C'
[2026.07.02-13.02.29:957][241]LogStats:     SeamlessTravel FlushLevelStreaming  -  0.000 s
[2026.07.02-13.02.29:958][241]LogWorld: Bringing World /Game/Maps/LVL_Title.LVL_Title up for play (max tick rate 0) at 2026.07.02-23.02.29
[2026.07.02-13.02.29:958][241]LogWorld: Bringing up level for play took: 0.001399

As you can see the third line in the log says its attempting seamless travel to LVL_Title and not LVL_Lobby like it should. The only thing I see that makes me wonder if this could be part of the problem is Steam’s return from find sessions; I’ll only put one pair here, but this doesn’t happen for all of them just a large amount of them.

[2026.07.02-13.02.29:424][224]LogOnlineSession: STEAM: Search result 35: LobbyId=Lobby[0x18600002934D228], LobbyId.IsValid()=true, CSteamID(LobbyId).IsLobby()=true
[2026.07.02-13.02.29:424][224]LogOnlineSession: Warning: STEAM: Unknown or unsupported data type from Steam key data 199_cce4cdac3a5fc4b9cb216c8f49a0df6b68014861867eecce0584313ed0f96964_6 lobby_password_ykssr_199
[2026.07.02-13.02.29:424][224]LogOnlineSession: Warning: STEAM: Failed to parse setting from key 199_cce4cdac3a5fc4b9cb216c8f49a0df6b68014861867eecce0584313ed0f96964_6 value lobby_password_ykssr_199
[2026.07.02-13.02.29:424][224]LogOnlineSession: Warning: STEAM: Unknown or unsupported data type from Steam key data lobby_key 16ca67264987f56ca821525a2f65884cedc2546cf15ca3e69292c5c394c9d4ef
[2026.07.02-13.02.29:424][224]LogOnlineSession: Warning: STEAM: Failed to parse setting from key lobby_key value 16ca67264987f56ca821525a2f65884cedc2546cf15ca3e69292c5c394c9d4ef
[2026.07.02-13.02.29:424][224]LogOnlineSession: Warning: STEAM: Unknown or unsupported data type from Steam key data lobby_preferences 00000000000000000000000000010001
[2026.07.02-13.02.29:424][224]LogOnlineSession: Warning: STEAM: Failed to parse setting from key lobby_preferences value 00000000000000000000000000010001
[2026.07.02-13.02.29:424][224]LogOnlineSession: Warning: STEAM: Unknown or unsupported data type from Steam key data lobby_type yknx3_seamless_master_lobby
[2026.07.02-13.02.29:424][224]LogOnlineSession: Warning: STEAM: Failed to parse setting from key lobby_type value yknx3_seamless_master_lobby
[2026.07.02-13.02.29:424][224]LogOnlineSession: Warning: STEAM: Unable to parse search result for lobby 'Lobby[0x18600002934D228]'

I did search online for the last warning message there and that is what has lead me to the place I’m at currently. I know its a lot of information, I’m just hoping that somewhere in there is an obvious mistake I’ve made or something I just haven’t figured out. I would appreciate any advice anyone could give. One last thing…I’m working on a Mac so that might have some bearing on this issue as well. I have dealt with the Mac entitlements issue already, so that shouldn’t be the problem.

Your session code is fine, the problem is this line:

PlayerController->ClientTravel(Address, TRAVEL_Relative, true);

Should be:

PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute);

There are two separate things wrong in there and your log shows both of them.

The travel type. GetResolvedConnectString gives you an absolute connect string, a host. TRAVEL_Relative means “resolve this against the URL I’m already on”, so the engine never treats your steam address as a host, it treats it as a path on the machine you’re currently on. Nothing new resolves and you land back where you started. That’s why it’s LVL_Title specifically and not some random map or an error. It’s literally reloading the map you were standing on.

The seamless flag. The third param is bSeamless and it would have broken you even after fixing the travel type. Seamless travel needs an existing connection to carry across, it’s for hopping between maps on a server you’re already connected to. On a first join there’s no connection yet, so there’s nothing to carry and it falls back to a local travel. That’s your LVL_Title to LVL_Transition back to LVL_Title hop.

Rule I go by: ClientTravel to join someone, never seamless. ServerTravel once everyone’s already connected, seamless. Your transition map and the seamless flag on the GameMode aren’t wasted, they’ll do their job when the lobby starts the match, they just have nothing to do with the initial join. Default for that third param is false anyway so just drop it.

The line in your log that actually proves it

LogLoad: Game class is 'BP_ThirdPersonGameMode_C'

A client that’s connected to a remote server never creates a GameMode, it only exists on the authority. You’ve got one, so you never connected to anything. You did a local map load and stayed single player, and everything after that in the log is just a normal local travel doing its thing.

Worth noticing the ordering too. SeamlessTravel is logged at :708, two ms before your own UE_LOG at :710, which means that seamless travel IS your ClientTravel call and not something else hijacking it. Handy check in general, when a travel happens you didn’t ask for, look at whether it fired before or after your own log line.

Couple of other things while I’m here

DestroySession is async and you’re treating it like it isn’t:

if (auto ExistingSession = SessionInterface->GetNamedSession("Brandon_Test"); ExistingSession != nullptr)
{
    SessionInterface->DestroySession("Brandon_Test");
}

You fall straight through into CreateSession while the old session is still tearing down. It’ll work most of the time and then fail on a second host attempt, or only on a slower machine, which is the worst kind. Bind OnDestroySessionComplete and create from inside the callback.

I’d also swap “Brandon_Test” for NAME_GameSession. Yours is consistent so it mostly works, but parts of the engine assume that name (AGameSession registration and the login path), and you get nothing out of a custom one.

The steam warnings you found, ignore them

The Unable to parse search result for lobby stuff isn’t yours. Keys like lobby_password_ykssr_199 and yknx3_seamless_master_lobby were written by other people’s games. Everyone without their own App ID is on 480, so your search is pulling in the whole world’s test lobbies and the engine complains about settings it didn’t write and can’t type. Harmless. They just happen to land right next to the actual failure, which is why they look relevant.

Two things that do matter out of that though. Keep MaxSearchResults high, a couple hundred rather than 10, because you’re competing with thousands of strangers’ lobbies. And keep the MatchType filter, that’s the only reason you ever find your own session.

Checking it worked

You want Browse and “Game client on port 7777” in the log, but the real tell is that LogLoad: Game class is ... should be gone from the joining client. If that line is still there you’re still travelling locally.

One more thing, log the value of Address before you travel. If GetResolvedConnectString comes back empty, which happens when the session name doesn’t match the one you joined with, ClientTravel with an empty URL quietly reloads the current map and it looks identical to what you have now. Different cause, same symptom, much easier to rule out in one line than to debug twice.

Mac isn’t the problem here by the way, “Using P2P Data for Connection Serialization” means steam’s side is working fine.

Looking at your log, I think the culprit is the third parameter of ClientTravel. You are calling ClientTravel(Address, TRAVEL_Relative, true), where the “true” is bSeamless. The log shows “SeamlessTravel to: LVL_Title” and “StartLoadingDestination to: LVL_Title” right after JoinSession succeeds. In other words, the client does a seamless travel (through your transition map) to the destination it already has, instead of connecting to the host. A client joining a remote server can’t do a seamless travel, because seamless travel only applies once you are already connected to a server.

Try this instead:
PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute);
(the bSeamless argument defaults to false). TRAVEL_Absolute is also the type normally used with a resolved connect string like the Steam one. Keep seamless travel enabled in the GameMode for the server-side ServerTravel from the lobby onward; that part is fine.

Smaller things worth checking: the ‘Unable to parse search result’ warnings come from other lobbies on the shared Spacewar App ID 480 and are harmless, but you may want to filter your results by your own session settings key (as you already do with MatchType) so you never try to join someone else’s lobby. If it still fails after the change, post the log from just after ClientTravel and see whether “Browse:” shows a steam. URL.