Claude Skill

roblox-teleport

Move Roblox players between places, public servers, or reserved servers. Use for TeleportAsync, matchmaking, loading screens, teleport failures, and persistent-data handoff.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download nonlooped-roblox-suite-roblox-teleport-2904c16.zip · 13 KB
Part of nonlooped/roblox-suite — 15 skills

Install

skills CLI npx skills add https://github.com/nonlooped/roblox-suite/tree/main/roblox-teleport
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install nonlooped-roblox-suite@llmmart
Git git clone https://github.com/nonlooped/roblox-suite.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole nonlooped/roblox-suite collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

roblox-teleport

Official sources:

TeleportService moves players between places and servers. The modern entry point is TeleportAsync (server-only); the older Teleport (client), TeleportPartyAsync, TeleportToPlaceInstance, TeleportToPrivateServer, and TeleportToSpawnByName methods are deprecated. Use TeleportAsync() instead. For client-initiated teleports, fire a RemoteEvent to the server and let the server call TeleportAsync(); do not call Teleport() from the client for new work, even if the place is configured as "Fully Open." See the migrate to secure teleports guide.

Cross-reference:

Multi-place architecture

A universe (experience) can contain multiple places. Each place has its own PlaceId; the universe has a UniverseId (exposed as game.GameId). The primary place is the one players join from the Roblox app's main entry point.

  • Create additional places in the Creator Dashboard under your experience → Places.
  • Each place must be published and (for it to be joinable) visible/enabled.
  • game.PlaceId is the current place; game.GameId is the universe ID (sometimes called UniverseId).
  • Places in the same universe share DataStores, MemoryStores, and Asset permissions.
  • Cross-universe (cross-experience) teleports are restricted by default. See "Cross-experience teleports" below.

TeleportAsync (the modern method)

TeleportService:TeleportAsync(placeId, players, teleportOptions?) is the single unified method. It can teleport to a different place, a specific server, or a reserved server. Server-only. Calling from the client errors. Teleport() and the other legacy methods now carry a deprecation message directing you to TeleportAsync() (see Teleport between places).

--!strict
local TeleportService = game:GetService("TeleportService")
local Players = game:GetService("Players")

local DESTINATION_PLACE_ID = 1234567890

local function teleportToPlace(player: Player)
    local options = Instance.new("TeleportOptions")
    -- options.ServerInstanceId = "..." -- to target a specific server
    -- options.ReservedServerAccessCode = "..." -- to join a reserved server
    -- options.ShouldReserveServer = true -- to create+join a new reserved server
    options:SetTeleportData({ reason = "lobby_choice", mode = "classic" })

    local ok, result = pcall(function()
        return TeleportService:TeleportAsync(DESTINATION_PLACE_ID, { player }, options)
    end)
    if not ok then
        warn("TeleportAsync failed:", tostring(result))
        return false
    end
    -- result is a TeleportAsyncResult with info about the destination
    return true
end

Group teleport limits

  • Groups can only be teleported within a single experience (same universe).
  • Max 50 players per TeleportAsync call.

Conflicting TeleportOptions

These combinations error with "Incompatible Parameters":

  • ReservedServerAccessCode + ServerInstanceId
  • ShouldReserveServer + ServerInstanceId
  • ShouldReserveServer + ReservedServerAccessCode

TeleportOptions

Property Purpose
ServerInstanceId Target a specific server by JobId.
ReservedServerAccessCode Join an existing reserved server by its access code.
ShouldReserveServer Create a new reserved server and teleport into it.
SetTeleportData(data) / GetTeleportData() Pass data to the destination (retrieved via GetLocalPlayerTeleportData).

TeleportAsyncResult

When TeleportOptions is passed, TeleportAsync returns a TeleportAsyncResult with information about the final teleport destination. Useful for confirming where a player ended up.

TeleportInitFailed (error handling)

TeleportService.TeleportInitFailed fires when a teleport fails to start. Connect to it to show a retry UI. Failure reasons include invalid place ID, empty player list, non-Player instances, wrong teleport options type, client-called TeleportAsync, and conflicting parameters.

TeleportService.TeleportInitFailed:Connect(function(
    player: Player,
    reason: Enum.TeleportResult,
    errMsg: string,
    placeId: number,
    options: TeleportOptions
)
    warn(`Teleport failed for {player.Name} to {placeId}: {reason.Name} - {errMsg}`)
    -- Show retry UI, log options, etc.
end)

Reserved servers

TeleportService:ReserveServerAsync(placeId) (server-only, yields) returns an access code plus a PrivateServerId. Players can only join a reserved server via that access code. They can't join through normal matchmaking.

  • A server starts when the access code is first used.
  • Access codes remain valid indefinitely; a reserved server can be rejoined even if no server is currently running (a new one starts).
  • Detect reserved-server context: local isReserved = game.PrivateServerId ~= "" and game.PrivateServerOwnerId == 0.
  • PrivateServerId is constant across server instances for the same access code; JobId is not.
  • Cross-platform caveat: Xbox/PlayStation players with cross-play disabled land in a different server than cross-play-enabled players. Multiple servers can share a PrivateServerId. Use game.MatchmakingType to differentiate.
--!strict
local TeleportService = game:GetService("TeleportService")
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

