FiveM playerConnecting and deferrals: defer, update, done explained
How AddEventHandler('playerConnecting') works with deferrals: defer, Wait(0), update, done(reason), adaptive cards, and why players hang when you forget to call done.
You want to run something before a player is allowed in: a whitelist check, a ban list, a database lookup, a welcome card. The place for that is the playerConnecting event, and the tool is deferrals. Used wrongly, players sit on the connecting screen forever, so the details matter.
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
-- runs when a player starts connecting
end)What the arguments are
name: the player's name.setKickReason(reason): a function that rejects the connection with a message. It exists for simple, immediate rejections.deferrals: an object with functions that let you pause the connection and talk to the player while you work.
Inside the handler, source is the player's connection id.
The four deferral calls
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
local src = source -- save it now
deferrals.defer() -- 1. pause the connection
Wait(0) -- 2. let the engine register it
deferrals.update('Checking your account...') -- 3. show a message
-- do your checks here
deferrals.done() -- 4. let the player in
end)deferrals.defer()tells FiveM to hold the player until you decide. Without it, the player is let in as soon as your handler returns, even if you started an asynchronous task.Wait(0)is needed right afterdefer(). Calls toupdate,doneorpresentCardin the same tick asdefermay not work.deferrals.update(message)replaces the text the player sees on the connecting screen. Use it to show progress.deferrals.done(reason)ends the wait. No argument means the player is accepted. A string means they are rejected and see that string.
Tip: copy
sourceinto a local at the very start. AfterWaitor an HTTP request, the globalsourcecan point to another player.
Rejecting a player
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
local src = source
deferrals.defer()
Wait(0)
local license
for _, id in ipairs(GetPlayerIdentifiers(src)) do
if id:sub(1, 8) == 'license:' then license = id break end
end
if not license then
return deferrals.done('No Rockstar license found. Restart the game and try again.')
end
deferrals.done()
end)return deferrals.done(...) ends the handler at the same time, which keeps you from calling done twice.
Asynchronous checks
Deferrals are made for work that takes time: a database query or an HTTP call. Keep the connection deferred and call done in the callback.
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
local src = source
deferrals.defer()
Wait(0)
deferrals.update('Looking you up...')
local license = GetPlayerIdentifierByType(src, 'license')
MySQL.scalar('SELECT 1 FROM whitelist WHERE license = ?', { license }, function(found)
if found then
deferrals.done()
else
deferrals.done('You are not whitelisted.')
end
end)
end)GetPlayerIdentifierByType exists in current server builds. If your artifact is old, loop over GetPlayerIdentifiers instead, as above. See the oxmysql guide for the query syntax. A full example with Discord roles is in Discord whitelist for FiveM.
The common mistake: forgetting done
Players stuck on the connecting screen almost always mean that one code path never reaches deferrals.done.
-- Bad: nothing happens when the query returns no row
MySQL.single('SELECT * FROM bans WHERE license = ?', { license }, function(row)
if row then
deferrals.done('You are banned.')
end
end)If row is nil, the player is held forever. The fix is an else:
MySQL.single('SELECT * FROM bans WHERE license = ?', { license }, function(row)
if row then
deferrals.done('You are banned.')
else
deferrals.done()
end
end)Three other ways to get stuck:
- A script error inside the handler before
done. The handler stops and the player waits. Read the console. See reading a script error. - An HTTP call that never answers. Add a timeout, for example by tracking the start time and finishing with a message if nothing came back.
- Calling
donetwice, or callingupdateafterdone. Usereturnto leave the handler after each ending.
Several handlers at once
Many resources listen to playerConnecting, and each one that calls defer adds to the wait. The player is let in only when all of them finish. A slow or broken handler in one script holds everybody up, so test with only that resource first when connections hang.
Adaptive cards
deferrals.presentCard shows a form or a rules screen from an Adaptive Card, which is a JSON layout. The player can press a button, and your callback receives the result.
local card = {
type = 'AdaptiveCard',
version = '1.3',
body = {
{ type = 'TextBlock', text = 'Server rules', weight = 'Bolder', size = 'Large' },
{ type = 'TextBlock', text = 'Be respectful and no cheating.', wrap = true },
},
actions = {
{ type = 'Action.Submit', title = 'I agree', data = { accepted = true } },
},
}
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
deferrals.defer()
Wait(0)
deferrals.presentCard(json.encode(card), function(data)
if data and data.accepted then
deferrals.done()
else
deferrals.done('You must accept the rules.')
end
end)
end)Keep cards simple: text, an image and one or two buttons. The callback still needs to end in done.
Checklist
| Symptom | Fix |
|---|---|
| Player stuck on connecting | One path never calls deferrals.done; add the missing else |
update or done has no effect |
Call Wait(0) right after deferrals.defer() |
| Wrong player affected | Save local src = source first |
| Player let in before the check ends | You forgot deferrals.defer() |
| Reject with a message | deferrals.done('reason') |
| Accept the player | deferrals.done() with no argument |
| Hang only with many scripts | Another resource's playerConnecting handler is slow or broken |
Quick answers
Why do I need Wait(0) after deferrals.defer()?
The deferral is only active once the engine has moved on a tick. Calling deferrals.update or deferrals.done in the same tick can be ignored, so yield once with Wait(0) first.
How do I reject a player?
Call deferrals.done('your reason') with a string. The player sees the text and is not allowed in. Calling deferrals.done() with no argument lets them connect.
What happens if I never call deferrals.done?
The player stays on the connecting screen until the connection times out. Every code path in your handler must end in deferrals.done.

