PlayDeck
Home / Course / Performance & Security
Track A · Module 11

Performance & Security

A script can work perfectly and still get you banned from a server's resource list. If it eats 8 ms every frame, the owner drops it. If it trusts the client, a cheater drains the economy. This module turns "it runs" into "it ships": fast enough to live on a real server, and locked down so players can't lie to it.

The number that decides everything: resmon

Open your server console (F8 in-game on a server you own) and type resmon. You get a live table of every resource and its CPU time in milliseconds. That column is the whole game. The unwritten rule across the FiveM scene is simple: idle resources should sit near 0.00–0.01 ms, and even while actively doing something a single script should stay under ~4 ms. Cross 4 ms and you cause frame hitches; a few greedy scripts stacking up is why servers feel laggy.

resmon tells you which resource is slow. To find which line is slow, FiveM ships a profiler: run profiler record 500 to capture 500 frames, then profiler view to open a flame-graph in your browser. The wide bars are your problem. Don't guess at performance — measure, fix, measure again. AI assistants love to "optimize" code that was never slow; resmon is how you check whether the change actually moved the number.

Kill per-frame work

The number one cause of a hot resource is a thread that runs every single frame for no reason. In Lua for FiveM, a loop with Wait(0) runs ~60+ times a second. That's fine for one frame of work — it's a disaster when it's drawing a marker or checking distance forever.

Two classic offenders:

The fix is adaptive sleep: when nothing's nearby, sleep a long time (e.g. 1000 ms). When the player is close, tighten the loop to Wait(0) so the marker is smooth. Same logic, a fraction of the cost.

-- client.lua — adaptive marker loop. Idle cost stays near 0.00 ms on resmon.
local shops = {
    { coords = vec3(25.7, -1345.3, 29.5), label = "24/7 Store" },
}

local DRAW_RADIUS  = 15.0  -- start drawing the marker within this range
local INTERACT_RADIUS = 2.0 -- close enough to actually open the shop

CreateThread(function()
    while true do
        -- Default to a long sleep. We only shorten it if something is near.
        local sleep = 1000
        local ped = PlayerPedId()
        local pos = GetEntityCoords(ped)

        for _, shop in ipairs(shops) do
            local dist = #(pos - shop.coords) -- vector length = distance
            if dist < DRAW_RADIUS then
                -- Player can see it: we MUST run every frame to draw smoothly.
                sleep = 0
                DrawMarker(2, shop.coords.x, shop.coords.y, shop.coords.z,
                    0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                    0.3, 0.3, 0.3, 30, 144, 255, 120,
                    false, true, 2, false, nil, nil, false)

                if dist < INTERACT_RADIUS and IsControlJustReleased(0, 38) then
                    -- E pressed near the shop. lib.callback asks the SERVER,
                    -- which is the only place we trust the answer.
                    local ok = lib.callback.await('playdeck:openShop', false, shop.label)
                    if ok then exports.ox_inventory:openInventory('shop', shop.label) end
                end
            end
        end

        Wait(sleep) -- long when far, 0 when close
    end
end)

Better still: for static interaction points, skip the loop entirely and register the zone with ox_target or ox_lib points. Those systems share one optimized loop across every resource, so your script's idle cost drops to basically nothing. Link to the official ox_target and ox_lib docs and use their addBoxZone / lib.points APIs instead of rolling your own — that's the modern, ox-first way.

Debounce your database writes

Saving to MySQL on every tiny change will tank your server under load. If a player's position or inventory saves 60 times a second, you've built a DDoS against your own database. Debounce: collect changes and flush them on a timer or on meaningful events (logout, periodic autosave) instead of every frame.

-- server.lua — flush dirty players every 5 minutes, not every change.
local dirty = {}

function MarkDirty(src) dirty[src] = true end -- call this when data changes

CreateThread(function()
    while true do
        Wait(5 * 60 * 1000) -- 300,000 ms = 5 minute autosave window
        for src in pairs(dirty) do
            SavePlayer(src) -- one batched write per player, not per change
            dirty[src] = nil
        end
    end
end)

Server-side validation is the only real security

Here's the rule that separates hobby scripts from shippable ones: the client always lies. Anything a player's game sends you — coordinates, item counts, prices, "I'm allowed to do this" — can be forged with a menu cheat. The client is for display and input. The server is for deciding.

That's why the example above calls lib.callback.await('playdeck:openShop', ...). The client asks; the server decides and re-checks everything:

-- server.lua — never trust the client's claim. Verify it yourself.
lib.callback.register('playdeck:openShop', function(source, shopLabel)
    local ped = GetPlayerPed(source)
    local pos = GetEntityCoords(ped)
    -- Re-check distance SERVER-SIDE so a cheater can't open a shop from across the map.
    if #(pos - vec3(25.7, -1345.3, 29.5)) > 3.0 then return false end
    return true
end)

This is where AI help goes wrong most often. Code assistants happily trust a client-sent price or item id, mix QBCore and ESX exports in one file, or invent natives that don't exist. Pin one framework (Qbox/QBCore or ESX), verify every native and export against the official FiveM reference and your PlayDeck sandbox, and put every decision that affects money, items, or permissions on the server.

Practice

In the PlayDeck browser sandbox, paste a loop that runs print("tick") inside Wait(0) and watch the simulated tick counter explode. Now change it to the adaptive pattern: start with local sleep = 1000, and only set sleep = 0 when a mock distance variable (set it to 1.5) is under 5.0. Re-run and watch the tick rate collapse. Then flip the distance to 50.0 and confirm it idles. You just felt the difference resmon would show — no FiveM install required.

Recap

Learn it by building it

PlayDeck is an original course on building GTA roleplay scripts with AI — Lua, frameworks, NUI, debugging, and a browser sandbox to test every lesson without booting the game.

Browse the course