-- Reserve a candidate, then atomically publish-or-use the winner.
local store = DataStoreService:GetDataStore("ReservedServerAllocations_v2")
local reserveOk, candidateCode = pcall(function()
    return select(1, TeleportService:ReserveServerAsync(game.PlaceId))
end)
local code: string? = nil
if reserveOk and typeof(candidateCode) == "string" then
    local now = os.time()
    local candidate = { accessCode = candidateCode, expiresAt = now + 3600, state = "allocated" }
    local publishOk, winner = pcall(function()
        return store:UpdateAsync("DungeonRoom_1", function(current)
            if type(current) == "table" and type(current.accessCode) == "string" and current.expiresAt > now then
                return current
            end
            return candidate
        end)
    end)
    if publishOk and type(winner) == "table" then
        code = winner.accessCode -- every racing allocator uses the same winner
    end
end

local function sendToDungeon(player: Player)
    if not code then return end
    local options = Instance.new("TeleportOptions")
    options.ReservedServerAccessCode = code
    options:SetTeleportData({ room = 1 })
    pcall(function()
        TeleportService:TeleportAsync(game.PlaceId, { player }, options)
    end)
end

Passing data across teleports

Two mechanisms, with different security profiles:

TeleportOptions:SetTeleportData / GetLocalPlayerTeleportData

Set on the source server (via TeleportOptions), retrieved on the destination client via TeleportService:GetLocalPlayerTeleportData(). Client-only retrieval. Can carry any serializable value.

Security: exploiters can spoof teleport data. Send sensitive data (currency, entitlements) through DataStoreService instead, and use teleport data only for non-sensitive hints (selected game mode, cosmetic loadout preference).

SetTeleportSetting / GetTeleportSetting

Client-only, stored locally, preserved across teleports within the same game. Good for client-side preferences (crouch state, UI tab) that don't need server trust. Also spoofable. Use server-side validation for anything that affects gameplay.

Custom loading screen

TeleportService:SetTeleportGui(gui) (client) sets a ScreenGui shown during teleport. TeleportService:GetArrivingTeleportGui() (client, at the destination) retrieves it so you can parent it to PlayerGui and run your own transition. The teleport GUI does not cross into a different game (universe), and does not persist across multiple teleports. Set it before each one.

--!strict
-- At the destination, in ReplicatedFirst
local TeleportService = game:GetService("TeleportService")
local Players = game:GetService("Players")
local ReplicatedFirst = game:GetService("ReplicatedFirst")

local customLoadingScreen = TeleportService:GetArrivingTeleportGui()
if customLoadingScreen then
    local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
    ReplicatedFirst:RemoveDefaultLoadingScreen()
    customLoadingScreen.Parent = playerGui
    task.wait(5)
    customLoadingScreen:Destroy()
end

Cross-experience teleports

Teleporting from your experience to another experience owned by others fails by default. To enable cross-experience teleportation, follow the steps in the official Teleport between places doc. Within your own universe (cross-place), teleports work without extra setup.

TeleportService:PromptExperienceDetailsAsync(player, universeId) (client-only, yields) prompts the player with experience details and a Join button. If the player is ineligible to join the target experience, the button is disabled. The join button is always disabled during Studio playtesting.

Matchmaking patterns

Pattern 1: lobby → match (simple round-robin)

A lobby place collects players; when enough are ready, TeleportAsync sends them to a game place. Use MessagingService to coordinate across lobby servers if you have multiple lobbies.

Pattern 2: reserved server per match

For instanced dungeons or private matches: ReserveServerAsync → persist the access code in a DataStore keyed by match ID → teleport the party with ReservedServerAccessCode → players arrive in an isolated server.

Pattern 3: server browser via MemoryStoreService

Maintain a MemoryStoreService sorted map of active servers (keyed by JobId, value = player count / mode / ping). The lobby lists entries; when a player picks one, TeleportOptions.ServerInstanceId = jobId and TeleportAsync. Update the sorted map from each game server on join/leave. This scales better than MessagingService for many servers.

Pattern 4: external matchmaking service

For complex matchmaking (skill-based, party queues), use an external HTTP service as the matchmaker. It returns a placeId + JobId or a fresh reserved-server access code; the lobby then TeleportAsyncs the party. See roblox-networking for the HttpService security model and roblox-open-cloud for in-experience Open Cloud calls.

All matchmaking must respect the 50-player-per-TeleportAsync limit; split larger groups into multiple calls.

DataStore handoff across teleports

To move player state between places:

  1. Source server: on the triggering event, save the player's profile via UpdateAsync (see roblox-datastores). Then teleport.
  2. Destination server: on PlayerAdded, load the profile via GetAsync (consider UseCache=false for a fresh read if the save was recent).
  3. Race condition: if the save hasn't flushed by the time the destination loads, you'll get stale data. Mitigate by: (a) awaiting save confirmation before teleporting when the data is critical, or (b) passing a "load this version" hint in teleport data and retrying the load until it matches.
  4. Never trust teleport data for the authoritative state. Teleport data is a hint; the DataStore is the truth.

