Roblox TeleportService: Building Multi-Place Games That Carry Player State Between Servers

Has your horror game started to strain under the weight of its own map? Once a liminal experience grows past a handful of rooms, such as a backrooms level, an abandoned hospital wing or an office floor that never ends, a single place file means longer join times, heavier memory budgets, and every player loading assets for areas they may never reach.
That is usually the point where teams split the experience into a lobby place and a set of level places, and it is where TeleportService comes in. This guide walks through how a teleport actually works, how reserved servers keep a party together, and how to carry player state across the jump without trusting the wrong layer.
TeleportService moves players between places, or between servers of the same place, inside a Roblox experience. Use TeleportAsync on the server, reserved servers for private sessions, and DataStores for any state that must survive the move.
When Should You Split A Game Into Multiple Places?
Every Roblox experience can hold several places, and each place runs on its own servers with its own scripts, assets and player cap. Keep in mind that a teleport costs the player a loading screen, so the split has to buy something worth that pause.
Signs that a multi-place structure will pay off include but are not limited to:
- Memory pressure. Your level geometry, audio and lighting presets push lower-end phones toward crashes, and streaming alone no longer keeps the footprint in check.
- Distinct game phases. A social lobby, a matchmaking queue and a tense four-player level have different player caps, different scripts and different performance budgets.
- Private runs. Players expect to enter a level with only their party, which is exactly what reserved servers provide.
- Independent iteration. Your team wants to publish a new chapter without touching the lobby, which is far easier when each chapter lives in its own place.
If two or more of these apply, the split is usually worth it. For the broader picture of how servers, places and instances fit together, our guide to Roblox server architecture covers the layer underneath this one.
How Does A Teleport Actually Move A Player?
A teleport looks instant from the player's side, but several systems hand off to one another in sequence. Understanding that sequence is what lets you put saves, loading screens and retries in the right place.
Here is the order of events for a typical server-initiated teleport:
- The source server calls TeleportAsync. It passes a destination place ID, an array of Player objects, and an optional TeleportOptions instance.
- Roblox finds or creates a destination server. For a public place it matchmakes into an existing server with room; for a reserved server it routes to the instance tied to the access code.
- The client disconnects and loads the destination. A teleport GUI set with SetTeleportGui stays on screen during this gap, which matters for atmosphere in a horror game.
- PlayerRemoving fires on the source server. This is where most games trigger a save, and it is also where the save race described below begins.
- PlayerAdded fires on the destination server. The destination reads join data, loads the player's profile, and spawns the character.
Note that steps four and five can overlap in time. The source server may still be writing the player's data while the destination server is already trying to read it.
Call TeleportAsync from a server Script, not a LocalScript. Server-side teleports can move whole parties at once, attach TeleportOptions, and return a result containing the reserved server's access code and PrivateServerId.
Reserved Servers: Keeping A Party Together
Public servers are matchmade, so four friends who teleport separately into a level place could land in four different servers. Reserved servers solve this by creating a private instance that only players carrying its access code can enter.
There are two ways to get one. You can call ReserveServer ahead of time to get an access code, or you can set ShouldReserveServer on a TeleportOptions object and let TeleportAsync reserve and send in one step.
The one-step version is the cleaner choice for a lobby that sends a ready party into a fresh level:
```lua local TeleportService = game:GetService("TeleportService") local LEVEL_PLACE_ID = 0 -- replace with your level place ID local function sendPartyToLevel(players, chapterId) local options = Instance.new("TeleportOptions") options.ShouldReserveServer = true options:SetTeleportData({ chapterId = chapterId, partySize = #players, }) local ok, result = pcall(function() return TeleportService:TeleportAsync(LEVEL_PLACE_ID, players, options) end) if ok then return result.ReservedServerAccessCode end warn("Party teleport failed:", result) return nil end ```The returned access code is worth keeping. If a player disconnects mid-level and you want to let them rejoin their party, store the code against the party in a MemoryStore sorted map or hash map and send the returning player with ReservedServerAccessCode set on their TeleportOptions.
Be aware that reserved servers shut down once they empty out. The access code stays valid, but reusing it starts a fresh instance, so any state that lived only in server memory, such as which doors were opened or which entity was hunting whom, is gone unless you persisted it.
Detecting a reserved server. A server is reserved when game.PrivateServerId is a non-empty string and game.PrivateServerOwnerId is 0. VIP servers also carry a PrivateServerId, but their owner ID is the purchasing player's user ID, which is how you tell the two apart.
Teleport Data: What It Can And Cannot Carry
TeleportOptions:SetTeleportData lets you attach a small table of serializable values to the teleport. On the destination server, Player:GetJoinData returns a table whose TeleportData field holds that payload, alongside fields such as SourcePlaceId and SourceGameId.
The important caveat is that teleport data is not a secure channel. Roblox documents that it travels through the client during the jump, which means an exploiter can read it and, in principle, alter it before it arrives.
That limits what belongs in the payload. Good candidates for teleport data include:
- Intent. Which chapter or difficulty the party selected in the lobby, validated on arrival against what the player has actually unlocked.
- Cosmetic context. Which loading-screen variant to fade out of, or which lobby the player came from for a return trip.
- Coordination hints. A party identifier the destination can use to look up authoritative party membership in a MemoryStore.
Anything with value, such as currency, inventory, unlocks or progress flags, belongs in DataStores instead. A destination script that reads teleport data should treat every field as a claim to verify, not a fact to apply:
```lua local Players = game:GetService("Players") local VALID_CHAPTERS = { intro = true, ward_b = true, sublevel = true } Players.PlayerAdded:Connect(function(player) local joinData = player:GetJoinData() local hint = joinData.TeleportData local requested = typeof(hint) == "table" and hint.chapterId or "intro" if not VALID_CHAPTERS[requested] then requested = "intro" end -- Load the authoritative profile, then confirm the chapter is unlocked -- before spawning the player there. end) ```The same principle runs through our guide to Roblox anti-exploit patterns. The server owns the truth, and the client only ever makes requests.
Teleport data passes through the client, so exploiters can read or alter it. Use it only for hints such as a selected chapter or party ID, and verify every field against DataStore records on the destination server.
Choosing The Right Layer For Each Piece Of State
Multi-place games usually lean on four storage and messaging layers at once. Each one has a different lifetime and a different trust level, and the bugs tend to show up when state lands in the wrong one.
Here is how the layers compare for a lobby-and-levels structure:
| Layer | Lifetime | Trust level | Best used for |
|---|---|---|---|
| Teleport data | A single teleport | Untrusted, client-routed | Selected chapter, party ID, cosmetic context |
| DataStores | Persistent | Server-authoritative | Progress, inventory, currency, unlocks |
| MemoryStore | Short-lived, with an expiration you set | Server-authoritative | Party rosters, reserved access codes, matchmaking queues |
| MessagingService | In flight only | Server-to-server | Notifying lobbies that a level finished or a party disbanded |
For the persistence layer in depth, see our Roblox DataStores guide. For live coordination between running servers, our walkthrough of cross-server messaging covers MessagingService topics and their delivery guarantees.
The Save Race Between Source And Destination
The most common multi-place bug is a stale read. The source server saves in PlayerRemoving, the destination server loads in PlayerAdded, and if the load wins the race, the player arrives in the level missing whatever they earned in the last few seconds of the lobby.
There are two complementary fixes. First, save explicitly before calling TeleportAsync rather than relying only on PlayerRemoving; second, use a session lock so the destination knows when the source has finished writing.
A session lock is a field inside the player's DataStore record that names the server currently holding the profile. The pattern works like this:
- Claim on load. The destination uses UpdateAsync to set the lock to its own game.JobId, but only if the lock is empty or older than a timeout you choose.
- Wait if claimed. If another server still holds a fresh lock, the destination waits a few seconds and tries again rather than loading a stale copy.
- Release on save. The source server writes its final save and clears the lock in the same UpdateAsync call, so the release and the data arrive together.
The release step is the one most often written incorrectly. Clearing the lock in a separate call from the final save opens a window where the destination can claim a profile that is still missing its last write:
```lua local function saveAndRelease(store, key, profile) store:UpdateAsync(key, function(current) if current and current.lockedBy ~= game.JobId then return nil -- another server owns this profile; do not overwrite it end profile.lockedBy = nil profile.lockedAt = nil return profile end) end ```Remember to call the same release path from game:BindToClose. Reserved servers shut down as soon as the last player teleports out, and a close handler that skips the release can leave a lock in place until its timeout expires.
If your game also syncs progress across devices or platforms, the same locking logic applies there. Our guide to cross-platform save state walks through conflict handling when two sessions touch one profile.
Save explicitly before calling TeleportAsync, then use a DataStore session lock so the destination server waits until the source server has released the profile. That prevents players arriving with stale progress.
Building A Retry Policy For Failed Teleports
Teleports fail more often than most developers expect, particularly during a busy launch weekend. A call can throw outright, or it can succeed initially and then fail asynchronously, in which case TeleportService.TeleportInitFailed fires with a TeleportResult value describing why.
Not every failure deserves a retry. The TeleportResult values sort roughly into these groups:
- Transient. Flooded means too many teleport requests went out in a short window, and Failure covers generic errors; both are worth retrying after a delay.
- Capacity. GameFull means the target server filled up, which for a public place is often solved by retrying without a specific server instance.
- Configuration. Unauthorized and GameNotFound point to a wrong place ID, an unpublished place or disabled third-party teleports, and retrying will never fix them.
- Already in progress. IsTeleporting means a teleport for that player is already underway, so a second request should simply be dropped.
A retry handler that respects those groups looks something like this:
```lua local TeleportService = game:GetService("TeleportService") local MAX_ATTEMPTS = 3 local RETRYABLE = { [Enum.TeleportResult.Flooded] = true, [Enum.TeleportResult.Failure] = true, } local attempts = {} TeleportService.TeleportInitFailed:Connect(function(player, result, message, placeId, options) local count = (attempts[player] or 0) + 1 attempts[player] = count if not RETRYABLE[result] or count >= MAX_ATTEMPTS then warn("Teleport abandoned for", player.Name, result, message) attempts[player] = nil return end task.delay(2 ^ count, function() if player.Parent then pcall(TeleportService.TeleportAsync, TeleportService, placeId, { player }, options) end end) end) ```Keep in mind that the player is still standing in your lobby while all of this happens. Show them a clear message after the final attempt fails, rather than leaving a frozen loading screen, which in a horror game reads as part of the scare until it very much is not.
Loading Screens That Survive The Jump
For atmospheric games, the teleport gap is a design surface rather than dead time. Calling SetTeleportGui on the client before the teleport keeps a ScreenGui visible while the destination loads.
On the destination side, a LocalScript in ReplicatedFirst can call GetArrivingTeleportGui to pick up that same interface and fade it out once the level is ready. That handoff lets the static, flicker or ambient hum from the lobby carry straight into the first room, and it pairs well with the techniques in our guide to Roblox camera systems for staging the first shot of a level.
Call SetTeleportGui on the client before teleporting, then call GetArrivingTeleportGui from a ReplicatedFirst LocalScript in the destination place. The same loading screen stays visible across the jump and fades out on arrival.
Returning Players To The Lobby
The trip back is simpler than the trip out, since the lobby is usually a public place. A plain TeleportAsync call with the lobby's place ID and no reservation lets Roblox matchmake each player into any lobby server with room.
That said, a few details are easy to miss on the return leg:
- Save first, again. The level server should run the same explicit save and lock release before sending players home, so rewards earned in the final room are not lost.
- Keep parties together if you want them together. Sending the whole party in one TeleportAsync call keeps them in the same lobby server, whereas separate calls may scatter them.
- Announce the result. A MessagingService publish from the level server can tell lobby servers that a run finished, which is useful for leaderboards and party UI.
- Clean up MemoryStore entries. Remove the party's reserved access code once the run is over, or set an expiration on it in the first place.
All of these come back to the same idea. Each server should leave the shared layers in a clean, predictable state before its players move on.
How Do You Test Multi-Place Teleports?
TeleportService calls do not work in Studio playtests, so the lobby-to-level flow has to be tested in a published experience. Plan for this early, because it changes how quickly you can iterate on anything that sits on either side of a teleport.
A testing workflow that holds up in practice includes the following steps:
- Publish to a private test experience. Keep a separate copy of the experience with matching places, so teleport bugs never reach your live players.
- Store place IDs in one module. A single configuration ModuleScript that maps place names to IDs per environment prevents the classic bug of a test lobby sending players into the live level place.
- Test with at least two accounts. Party behaviour, reserved-server routing and the save race only show up when more than one player moves at once.
- Log every teleport outcome. Record the TeleportResult, place ID and attempt count so a failure in a live server leaves a trail you can follow.
If your places live in source control, our guide to Rojo and Git workflows shows how to keep a lobby and several level places in one repository. Automated publishing to the test experience can then run through Roblox Open Cloud APIs instead of manual uploads.
Teleports cannot be tested in Studio playtests. Publish to a separate test experience with matching places, keep place IDs in one config module, and test every flow with at least two accounts.
Frequently Asked Questions
These questions come up most often when splitting a game into lobby and level places. Each answer covers a detail the sections above only touched on.
Why do teleports fail when I test them in Roblox Studio?
Studio playtests run without the live server infrastructure TeleportService needs, so teleport calls error locally. Publish the experience and test the lobby-to-level flow in a live server with a second account.
What happens to a reserved server's access code after everyone leaves?
The instance shuts down once it empties, but the access code stays valid. Reusing the code starts a fresh instance, so persist anything you want to survive and treat in-memory state as gone.
Can exploiters tamper with teleport data?
Yes. Teleport data travels through the client, so treat it as an unverified hint and validate every field against DataStore records on arrival.
How can a script tell whether it is running in a reserved server?
Check that game.PrivateServerId is a non-empty string and game.PrivateServerOwnerId equals 0. A VIP server also has a PrivateServerId, but it carries the owner's user ID.
Which teleport failures are worth retrying automatically?
Retry Flooded and generic Failure results with a short backoff. Unauthorized and GameNotFound point to configuration problems that retries will never fix.
Where To Go From Here
Splitting a game across places is less about the single TeleportAsync call and more about everything around it: where state lives, who owns it at each moment, and what happens when a teleport fails halfway through. Once those rules are written down, adding a fourth or fifth level place becomes routine work rather than a fresh source of lost progress.
If you are still building out the foundations, our Roblox scripting learning path puts these systems in order. Pair it with our guide to Roblox replication, and your next chapter will arrive with its players, and their progress, intact.


