PlayerAdded & CharacterAdded Done Right
Last updated:
Quick answer: PlayerAdded can miss players because joining races your script startup, so connect the event first and then run the same handler for everyone already in Players:GetPlayers() with task.defer — and handle player.Character for anyone whose character already spawned.
Spawn code that "sometimes" skips players is almost always a race condition: your script connects PlayerAdded after someone has already joined, or connects CharacterAdded after their character already spawned. The robust pattern is to write one handler, connect the event, and then run that same handler for everyone the event will never fire for — using Players:GetPlayers() and task.defer. Do that and every join and every respawn is covered.
Player, Character, Humanoid — which object do you actually need?
Three different objects live at the center of join and respawn code, and mixing them up causes half of all spawn bugs:
Player— the player's session. It exists from join until leave and does not reset on death. Anything that should survive respawns (coins, wins, leaderstats) belongs here or in a server-side table keyed by it.Character— the model in the workspace (aModelcontaining aHumanoidRootPartand more). It is destroyed on death and a brand-new one is created on every respawn, so anything you attach to it dies with it.Humanoid— the object inside the character that manages health, walking, and jumping. It exposesDiedand dies exactly once per character.
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(character)
local humanoid = character:WaitForChild("Humanoid")
humanoid.Died:Connect(function()
-- runs once for THIS character only
end)
end)
end)
Rule of thumb: state goes on the Player (or in your own table), behavior goes on the Character and its Humanoid, and anything that must run again on respawn goes in a CharacterAdded handler.
Why does PlayerAdded sometimes miss players in Studio tests?
Event connections only fire for things that happen after the connection exists. Scripts do not all start at the same instant: a heavy ServerScriptService script that yields, a ModuleScript chain that takes time to require, or simply pressing Play when the character spawns immediately — any of these can leave your PlayerAdded connection established after the player already joined. The event will never fire for them, and your setup silently skips that player. In Studio this shows up as "it works when I test twice but not the first time," which is exactly the signature of the race.
What is the robust join pattern? (GetPlayers + task.defer)
Connect first, then sweep up the players who are already here:
local Players = game:GetService("Players")
local function onPlayerAdded(player)
-- one place for ALL per-player setup:
-- load data, build leaderstats, hook CharacterAdded
player.CharacterAdded:Connect(function(character)
-- per-respawn setup
end)
end
-- 1. Connect first: covers everyone who joins from now on
Players.PlayerAdded:Connect(onPlayerAdded)
-- 2. Then sweep: covers everyone who joined before this script ran
for _, player in Players:GetPlayers() do
task.defer(onPlayerAdded, player)
end
Order matters. Connecting first means nobody can slip through the gap while you iterate; the loop then catches the players the event already missed. task.defer runs each handler on its own thread at the next resumption point, so a handler that yields (loading data, waiting for a character) never blocks the loop or the event thread.
The same trick applies to the character. CharacterAdded misses any character that already spawned, so handle both cases:
local function setupCharacter(player, character)
local humanoid = character:WaitForChild("Humanoid")
-- spawn effects, tool setup, movement tweaks
end
if player.Character then
task.defer(setupCharacter, player, player.Character)
end
player.CharacterAdded:Connect(setupCharacter)
That pair — connect the event and handle the already-spawned case — is the whole fix for "my spawn code sometimes doesn't run."
CharacterAdded vs CharacterAppearanceLoaded vs Humanoid.Died — which fires when?
All three fire per character, but at different moments and for different jobs:
CharacterAdded(character)— as soon as the new character model exists. The model may not have its accessories or appearance yet, but its core parts andHumanoidare there (useWaitForChildto be safe).CharacterAppearanceLoaded(character)— after the character's appearance (face, clothing, body colors) has finished loading. Use it for code that readsHumanoidDescription, checks worn items, or shows player-specific visuals — anything appearance-dependent would see incomplete data insideCharacterAdded.Humanoid.Died— when this character's health reaches zero. It fires once per character and never again for the next one, which is why kill feeds, death counters, and round scoring connect it fresh inside everyCharacterAdded.
player.CharacterAdded:Connect(function(character)
local humanoid = character:WaitForChild("Humanoid")
humanoid.Died:Connect(function()
deaths += 1
end)
character.CharacterAppearanceLoaded:Connect(function()
-- safe to read appearance here
end)
end)
If you need "the round is over" logic, connect to Died on the server and let it update server state — the client can report UI, never truth.
How do you track respawn state without leaking connections?
Every respawn creates a new character, and each one may collect connections (Died, Touched, Changed). If you never disconnect them, they pile up per respawn: old ones usually die with their character, but connections made on the Player or on services live forever and keep running against dead objects. Store them and clean up on the character change:
local connections = {} -- [player] = { RBXScriptConnection, ... }
local function watchCharacter(player, character)
connections[player] = connections[player] or {}
local humanoid = character:WaitForChild("Humanoid")
table.insert(connections[player], humanoid.Died:Connect(function()
-- score the death
end))
end
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(character)
watchCharacter(player, character)
end)
player.CharacterRemoving:Connect(function()
for _, conn in connections[player] do
conn:Disconnect()
end
table.clear(connections[player])
end)
Players.PlayerRemoving:Connect(function()
connections[player] = nil
end)
end)
Note where the state lives: the connection list is keyed by Player, not by Character, so respawns reuse it and leaving players get removed entirely. That is the same discipline as keeping coins on the player instead of on the model — see the leaderstats tutorial for the persistent-state side.
What should run on the client instead?
Client code follows the same event pattern, but the container decides its lifetime:
StarterPlayerScripts— aLocalScripthere runs once per session and survives respawns. Put persistent client logic here: input handling, UI state, camera controllers.StarterGui— scripts here are copied intoPlayerGuiand die on respawn (withResetPlayerGuiOnSpawnon, which is the default), so they re-run every respawn and any state inside them is lost. Fine for per-life HUD code, wrong for anything that must remember.StarterCharacterScripts— re-run for every new character, likeCharacterAddedbut declarative.
-- StarterPlayer > StarterPlayerScripts > LocalScript
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local function onCharacterAdded(character)
local humanoid = character:WaitForChild("Humanoid")
-- per-respawn client work: camera, HUD bindings
end
if player.Character then
task.defer(onCharacterAdded, player.Character)
end
player.CharacterAdded:Connect(onCharacterAdded)
Same connect-plus-sweep pattern as the server, just with Players.LocalPlayer. One caveat: the client's view of the world replicates with a delay, so WaitForChild on anything under the character or ReplicatedStorage instead of assuming it exists — an unhandled wait on a missing instance turns into the infinite yield WaitForChild fix. And when client code must affect gameplay, it sends a request the server validates through RemoteEvents; the client never decides what happens.
Common mistakes
- Connecting
CharacterAddedwithout handling the current character. The already-spawned character never fires the event — checkplayer.Charactertoo. - Putting permanent state on the character. Coins on the model vanish on every respawn; keep them on the
Playeror in a server table. - Connecting
Humanoid.Diedonce per player. It fires once per character — connect it fresh inside everyCharacterAdded. - Leaking per-character connections. Disconnect them in
CharacterRemovingand clear the list. - Relying on legacy
wait(),spawn(), ordelay()for respawn timing. They are deprecated and unreliable under load — use events for timing at all, andtask.wait/task.deferwhere a yield is genuinely needed. - Assuming StarterGui scripts persist. They are destroyed and re-created on respawn; session-long client logic belongs in
StarterPlayerScripts.
Learn Luau by doing
The connect-plus-sweep pattern is two lines, but knowing why it works is what keeps spawn bugs away for good. On Luablox you can practice Luau interactively through /theory, drill event and state problems on /problems, and follow structured Luablox lessons that build from basics to full gameplay systems.
Frequently asked questions
Why does my CharacterAdded code not run the first time?
Because the character already spawned before you connected the event — CharacterAdded only fires for characters created after the connection exists. Handle both cases: run your setup for player.Character if it is already there, and connect CharacterAdded for every respawn after that.
What is the difference between PlayerAdded and CharacterAdded?
PlayerAdded fires once when a player joins the server and its Player object persists until they leave. CharacterAdded fires every time that player gets a new character model — once at spawn and again after every death — so per-life setup like Humanoid hooks or spawn effects belongs in CharacterAdded, while persistent setup like data loading belongs in PlayerAdded.
How do I run code every time a player respawns?
Connect player.CharacterAdded inside your PlayerAdded handler (and handle player.Character for the first spawn). Everything you need per respawn goes in that callback; anything attached to the character is recreated automatically, and connections you make there should be disconnected when the character is removed.
Why does PlayerAdded sometimes not fire in Studio?
Your script connected the event after the player had already joined — a startup race that is common in Studio where the character spawns immediately. Connect PlayerAdded first, then loop over Players:GetPlayers() and run the same handler with task.defer so already-present players are covered too.
Do LocalScripts in StarterGui run again on respawn?
Yes — with ResetPlayerGuiOnSpawn enabled (the default) the PlayerGui is cleared on respawn and StarterGui scripts are copied in and run again. Any state those scripts held is lost, so session-long client logic belongs in StarterPlayerScripts, which runs only once per session.
Learn core Roblox scripting and Luau programming concepts, including: roblox PlayerAdded CharacterAdded tutorial, PlayerAdded not firing roblox, CharacterAdded not firing, roblox run code every respawn, roblox player join script.