How to run join code for players already in the server

A script that waits before connecting PlayerAdded misses players who join during the wait. How to connect first and handle existing players too.

warningLuaudit check: late-join-handlers

What it looks like

A server script waits for something before it connects Players.PlayerAdded: a data store read, a WaitForChild, a task.wait, or a require of a module that does one of those while it loads. Players who join during that wait are already in the server when the handler connects, and the script never runs its join code for them because it doesn't go through Players:GetPlayers() as well.

Luau
local season = config:GetAsync("Season")

Players.PlayerAdded:Connect(setUpSeason)

What an exploiter could do

Players who join while the script waits skip whatever the join code does. Depending on that code, they keep last season's progress and can claim this season's rewards with it, skip a ban check and play while banned, never get their data loaded so nothing they earn is saved, or never get the perk they paid for. A player who notices can try to repeat it by joining servers as they start.

How to fix it

  • Connect PlayerAdded before anything that yields, then run the same function for every player in Players:GetPlayers().
  • If the join code needs what the script waits for, connect first and have the handler wait for it, or start the wait before connecting and run the join code for existing players once it's done.
  • Make the join code safe to run twice for one player, since a player can be picked up both ways in rare cases.

Vulnerable example

Vulnerable
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local PlayerData = require(script.Parent.PlayerData)

local events = DataStoreService:GetDataStore("Events")
local ok, current = pcall(events.GetAsync, events, "Current")
local eventId = if ok and typeof(current) == "number" then current else 0

Players.PlayerAdded:Connect(function(player)
	local profile = PlayerData.waitFor(player)
	if profile and profile.Progress.EventId ~= eventId then
		profile.Progress.EventId = eventId
		profile.Progress.EventPoints = 0
	end
end)

Fixed example

Fixed
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local PlayerData = require(script.Parent.PlayerData)

local events = DataStoreService:GetDataStore("Events")
local ok, current = pcall(events.GetAsync, events, "Current")
local eventId = if ok and typeof(current) == "number" then current else 0

local function startEvent(player: Player)
	local profile = PlayerData.waitFor(player)
	if profile and profile.Progress.EventId ~= eventId then
		profile.Progress.EventId = eventId
		profile.Progress.EventPoints = 0
	end
end

Players.PlayerAdded:Connect(startEvent)
for _, player in Players:GetPlayers() do
	task.spawn(startEvent, player)
end

How to spot this in your own scripts, and the cases that only look like it, are part of the Luaudit check your AI runs.