Security

  • TeleportAsync is server-only. The client cannot call it directly. If you need client-initiated teleports, the client sends a RemoteEvent and the server validates then calls TeleportAsync. The legacy client method Teleport() is now deprecated (use TeleportAsync() on the server); see the migration guide above.
  • Rate-limit teleport requests per player. A hostile client spamming "teleport me" Remotes can disrupt your server flow and burn DataStore budget if you save on each trigger.
  • Validate the destination. Don't let a client-supplied place ID drive the teleport unsanitized. Whitelist allowed destinations server-side.
  • Teleport data is spoofable. Treat it as a hint; re-validate any gameplay-affecting claim on the destination server against DataStores.
  • GetLocalPlayerTeleportData is client-only and returns client-controlled data. Don't use it for authoritative decisions on the server.

Studio limitation

TeleportService does not work during Studio playtesting. You must publish the experience and test teleports in the Roblox application. Plan for this in your testing workflow. Teleport logic can only be verified live.

Gotchas

  • Players lose character state across teleports (new place = new character). Save/load via DataStores; don't try to carry live character state.
  • BindToClose on the source server can race with the teleport. Finish saves before triggering teleports when data is critical.
  • The default teleport loading message was removed; CustomizedTeleportUI is deprecated and does nothing.
  • Cross-platform (Xbox/PS with cross-play off) can split a reserved server into multiple servers with the same PrivateServerId.
  • Group teleports are universe-only; you cannot TeleportAsync a group across experiences.
  • The 50-player limit is per call. Larger groups need multiple calls, and ordering matters for "arrive together" semantics.
  • Follow-friend: Players.PlayerAdded → check player.FollowUserId → GetPlayerPlaceInstanceAsync(followId) → TeleportToPlaceInstance. Note GetPlayerPlaceInstanceAsync returns JobId, not the reserved-server access code, so it won't work to follow a friend into a reserved server.

Scripts

  • scripts/TeleportHelper.lua: a reviewed server-side example with atomic reserved-code publication, expiry, ambiguous-allocation reconciliation, and separate teleporting/arrived states.

How to proceed

  1. Decide your place architecture: primary place + sub-places in one universe.
  2. For cross-place teleports, use TeleportAsync (server) + TeleportOptions.
  3. For reserved servers, reserve a candidate and publish it through UpdateAsync; use whichever allocation wins. Track teleport initiation separately from destination arrival.
  4. Pass non-sensitive hints via teleport data; pass authoritative state via DataStores.
  5. Connect TeleportInitFailed for retry UI.
  6. Validate and rate-limit any client-initiated teleport Remote.
  7. Test in the Roblox app (not Studio) with a published experience.

Reference index

  • matchmaking.md: Coordinate lobbies, reserved servers, allocation races, or player-data handoff.
  • teleport-options.md: Select a destination server or inspect a teleport result.
