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:

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:

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:

-- 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

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.

Related pages