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.

lua
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

lua
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)
  1. 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.
  2. Wait(0) is needed right after defer(). Calls to update, done or presentCard in the same tick as defer may not work.
  3. deferrals.update(message) replaces the text the player sees on the connecting screen. Use it to show progress.
  4. 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 source into a local at the very start. After Wait or an HTTP request, the global source can point to another player.

Rejecting a player

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

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

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

lua
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 done twice, or calling update after done. Use return to 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.

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

Scripts that skip this problem

Tebex TemplateA code-free premium theme for your Tebex store, edited entirely from the Tebex panel.View script →Mic PhoneA foldable phone that unfolds into a tablet and carries onto a player's real phone.View script →

Keep reading