Files (roblox-suite)
  • references
    • matchmaking.md 8 KB
      ---
      read_when: "Coordinate lobbies, reserved servers, allocation races, or player-data handoff"
      last_reviewed: 2026-07-16
      ---
      
      # Matchmaking patterns
      
      **Official sources:**
      - https://create.roblox.com/docs/en-us/projects/teleport
      - https://create.roblox.com/docs/reference/engine/classes/MessagingService
      - https://create.roblox.com/docs/en-us/cloud-services/memory-stores
      
      Build matchmaking by combining `TeleportService:TeleportAsync` with a coordination layer (`MessagingService`, `MemoryStoreService`, or an external HTTP service) to route players into the right servers.
      
      All patterns respect the **50-player-per-`TeleportAsync`** limit (split larger groups into multiple calls) and the **server-only** rule for `TeleportAsync`.
      
      ## Pattern 1: simple lobby → match (intra-universe)
      
      A dedicated lobby place collects players; when enough are ready, `TeleportAsync` sends them to a game place in the same universe.
      
      ```lua
      --!strict
      -- Server, in the lobby place
      local TeleportService = game:GetService("TeleportService")
      local Players = game:GetService("Players")
      
      local GAME_PLACE_ID = 1234567890
      local PLAYERS_PER_MATCH = 8
      
      local readyPlayers: { Player } = {}
      
      local function tryStartMatch()
          if #readyPlayers >= PLAYERS_PER_MATCH then
              local batch = {}
              for i = 1, PLAYERS_PER_MATCH do
                  table.insert(batch, readyPlayers[i])
              end
              local options = Instance.new("TeleportOptions")
              options:SetTeleportData({ matchStartedAt = os.time() })
              pcall(function()
                  TeleportService:TeleportAsync(GAME_PLACE_ID, batch, options)
              end)
              -- Remove the batch from readyPlayers
              for i = PLAYERS_PER_MATCH, 1, -1 do
                  table.remove(readyPlayers, i)
              end
          end
      end
      ```
      
      **Scaling:** if you have multiple lobby servers, use `MessagingService` to broadcast readiness across them, or move to Pattern 3.
      
      ## Pattern 2: reserved server per match
      
      For instanced dungeons, private matches, or anything that must be isolated: reserve a server, persist the access code, teleport the party in.
      
      ```lua
      --!strict
      local TeleportService = game:GetService("TeleportService")
      local DataStoreService = game:GetService("DataStoreService")
      
      local PLACE_ID = game.PlaceId
      local codes = DataStoreService:GetDataStore("MatchServerCodes")
      
      local function getOrCreateCode(matchId: string): string?
          local reserveOk, code = pcall(function()
              return select(1, TeleportService:ReserveServerAsync(PLACE_ID))
          end)
          if not reserveOk or typeof(code) ~= "string" then return nil end
      
          local now = os.time()
          local candidate = { accessCode = code, expiresAt = now + 3600, state = "allocated" }
          local publishOk, winner = pcall(function()
              return codes:UpdateAsync("match_" .. matchId, function(current)
                  if type(current) == "table" and type(current.accessCode) == "string" and current.expiresAt > now then
                      return current
                  end
                  return candidate
              end)
          end)
          if publishOk and type(winner) == "table" then
              return winner.accessCode -- all concurrent allocators use the atomic winner
          end
          -- A failed write can be ambiguous. Reconcile with GetAsync(UseCache=false)
          -- before deciding whether allocation failed; see scripts/TeleportHelper.lua.
          return nil
      end
      
      local function sendPartyToMatch(matchId: string, party: { Player })
          local code = getOrCreateCode(matchId)
          if not code then return end
          local options = Instance.new("TeleportOptions")
          options.ReservedServerAccessCode = code
          options:SetTeleportData({ matchId = matchId })
          pcall(function()
              TeleportService:TeleportAsync(PLACE_ID, party, options)
          end)
      end
      ```
      
      Reserved server access codes remain valid, but allocation records should carry an expiry so matchmaking state does not live forever. The source can record `teleporting` only after allocation; the destination must separately mark `arrived`. For short-lived queues and locks, prefer a TTL'd MemoryStore record.
      
      ## Pattern 3: server browser via MemoryStoreService
      
      For experiences with many game servers, maintain a `MemoryStoreService` sorted map of active servers and let players browse/join. This scales better than `MessagingService` fan-out.
      
      ```lua
      --!strict
      -- On each game server, register and keep fresh:
      local MemoryStoreService = game:GetService("MemoryStoreService")
      local Players = game:GetService("Players")
      
      local serversMap = MemoryStoreService:GetSortedMap("ActiveGameServers")
      local SERVER_TTL = 60 -- seconds
      
      local function heartbeat()
          pcall(function()
              serversMap:SetAsync(game.JobId, {
                  playerCount = #Players:GetPlayers(),
                  mode = "classic",
                  updated = os.time(),
              }, SERVER_TTL)
          end)
      end
      
      -- Loop the heartbeat while the server is alive
      task.spawn(function()
          while true do
              heartbeat()
              task.wait(15)
          end
      end)
      ```
      
      ```lua
      --!strict
      -- In the lobby, list servers and let a player join one:
      local TeleportService = game:GetService("TeleportService")
      local MemoryStoreService = game:GetService("MemoryStoreService")
      
      local serversMap = MemoryStoreService:GetSortedMap("ActiveGameServers")
      
      local function listServers(): { { jobId: string, playerCount: number } }?
          local ok, entries = pcall(function()
              return serversMap:GetRangeAsync(Enum.SortDirection.Ascending, 50)
          end)
          if not ok then return nil end
          local result = {}
          for _, entry in ipairs(entries) do
              table.insert(result, { jobId = entry.key, playerCount = entry.value.playerCount })
          end
          return result
      end
      
      local function joinServer(player: Player, jobId: string)
          local options = Instance.new("TeleportOptions")
          options.ServerInstanceId = jobId
          pcall(function()
              TeleportService:TeleportAsync(game.PlaceId, { player }, options)
          end)
      end
      ```
      
      Use `MemoryStoreService`'s TTL so dead servers age out of the map automatically.
      
      ## Pattern 4: external matchmaking service
      
      For skill-based matchmaking, party queues, or anything too complex for in-engine coordination, use an external HTTP service as the matchmaker. It returns a destination (place ID + `JobId`, or a fresh reserved-server access code); the lobby then `TeleportAsync`s the party.
      
      - See roblox-networking for the `HttpService` security model (server-only, validate responses, don't trust client-supplied destinations).
      - See roblox-open-cloud for in-experience Open Cloud calls if your matchmaker needs to read/write Roblox resources.
      
      ## Cross-server coordination: when to use what
      
      | Need | Use |
      | --- | --- |
      | Broadcast a one-off event to all servers of a universe | `MessagingService:PublishAsync` |
      | Shared, TTL'd, queryable state across servers (server lists, queues, lobbies) | `MemoryStoreService` (sorted maps, queues, hash maps) |
      | Complex/skill-based matchmaking, cross-experience, or heavy logic | External HTTP service |
      | Persisting match results or player data | `DataStoreService` |
      
      `MessagingService` is fire-and-forget; if a server isn't listening when the message publishes, it misses it. `MemoryStoreService` is stateful and queryable, so use it to track currently available servers. External services are the most flexible but add a dependency and latency.
      
      ## Security and limits
      
      - **`TeleportAsync` is server-only.** Client-initiated teleports must go through a validated RemoteEvent.
      - **Rate-limit** teleport request Remotes per player.
      - **Whitelist** allowed destination place IDs server-side; don't accept client-supplied place IDs unsanitized.
      - **50 players per `TeleportAsync`** call: split larger parties.
      - **Group teleports are universe-only**: you cannot `TeleportAsync` a group across experiences.
      - **Studio can't test teleports**: publish and test in the Roblox app.
      - Reserved-server access codes are long-lived; treat them as semi-secret (anyone with the code can join the reserved server).
      
      ## Sources
      
      - https://create.roblox.com/docs/en-us/projects/teleport
      - https://create.roblox.com/docs/reference/engine/classes/MessagingService
      - https://create.roblox.com/docs/en-us/cloud-services/memory-stores
      - https://create.roblox.com/docs/en-us/reference/engine/classes/TeleportService
      
    • teleport-options.md 4.4 KB
      ---
      read_when: "Select a destination server or inspect a teleport result"
      last_reviewed: 2026-10-17
      ---
      
      # TeleportOptions and TeleportAsyncResult
      
      > The September 2026 teleport value-type, GUI-reference, and arrival-confirmation corrections are experimental guidance pending a second human review. Test cross-place behavior in a published test experience.
      
      **Official sources:**
      - https://create.roblox.com/docs/en-us/reference/engine/classes/TeleportOptions
      - https://create.roblox.com/docs/en-us/reference/engine/classes/TeleportAsyncResult
      
      ## TeleportOptions
      
      `Instance.new("TeleportOptions")` creates an options object you pass as the third argument to `TeleportService:TeleportAsync(placeId, players, options)`. If omitted, `TeleportAsync` returns no result.
      
      ### Properties
      
      | Property | Type | Purpose |
      | --- | --- | --- |
      | `ServerInstanceId` | string | Target a specific server by its `JobId`. Conflicts with `ReservedServerAccessCode` and `ShouldReserveServer`. |
      | `ReservedServerAccessCode` | string | Join an existing reserved server by its access code (from `ReserveServerAsync` or `TeleportAsyncResult.ReservedServerAccessCode`). Conflicts with `ServerInstanceId` and `ShouldReserveServer`. |
      | `ShouldReserveServer` | boolean | Create a new reserved server and teleport the players into it. Conflicts with `ServerInstanceId` and `ReservedServerAccessCode`. |
      
      ### Methods
      
      | Method | Purpose |
      | --- | --- |
      | `SetTeleportData(data: Variant)` | Pass data to the destination. Retrieved via `GetLocalPlayerTeleportData()` on the destination client. |
      | `GetTeleportData(): Variant` | Read back what was set; returns the data previously stored by `SetTeleportData()`, or `nil` if no data was set. |
      
      ### Conflicting combinations (error)
      
      - `ReservedServerAccessCode` + `ServerInstanceId`
      - `ShouldReserveServer` + `ServerInstanceId`
      - `ShouldReserveServer` + `ReservedServerAccessCode`
      
      Pick exactly one mode: public server (neither), specific server (`ServerInstanceId`), existing reserved (`ReservedServerAccessCode`), or new reserved (`ShouldReserveServer`).
      
      ### Teleport data rules
      
      - Client-retrieved via `TeleportService:GetLocalPlayerTeleportData()`: **client-only**.
      - **Spoofable.** Treat as a hint; validate gameplay-affecting claims server-side against DataStores.
      - Supports primitives and engine value types such as `Vector3`, `CFrame`, `Color3`, `UDim2`, sequences, and enum items, including tables/arrays of supported values without mixed keys.
      - Instances, functions, connections, signals, `SharedTable`, and engine-state objects such as `RaycastParams`/`RaycastResult` cannot cross this boundary. Roblox removes disallowed values and logs an error. Convert references to plain IDs or paths.
      - `SetTeleportSetting` uses the same value-only restriction. A custom teleport GUI preserves references inside its own tree, but references to instances outside that tree are cleared; include required dependencies in the GUI tree.
      
      ## TeleportAsyncResult
      
      Returned by `TeleportAsync` when a `TeleportOptions` is passed. Describes the selected destination, including a newly reserved server when `ShouldReserveServer` is used. It does not prove that the player arrived; observe arrival separately in the destination server.
      
      Inspect its properties per the class reference; typical fields include the destination place ID and instance/access-code identifiers.
      
      ## Common patterns
      
      ### Teleport to a specific existing server
      
      ```lua
      local options = Instance.new("TeleportOptions")
      options.ServerInstanceId = targetJobId
      options:SetTeleportData({ reason = "join_friend" })
      TeleportService:TeleportAsync(DESTINATION_PLACE_ID, { player }, options)
      ```
      
      ### Create a new reserved server and teleport in
      
      ```lua
      local options = Instance.new("TeleportOptions")
      options.ShouldReserveServer = true
      options:SetTeleportData({ matchId = "abc123" })
      local result = TeleportService:TeleportAsync(DESTINATION_PLACE_ID, party, options)
      -- result carries info about the newly created reserved server
      ```
      
      ### Join an existing reserved server
      
      ```lua
      local options = Instance.new("TeleportOptions")
      options.ReservedServerAccessCode = savedAccessCode
      TeleportService:TeleportAsync(PLACE_ID, { player }, options)
      ```
      
      ## Sources
      
      - https://create.roblox.com/docs/en-us/reference/engine/classes/TeleportOptions
      - https://create.roblox.com/docs/en-us/reference/engine/classes/TeleportAsyncResult
      - https://create.roblox.com/docs/en-us/reference/engine/classes/TeleportService
      
  • scripts
    • TeleportHelper.lua 9.3 KB · in bundle
  • tests
    • TeleportHelper.spec.lua 1.3 KB · in bundle
  • SKILL.md 15.5 KB
    ---
    name: roblox-teleport
    description: "Move Roblox players between places, public servers, or reserved servers. Use for TeleportAsync, matchmaking, loading screens, teleport failures, and persistent-data handoff."
    last_reviewed: 2026-08-18
    ---
    
    # roblox-teleport
    
    **Official sources:**
    - https://create.roblox.com/docs/en-us/reference/engine/classes/TeleportService
    - https://create.roblox.com/docs/en-us/projects/teleport
    - https://create.roblox.com/docs/en-us/reference/engine/classes/TeleportOptions
    - https://create.roblox.com/docs/en-us/reference/engine/classes/TeleportAsyncResult
    - https://create.roblox.com/docs/projects/teleport (multi-place architecture)
    
    TeleportService moves players between places and servers. The modern entry point is `TeleportAsync` (server-only); the older `Teleport` (client), `TeleportPartyAsync`, `TeleportToPlaceInstance`, `TeleportToPrivateServer`, and `TeleportToSpawnByName` methods are **deprecated**. Use `TeleportAsync()` instead. For client-initiated teleports, fire a `RemoteEvent` to the server and let the server call `TeleportAsync()`; do not call `Teleport()` from the client for new work, even if the place is configured as "Fully Open." See the [migrate to secure teleports](https://create.roblox.com/docs/en-us/projects/teleport#migrate-to-secure-teleports) guide.
    
    Cross-reference:
    - [roblox-datastores/SKILL.md](../roblox-datastores/SKILL.md): handoff pattern for player data across teleports.
    - [roblox-networking/SKILL.md](../roblox-networking/SKILL.md): server authority, rate limiting, and the security model around teleport requests.
    - [roblox-core/SKILL.md](../roblox-core/SKILL.md): services and script contexts.
    
    ## Multi-place architecture
    
    A **universe** (experience) can contain multiple **places**. Each place has its own `PlaceId`; the universe has a `UniverseId` (exposed as `game.GameId`). The **primary place** is the one players join from the Roblox app's main entry point.
    
    - Create additional places in the Creator Dashboard under your experience → Places.
    - Each place must be published and (for it to be joinable) visible/enabled.
    - `game.PlaceId` is the current place; `game.GameId` is the universe ID (sometimes called UniverseId).
    - Places in the same universe share DataStores, MemoryStores, and Asset permissions.
    - Cross-universe (cross-experience) teleports are restricted by default. See "Cross-experience teleports" below.
    
    ## TeleportAsync (the modern method)
    
    `TeleportService:TeleportAsync(placeId, players, teleportOptions?)` is the single unified method. It can teleport to a different place, a specific server, or a reserved server. **Server-only**. Calling from the client errors. `Teleport()` and the other legacy methods now carry a deprecation message directing you to `TeleportAsync()` (see [Teleport between places](https://create.roblox.com/docs/en-us/projects/teleport#migrate-to-secure-teleports)).
    
    ```lua
    --!strict
    local TeleportService = game:GetService("TeleportService")
    local Players = game:GetService("Players")
    
    local DESTINATION_PLACE_ID = 1234567890
    
    local function teleportToPlace(player: Player)
        local options = Instance.new("TeleportOptions")
        -- options.ServerInstanceId = "..." -- to target a specific server
        -- options.ReservedServerAccessCode = "..." -- to join a reserved server
        -- options.ShouldReserveServer = true -- to create+join a new reserved server
        options:SetTeleportData({ reason = "lobby_choice", mode = "classic" })
    
        local ok, result = pcall(function()
            return TeleportService:TeleportAsync(DESTINATION_PLACE_ID, { player }, options)
        end)
        if not ok then
            warn("TeleportAsync failed:", tostring(result))
            return false
        end
        -- result is a TeleportAsyncResult with info about the destination
        return true
    end
    ```
    
    ### Group teleport limits
    
    - Groups can only be teleported **within a single experience** (same universe).
    - **Max 50 players** per `TeleportAsync` call.
    
    ### Conflicting TeleportOptions
    
    These combinations error with "Incompatible Parameters":
    - `ReservedServerAccessCode` + `ServerInstanceId`
    - `ShouldReserveServer` + `ServerInstanceId`
    - `ShouldReserveServer` + `ReservedServerAccessCode`
    
    ## TeleportOptions
    
    | Property | Purpose |
    | --- | --- |
    | `ServerInstanceId` | Target a specific server by `JobId`. |
    | `ReservedServerAccessCode` | Join an existing reserved server by its access code. |
    | `ShouldReserveServer` | Create a new reserved server and teleport into it. |
    | `SetTeleportData(data)` / `GetTeleportData()` | Pass data to the destination (retrieved via `GetLocalPlayerTeleportData`). |
    
    ## TeleportAsyncResult
    
    When `TeleportOptions` is passed, `TeleportAsync` returns a `TeleportAsyncResult` with information about the final teleport destination. Useful for confirming where a player ended up.
    
    ## TeleportInitFailed (error handling)
    
    `TeleportService.TeleportInitFailed` fires when a teleport fails to start. Connect to it to show a retry UI. Failure reasons include invalid place ID, empty player list, non-Player instances, wrong teleport options type, client-called `TeleportAsync`, and conflicting parameters.
    
    ```lua
    TeleportService.TeleportInitFailed:Connect(function(
        player: Player,
        reason: Enum.TeleportResult,
        errMsg: string,
        placeId: number,
        options: TeleportOptions
    )
        warn(`Teleport failed for {player.Name} to {placeId}: {reason.Name} - {errMsg}`)
        -- Show retry UI, log options, etc.
    end)
    ```
    
    ## Reserved servers
    
    `TeleportService:ReserveServerAsync(placeId)` (server-only, yields) returns an access code plus a `PrivateServerId`. Players can only join a reserved server via that access code. They can't join through normal matchmaking.
    
    - A server starts when the access code is first used.
    - Access codes remain valid indefinitely; a reserved server can be rejoined even if no server is currently running (a new one starts).
    - Detect reserved-server context: `local isReserved = game.PrivateServerId ~= "" and game.PrivateServerOwnerId == 0`.
    - `PrivateServerId` is constant across server instances for the same access code; `JobId` is not.
    - **Cross-platform caveat:** Xbox/PlayStation players with cross-play disabled land in a different server than cross-play-enabled players. Multiple servers can share a `PrivateServerId`. Use `game.MatchmakingType` to differentiate.
    
    ```lua
    --!strict
    local TeleportService = game:GetService("TeleportService")
    local DataStoreService = game:GetService("DataStoreService")
    local Players = game:GetService("Players")
    
    -- Reserve a candidate, then atomically publish-or-use the winner.
    local store = DataStoreService:GetDataStore("ReservedServerAllocations_v2")
    local reserveOk, candidateCode = pcall(function()
        return select(1, TeleportService:ReserveServerAsync(game.PlaceId))
    end)
    local code: string? = nil
    if reserveOk and typeof(candidateCode) == "string" then
        local now = os.time()
        local candidate = { accessCode = candidateCode, expiresAt = now + 3600, state = "allocated" }
        local publishOk, winner = pcall(function()
            return store:UpdateAsync("DungeonRoom_1", function(current)
                if type(current) == "table" and type(current.accessCode) == "string" and current.expiresAt > now then
                    return current
                end
                return candidate
            end)
        end)
        if publishOk and type(winner) == "table" then
            code = winner.accessCode -- every racing allocator uses the same winner
        end
    end
    
    local function sendToDungeon(player: Player)
        if not code then return end
        local options = Instance.new("TeleportOptions")
        options.ReservedServerAccessCode = code
        options:SetTeleportData({ room = 1 })
        pcall(function()
            TeleportService:TeleportAsync(game.PlaceId, { player }, options)
        end)
    end
    ```
    
    ## Passing data across teleports
    
    Two mechanisms, with different security profiles:
    
    ### `TeleportOptions:SetTeleportData` / `GetLocalPlayerTeleportData`
    
    Set on the source server (via `TeleportOptions`), retrieved on the destination client via `TeleportService:GetLocalPlayerTeleportData()`. **Client-only retrieval.** Can carry any serializable value.
    
    **Security:** exploiters can spoof teleport data. Send sensitive data (currency, entitlements) through `DataStoreService` instead, and use teleport data only for non-sensitive hints (selected game mode, cosmetic loadout preference).
    
    ### `SetTeleportSetting` / `GetTeleportSetting`
    
    Client-only, stored locally, preserved across teleports within the same game. Good for client-side preferences (crouch state, UI tab) that don't need server trust. Also spoofable. Use server-side validation for anything that affects gameplay.
    
    ## Custom loading screen
    
    `TeleportService:SetTeleportGui(gui)` (client) sets a `ScreenGui` shown during teleport. `TeleportService:GetArrivingTeleportGui()` (client, at the destination) retrieves it so you can parent it to `PlayerGui` and run your own transition. The teleport GUI does **not** cross into a different game (universe), and does not persist across multiple teleports. Set it before each one.
    
    ```lua
    --!strict
    -- At the destination, in ReplicatedFirst
    local TeleportService = game:GetService("TeleportService")
    local Players = game:GetService("Players")
    local ReplicatedFirst = game:GetService("ReplicatedFirst")
    
    local customLoadingScreen = TeleportService:GetArrivingTeleportGui()
    if customLoadingScreen then
        local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
        ReplicatedFirst:RemoveDefaultLoadingScreen()
        customLoadingScreen.Parent = playerGui
        task.wait(5)
        customLoadingScreen:Destroy()
    end
    ```
    
    ## Cross-experience teleports
    
    Teleporting from your experience to **another experience owned by others fails by default**. To enable cross-experience teleportation, follow the steps in the official [Teleport between places](https://create.roblox.com/docs/en-us/projects/teleport#enable-cross-experience-teleportation) doc. Within your own universe (cross-place), teleports work without extra setup.
    
    `TeleportService:PromptExperienceDetailsAsync(player, universeId)` (client-only, yields) prompts the player with experience details and a Join button. If the player is ineligible to join the target experience, the button is disabled. The join button is always disabled during Studio playtesting.
    
    ## Matchmaking patterns
    
    ### Pattern 1: lobby → match (simple round-robin)
    
    A lobby place collects players; when enough are ready, `TeleportAsync` sends them to a game place. Use `MessagingService` to coordinate across lobby servers if you have multiple lobbies.
    
    ### Pattern 2: reserved server per match
    
    For instanced dungeons or private matches: `ReserveServerAsync` → persist the access code in a DataStore keyed by match ID → teleport the party with `ReservedServerAccessCode` → players arrive in an isolated server.
    
    ### Pattern 3: server browser via MemoryStoreService
    
    Maintain a `MemoryStoreService` sorted map of active servers (keyed by `JobId`, value = player count / mode / ping). The lobby lists entries; when a player picks one, `TeleportOptions.ServerInstanceId = jobId` and `TeleportAsync`. Update the sorted map from each game server on join/leave. This scales better than `MessagingService` for many servers.
    
    ### Pattern 4: external matchmaking service
    
    For complex matchmaking (skill-based, party queues), use an external HTTP service as the matchmaker. It returns a `placeId` + `JobId` or a fresh reserved-server access code; the lobby then `TeleportAsync`s the party. See roblox-networking for the `HttpService` security model and roblox-open-cloud for in-experience Open Cloud calls.
    
    All matchmaking must respect the **50-player-per-`TeleportAsync`** limit; split larger groups into multiple calls.
    
    ## DataStore handoff across teleports
    
    To move player state between places:
    
    1. **Source server:** on the triggering event, save the player's profile via `UpdateAsync` (see roblox-datastores). Then teleport.
    2. **Destination server:** on `PlayerAdded`, load the profile via `GetAsync` (consider `UseCache=false` for a fresh read if the save was recent).
    3. **Race condition:** if the save hasn't flushed by the time the destination loads, you'll get stale data. Mitigate by: (a) awaiting save confirmation before teleporting when the data is critical, or (b) passing a "load this version" hint in teleport data and retrying the load until it matches.
    4. **Never trust teleport data for the authoritative state.** Teleport data is a hint; the DataStore is the truth.
    
    ## Security
    
    - **`TeleportAsync` is server-only.** The client cannot call it directly. If you need client-initiated teleports, the client sends a RemoteEvent and the server validates then calls `TeleportAsync`. The legacy client method `Teleport()` is now deprecated (use `TeleportAsync()` on the server); see the migration guide above.
    - **Rate-limit teleport requests** per player. A hostile client spamming "teleport me" Remotes can disrupt your server flow and burn DataStore budget if you save on each trigger.
    - **Validate the destination.** Don't let a client-supplied place ID drive the teleport unsanitized. Whitelist allowed destinations server-side.
    - **Teleport data is spoofable.** Treat it as a hint; re-validate any gameplay-affecting claim on the destination server against DataStores.
    - **`GetLocalPlayerTeleportData` is client-only** and returns client-controlled data. Don't use it for authoritative decisions on the server.
    
    ## Studio limitation
    
    **TeleportService does not work during Studio playtesting.** You must publish the experience and test teleports in the Roblox application. Plan for this in your testing workflow. Teleport logic can only be verified live.
    
    ## Gotchas
    
    - Players lose character state across teleports (new place = new character). Save/load via DataStores; don't try to carry live character state.
    - `BindToClose` on the source server can race with the teleport. Finish saves before triggering teleports when data is critical.
    - The default teleport loading message was removed; `CustomizedTeleportUI` is deprecated and does nothing.
    - Cross-platform (Xbox/PS with cross-play off) can split a reserved server into multiple servers with the same `PrivateServerId`.
    - Group teleports are universe-only; you cannot `TeleportAsync` a group across experiences.
    - The 50-player limit is per call. Larger groups need multiple calls, and ordering matters for "arrive together" semantics.
    - Follow-friend: `Players.PlayerAdded` → check `player.FollowUserId` → `GetPlayerPlaceInstanceAsync(followId)` → `TeleportToPlaceInstance`. Note `GetPlayerPlaceInstanceAsync` returns `JobId`, not the reserved-server access code, so it won't work to follow a friend into a reserved server.
    
    ## Scripts
    
    - `scripts/TeleportHelper.lua`: a reviewed server-side example with atomic reserved-code publication, expiry, ambiguous-allocation reconciliation, and separate `teleporting`/`arrived` states.
    
    ## How to proceed
    
    1. Decide your place architecture: primary place + sub-places in one universe.
    2. For cross-place teleports, use `TeleportAsync` (server) + `TeleportOptions`.
    3. For reserved servers, reserve a candidate and publish it through `UpdateAsync`; use whichever allocation wins. Track teleport initiation separately from destination arrival.
    4. Pass non-sensitive hints via teleport data; pass authoritative state via DataStores.
    5. Connect `TeleportInitFailed` for retry UI.
    6. Validate and rate-limit any client-initiated teleport Remote.
    7. Test in the Roblox app (not Studio) with a published experience.
    
    <!-- catalog:references:start -->
    ## Reference index
    
    - [matchmaking.md](references/matchmaking.md): Coordinate lobbies, reserved servers, allocation races, or player-data handoff.
    - [teleport-options.md](references/teleport-options.md): Select a destination server or inspect a teleport result.
    <!-- catalog:references:end -->
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related