TeleportRetry
Sends framework teleports, retries failed attempts on a short ladder chosen by TeleportResult, and keeps one unresolved teleport per player.
- Realm
- Server
- Require
ServerScriptService.ChloeKernelServer .TeleportRetry - Since
- v0.8.0
- Source
- TeleportRetry.luau
TeleportRetry is the teleport lifecycle behind Matchmaking and
SoftShutdown. It sends a group through TeleportService:TeleportAsync, matches each
TeleportInitFailed event to the attempt that caused it, retries on a short ladder and reports the players it could not
move. The rule that defines it: a player has at most one unresolved engine teleport across every tracker on the server,
and a deadline limits retries, never the engine. Roblox has no call that cancels a teleport once it has started, so an
attempt whose outcome is unknown keeps the player owned until the engine reports a failure or the player leaves.
How it works
Section titled “How it works”One owner per player
Section titled “One owner per player”Trackers built on the same TeleportService share a hub. The hub holds the pending map (player to attempt state) and the
only two connections the module makes: TeleportService.TeleportInitFailed and Players.PlayerRemoving. Matchmaking and
SoftShutdown both build their tracker on the game’s TeleportService, so a player one of them is teleporting is pending for
the other as well. The hub disconnects once no tracker is alive and no attempt is pending.
send skips every player who is already pending. It never replaces an earlier destination and never starts a second
engine call for that player; the other members of the group still go. A shutdown that finds a player in the middle of a
match teleport leaves the match destination in control, because superseding it could send the player to two servers.
Correlating failures
Section titled “Correlating failures”Every engine call carries a fresh GUID under the reserved teleport data key __ChloeKernelTeleportAttempt. The tracker
builds each call’s options by cloning the caller’s TeleportOptions and setting the teleport data on the copy: the data
read from the source plus the token. It sets the data explicitly because the engine reference does not say that Clone
carries teleport data, and SoftShutdown’s transit flag has to survive every attempt.
A TeleportInitFailed event counts only when the player is pending on this tracker, the event’s place id matches the
attempt, and the token inside the event’s options matches the current attempt. Duplicate events and events left over
from an earlier attempt are ignored.
Attempts and retries
Section titled “Attempts and retries”An attempt is in one of four states: calling (TeleportAsync is still yielding), awaiting departure (the call returned
without a known failure), waiting for a retry, or unknown (the deadline passed without an answer). A normal return from
TeleportAsync does not prove the player left, so a returned call only moves the attempt to awaiting departure.
A retry needs a known failure (a thrown call or a correlated TeleportInitFailed) and a call that has returned. A
failure event that arrives while the call is still yielding is recorded and acted on once the call returns, so there are
never two engine calls in flight for one player.
- At most 3 engine calls per player per
send. - The wait before the next call is the number of calls made so far, in seconds: 1 s, then 2 s. After a
Floodedresult it is five times that: 5 s, then 10 s. - A retry uses the options object that
TeleportInitFailedsupplied. Roblox builds a new object prefilled with the exact destination, including a reserved access code generated forShouldReserveServerand the public server picked for a failed member of a group. A thrown call retries with the options it was sent. - All attempts share one absolute deadline: 30 s after
sendby default, or thedeadlinethe caller passes. No attempt starts after it.
Retries are per player. When a group call fails late for one member, only that member is retried, so members whose teleport went through are never teleported a second time.
What TeleportResult decides
Section titled “What TeleportResult decides”| Result | Tracker response |
|---|---|
GameNotFound, Unauthorized |
Finish at once with that result as the reason. The same destination cannot succeed. |
IsTeleporting |
Not a failure: the player is already on the way. The result and message are recorded, ownership stays, and the tracker waits for departure or a later failure. |
Flooded |
Retry with the longer backoff (5 s, then 10 s) so the retries do not add to the flood. |
GameFull, GameEnded |
Finish at once when the destination is pinned: the event’s options carry a ReservedServerAccessCode or a ServerInstanceId, as every Matchmaking and transit teleport does. An unpinned place teleport retries, and the engine picks another server. |
Failure, or a thrown call |
The retry ladder above. |
The engine passes an Enum.TeleportResult. Anything else is treated as Failure.
Failure callbacks
Section titled “Failure callbacks”send takes onFailure(player, reason, result?, message?):
reasonis the result name that ended the attempt (GameNotFound,Unauthorized,GameFull,GameEnded),TeleportFailedwhen three calls failed, orTeleportTimedOutwhen the deadline passed with a known failure.resultis the last attempt’sTeleportResultname, andmessageits error text. A thrown call leavesresultnil and puts the thrown error inmessage.
The tracker releases the player before calling back, so the callback can queue the player again (Matchmaking does).
Each callback runs on its own thread inside a pcall. It may yield, a yielding callback cannot hold up the rest of the
group, and an error is logged as [TeleportRetry] failure callback failed: .... The callback does not run when the
player has left, the attempt was cancelled, or the tracker was destroyed.
Why a deadline cannot cancel a teleport
Section titled “Why a deadline cannot cancel a teleport”TeleportService has no operation that cancels an initiated teleport. Cancelling a Luau thread or letting a timer expire does not roll the engine back, so the tracker treats the deadline as the end of its own retries only. When it arrives:
- An attempt with a known failure and a returned call finishes with
TeleportTimedOut. - An attempt whose call is still yielding, or whose call returned with no failure and no departure, becomes unknown. Its scheduled retry is cancelled, the failure callback does not run, and the player stays pending.
A definitive failure that arrives later releases the player and reports it. A departure releases the player without a callback: once the player has left there is nothing to retry, whether or not they arrived. If neither signal ever arrives, the player stays blocked from new tracked teleports for the rest of the session. That is deliberate: another attempt could send the player to two destinations.
Cancel and destroy
Section titled “Cancel and destroy”cancel(player) and destroy() stop this tracker’s scheduled retry and deadline jobs and suppress its future
callbacks. Neither stops an engine teleport. An attempt whose call returned with a known failure is released at once. An
unresolved one keeps only its hub ownership, so the hub still sees the failure or departure after the tracker that
started it is gone. Callbacks that are already running are not interrupted.
cancel affects only players this tracker sent. Matchmaking calls it on dequeue and at session end, and destroys its
tracker in destroy(). SoftShutdown destroys its tracker at the end of its close handler.
Matchmaking and SoftShutdown create their own trackers, and most games never touch this module. A game that teleports players itself, such as a portal to another place in the universe, can send through a tracker so its teleports share the same ownership and never overlap a match or shutdown teleport.
--!strict-- Places: place ids in this universe that the game teleports to.
return { Dungeon = 123456789, -- replace with your dungeon place id}--!strict-- PortalService: sends groups to the dungeon place through the shared teleport tracker.
local ServerScriptService = game:GetService("ServerScriptService")local TeleportService = game:GetService("TeleportService")
local Places = require(script.Parent.Parent.Config.Places)local TeleportRetry = require(ServerScriptService.ChloeKernelServer.TeleportRetry)
local PortalService = { Name = "PortalService",}
function PortalService.init(self: any, kernel: any) self.Kernel = kernel self.Teleports = TeleportRetry.new(TeleportService)end
-- Yields while TeleportAsync runs. (false, "TeleportPending") means every player already had a teleport in flight.function PortalService.send(self: any, players: { Player }, entrance: string): (boolean, string?) local Options = Instance.new("TeleportOptions") Options:SetTeleportData({ Entrance = entrance }) -- teleport data is client-visible and spoofable: no rewards in it return self.Teleports:send(Places.Dungeon, players, Options, function(player, reason, result, message) self.Kernel.Bus:publish("Portal.Failed", player, reason, result, message) end)end
function PortalService.stop(self: any) if self.Teleports then self.Teleports:destroy() endend
return PortalServiceThe dungeon is an unpinned place teleport, so a GameFull result retries and the engine picks another server. Call
send from a spawned thread or a handler that may yield, never from a Scheduler job.
API reference
Section titled “API reference”| Member | Description |
|---|---|
TeleportRetry.new(service, options?) -> tracker |
A tracker on service (a TeleportService). Joins or creates the hub for that service. options are test injection points: Players (the PlayerRemoving source), Clock (default os.clock), Delay (default task.delay), CancelDelay (default task.cancel) and Present(player) (default player.Parent ~= nil). |
tracker:send(placeId, players, options?, onFailure, deadline?) -> (boolean, string?) |
Yields while TeleportAsync runs. Removes duplicates and players who left, skips players pending on any tracker for this service, and sends the rest in one call. options is a TeleportOptions (nil sends a plain place teleport). deadline is an absolute time on the tracker’s clock, default 30 s from now. Returns true when an engine call was made, with "TeleportPending" as the second value when some players were skipped. Returns false with "TeleportPending" (every player was pending), "NoPlayers", "Destroyed", or "TeleportTimedOut" (the deadline had already passed; each player’s callback gets TeleportTimedOut and nothing is sent). true means the call went out; it does not mean anyone arrived. |
tracker:isPending(player) -> boolean |
Whether the player has an unresolved attempt on any tracker that shares this TeleportService. |
tracker:cancel(player) |
Cancels this tracker’s retry and deadline jobs for the player and drops its callback. Releases the player at once only when the last call returned with a known failure; otherwise ownership stays until failure or departure. |
tracker:destroy() |
Cancels every player this tracker sent, then leaves the hub. Later send calls return false, "Destroyed". Idempotent. |
| Callback | Description |
|---|---|
onFailure(player, reason, result?, message?) |
Runs on its own thread after the player is released. reason: an ending result name, TeleportFailed or TeleportTimedOut. Skipped when the player left, the attempt was cancelled, or the tracker was destroyed. |
Engine events and Bus topics
Section titled “Engine events and Bus topics”TeleportRetry publishes nothing on the Bus. Its callers do: Matchmaking forwards every failure to
Matchmaking.TeleportFailed. The hub consumes two engine events.
| Event | Used for |
|---|---|
TeleportService.TeleportInitFailed(player, result, message, placeId, options) |
Matched by player, place id and attempt token, then handled by result as in the table above. |
Players.PlayerRemoving(player) |
Releases the player’s attempt without a callback. |