Var FiveM
ScriptsBundlesSubscriptionsDocs
VAR
Var FiveM
ScriptsBundlesSubscriptionsDocs
Theme CustomizerAboutContact
Shop Now
GUIDE

~/guides/fivem-server-optimization

FiveM Server Optimization

-- setup · updated august 2026 · var fivem team
-- 16.6 ms frame budget · 0.4 ms down to 0.05 ms · onesync culling at 424 units

Most servers are not slow because of hardware, they are slow because four or five resources each spend a third of a millisecond per frame doing work that could happen twice a second instead. This guide shows how to measure that with resmon and the profiler, what the numbers actually mean, the exact Lua patterns that cause them, and the server side settings that decide how much the server has to sync in the first place.

-- index

  1. 01Reading resmon
  2. 02What is an acceptable ms
  3. 03The expensive Lua patterns
  4. 04Server hitches and the profiler
  5. 05OneSync, culling, buckets
  6. 06Streaming and assets
  7. 07Database and oxmysql
  8. 08A diagnosis that works
  9. 09FAQ

Reading resmon without guessing

Everything starts in the client console. Press F8, type resmon and you get the resource monitor, which the Cfx.re docs describe as monitoring CPU usage in milliseconds and memory usage for each resource. Two columns matter. The millisecond column is how long that resource spent executing during the last tick, and the memory column is how much it holds. Only the first one causes stutter.

Read it while playing, not while standing in the spawn menu. A marker script costs nothing in an empty field and costs everything in a downtown block with forty registered points in range. Walk your busiest zone, open a menu, drive fast through the city, and watch which rows climb. Resmon is a client tool, so it only sees client scripts: server side cost is a different measurement covered further down.

CommandSideWhat it shows
resmonclientCPU milliseconds and memory per resource, live.
netgraphclientPing, packets in and out, bytes in and out, routing delay.
strdbgclientWhat the GTA streamer is currently loading.
profiler record 500bothCaptures 500 frames of script execution.
profiler viewbothOpens the capture as a timeline in Chrome.
profiler saveJSON x.jsonbothWrites the capture next to your run script for later.

What counts as an acceptable ms

Cfx.re publishes no official pass or fail number, so ignore anyone who quotes one as gospel. The useful benchmark is arithmetic. At 60 FPS a frame lasts 16.6 milliseconds, and that budget covers the game engine, the renderer and every resource you loaded. A server running forty client scripts that each take 0.3 ms is spending more than 12 ms of the frame on Lua alone, which is why players report lag even though no single script looks alarming in isolation. The number that matters is the total, and the way to lower a total is to fix the three worst offenders.

Resmon readingInterpretation
0.00 to 0.01 msThe thread is asleep between ticks. Nothing to do.
0.02 to 0.10 msNormal for an active client script. Thirty of these still fit in a frame.
0.10 to 0.50 msA per-frame loop is doing real work. Check the Wait values before anything else.
0.50 to 1.00 msOne resource is taking around 3 to 6 percent of a 60 FPS frame on its own. Refactor.
above 1.00 msFour resources like this and players feel it. Rewrite the loop or replace the script.

The short version

No single resource is what makes a server feel slow. Forty client scripts at 0.3 ms each burn more than 12 of the 16.6 ms a 60 FPS frame gives you, and none of them looks alarming on its own. Drive the total down, and the only rows worth touching first are the three worst in resmon.

The Lua patterns that actually cost you

Three patterns account for most of what you will find. A loop running per frame over a full config table, distance measured with natives instead of vector math, and a drawing thread that keeps ticking when there is nothing on screen. Here is the version almost every free marker script ships with.

-- BEFORE: 500 config entries, distance natives, one tight thread
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)
        local ped = PlayerPedId()
        local coords = GetEntityCoords(ped)
        for _, v in pairs(Config.Markers) do
            local dist = GetDistanceBetweenCoords(
                coords.x, coords.y, coords.z,
                v.coords.x, v.coords.y, v.coords.z, true)
            if dist < 30.0 then
                DrawMarker(1, v.coords.x, v.coords.y, v.coords.z - 1.0,
                    0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                    1.5, 1.5, 0.5, 0, 155, 255, 100,
                    false, false, 2, false, nil, nil, false)
            end
        end
    end
end)

Two things are wrong. The whole table is walked sixty times a second even when the nearest point is two kilometres away, and every iteration calls GetDistanceBetweenCoords, which crosses the native boundary. CfxLua has first class vectors, so #(a - b) gives you the same distance in pure Lua. A well known community writeup measured that single substitution taking a marker resource from over 0.4 ms to 0.28 ms, then down to 0.05 ms once the thread was split and the render distance reduced.

-- AFTER: a slow scanner feeds a render thread that idles when empty
-- Config.Markers[i].coords must be a vector3 for #(a - b) to work.
local nearby = {}

Citizen.CreateThread(function()            -- runs twice per second
    while true do
        local coords = GetEntityCoords(PlayerPedId())
        local found = {}
        for _, v in pairs(Config.Markers) do
            local dist = #(coords - v.coords)
            if dist < 15.0 then
                v.dist = dist
                found[#found + 1] = v
            end
        end
        nearby = found
        Citizen.Wait(500)
    end
end)

Citizen.CreateThread(function()            -- renders only what the scanner found
    while true do
        local count = #nearby
        if count == 0 then
            Citizen.Wait(500)              -- nothing on screen, stop burning frames
        else
            for i = 1, count do
                local v = nearby[i]
                DrawMarker(1, v.coords.x, v.coords.y, v.coords.z - 1.0,
                    0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                    1.5, 1.5, 0.5, 0, 155, 255, 100,
                    false, false, 2, false, nil, nil, false)
            end
            Citizen.Wait(0)
        end
    end
end)

The other half of the same idea is the adaptive sleep. One thread, one variable, and the wait time changes with what the player is doing. This is the pattern to use for a shop prompt, a job point or anything where the interaction only exists inside a small radius. The marker types and sprite IDs you will pass to DrawMarker are catalogued in the FiveM blips and markers reference.

-- One thread, variable sleep: 1 tick per second far away, per frame up close
local SHOP = vector3(25.7, -1347.3, 29.49)

Citizen.CreateThread(function()
    while true do
        local sleep = 1000
        local coords = GetEntityCoords(PlayerPedId())

        if #(coords - SHOP) < 15.0 then
            sleep = 0
            DrawMarker(27, SHOP.x, SHOP.y, SHOP.z - 0.95,
                0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                0.9, 0.9, 0.9, 0, 155, 255, 120,
                false, false, 2, false, nil, nil, false)

            if #(coords - SHOP) < 1.6 then
                BeginTextCommandDisplayHelp("STRING")
                AddTextComponentSubstringPlayerName("Press ~INPUT_CONTEXT~ to open the shop")
                EndTextCommandDisplayHelp(0, false, true, -1)

                if IsControlJustReleased(0, 38) then   -- E
                    TriggerServerEvent("var:shop:open")
                end
            end
        end

        Citizen.Wait(sleep)
    end
end)

Three rules cover the rest. Cache anything you call more than once per frame, so PlayerPedId() and GetEntityCoords go in a local at the top of the tick, never inside the inner loop. Register text entries and load animation dictionaries once on resource start, not on every frame. And when a script needs to watch many models at once, put them all on one shared scanner thread rather than spawning a thread per registration, which is how Var-Interact handles hash based registrations: squared distance checks, a single configurable scanner interval, and prompts that only arm inside range.

Server hitches and the profiler

Resmon never sees the server. When a server side resource takes too long inside one tick, FXServer prints a thread hitch warning telling you the main thread stalled and for how many milliseconds. The Cfx.re fact sheet is blunt about what that means: a hitch warning indicates that one of your resources is not performing as it should. A handful during startup is normal. Repeated hitches with players connected are exactly what your community experiences as rubber banding and delayed events.

CriterionClient sideServer side
What players reportFrame drops and stutter, worse in busy zonesRubber banding, delayed events, actions that fire late
Where you measure itF8 console: resmon, netgraph, strdbgServer console warnings, then profiler record 500
Visible in resmonyesno
The number you readMilliseconds per resource, per tickHitch warning: how long the main thread stalled
Usual causeA per-frame loop walking a full config tableA Wait that is too short, one query per player in a tick, an HTTP call with no callback
Still happens with nobody onlinenoyes
Resmon never sees the server. Quiet client rows next to a console printing hitch warnings means you are measuring the wrong half of the problem.

The profiler turns the warning into a file and a line number. Run the capture while the console is producing warnings, then open it. Chrome is required for the viewer. Hovering a spike in the CPU graph reveals the resource tick, the millisecond cost and the source location responsible.

profiler record 500          # capture 500 frames
profiler status              # confirm it is recording
profiler view                # open the timeline in Chrome
profiler saveJSON hitch.json # or keep it for later

While you are on the server, check what build you are running. Cfx.re promotes vetted builds to the Recommended channel and publishes master branch builds to Latest as soon as CI passes, and an old artifact carries scheduler bugs that were fixed months ago. Update to the newest Recommended build, restart, and re-measure before blaming a script. If you have not set the server up yet, the full installation walkthrough covers artifacts, txAdmin and server.cfg from zero.

OneSync, culling and routing buckets

OneSync decides how much work the server does before any of your Lua runs. Legacy mode caps out at 48 slots. Infinity raises the object ID pool from 8192 to 65535 and culls entities outside a focus zone of roughly 424 units around each player, which is what makes counts above 64 possible at all. The convars below are the ones that matter, and the two marked as startup only cannot be changed with a live restart of the resource.

# 1 to 2048. Values from 32 need onesync on or legacy, above 64 needs onesync on.
sv_maxClients 64
onesync on

# Startup only. Leave both true unless you know exactly why not.
set onesync_enableInfinity true
set onesync_population true

# Sync throttling: drop far entities, refresh near ones more often.
set onesync_distanceCulling true
set onesync_radiusFrequency true

Routing buckets are the underused half. A bucket is a separate synchronisation namespace: players inside one only see entities and players in the same bucket. Put a heist instance, a race or a test drive in its own bucket and the server stops broadcasting that activity to the other 60 players. Turning off ambient population inside the bucket removes the traffic and pedestrians nobody in an interior needs.

-- server side: isolate a crew, no ambient population, strict entity lockdown
local BUCKET = 42

for _, playerId in ipairs(crew) do
    SetPlayerRoutingBucket(playerId, BUCKET)
end

SetRoutingBucketPopulationEnabled(BUCKET, false)
SetRoutingBucketEntityLockdownMode(BUCKET, "strict")

Lockdown mode accepts strict (clients create nothing), relaxed (script owned entities blocked) and inactive (unrestricted). On a roleplay server, relaxed on the default bucket also removes an entire category of entity spawning exploits.

Streaming, assets and the client side of lag

When players report stutter that resmon cannot explain, it is usually the streamer, not Lua. Type strdbg in the F8 console to see what the GTA streamer is loading in real time. Textures are the usual culprit: a single vehicle shipped with 4K uncompressed YTD files can weigh more than thirty properly compressed ones, and every player pays that cost in load time and in memory. Compress vehicle and clothing textures, delete the duplicate props that three different MLO packs all stream, and split one enormous stream folder into per resource folders so you can disable them one at a time while testing. The add-on vehicle pipeline and its meta files are covered in the guide on adding cars to FiveM.

Two more client side wins. Run netgraph to separate a rendering problem from a network problem: if ping is stable and bytes in are low while frames still drop, the fault is local. And audit your loading screen and NUI resources, because a browser page left running behind the game costs frames for the entire session, not just during the load.

Auditing what the game streams

  1. 01Watch the streamerType strdbg in the F8 console to see what the GTA streamer is loading in real time, while a player drives the zone that stutters.
  2. 02Compress the texturesOne vehicle shipped with 4K uncompressed YTD files can weigh more than thirty properly compressed ones, in load time and in memory.
  3. 03Delete the duplicate propsThree different MLO packs commonly stream the same props. Every player downloads all three copies.
  4. 04Split the stream foldersOne folder per resource instead of a single enormous one, so you can disable them one at a time while testing.
netgraph settles the last question: if ping is stable and bytes in are low while frames still drop, the fault is local and none of the four steps above will help.

Database: where server ticks go to die

Most hitch warnings on a mature server trace back to the database. The classic shape is a save loop that fires one query per online player inside a single tick, every sixty seconds. With 60 players that is 60 round trips queued at once, and the main thread waits.

-- BEFORE: one query per player, every minute, all in the same tick
CreateThread(function()
    while true do
        Wait(60000)
        for _, playerId in ipairs(GetPlayers()) do
            local xPlayer = ESX.GetPlayerFromId(playerId)
            MySQL.update.await("UPDATE users SET money = ? WHERE identifier = ?", {
                xPlayer.getMoney(), xPlayer.identifier
            })
        end
    end
end)

oxmysql exposes MySQL.prepare, which executes a frequently repeated statement faster and accepts multiple parameter sets in a single call. Build the parameter list first, send it once, and stretch the interval: a money save every five minutes plus a save on disconnect loses nothing a player will notice.

-- AFTER: one prepared statement, one round trip, a saner interval
CreateThread(function()
    while true do
        Wait(300000)
        local params = {}
        for _, playerId in ipairs(GetPlayers()) do
            local xPlayer = ESX.GetPlayerFromId(playerId)
            params[#params + 1] = { xPlayer.getMoney(), xPlayer.identifier }
        end
        if #params > 0 then
            MySQL.prepare("UPDATE users SET money = ? WHERE identifier = ?", params)
        end
    end
end)

Two adjacent fixes. Prepare only accepts positional ? placeholders, so named and column placeholders will throw. And index the columns you filter on: an inventory table without an index on the identifier column turns every lookup into a full table scan, which is invisible with 200 rows and fatal with 200,000.

A diagnosis that actually converges

Change one thing at a time and measure between changes, otherwise you will never know what helped. The order below works because it separates the two halves of the problem before touching any code.

01Split client from serverOpen resmon with players online. If the client rows are quiet but the console prints hitch warnings, stop looking at Lua rendering and go straight to the profiler.
02Rank, do not sampleSort by milliseconds and write down the top five with their values. This is your baseline. Everything after this is measured against it.
03Bisect by stopping resourcesStop the worst offender, reconnect, re-measure. If the total drops by more than that resource was reporting, it was also causing work elsewhere.
04Read the loop before rewriting itNine times out of ten the fix is a Wait value and a distance check, not an architecture change. Apply the split thread pattern first.
05Re-measure in the same placeSame zone, same time of day, same player count. A number taken in an empty field proves nothing.

The uncomfortable conclusion of most audits is that the cheapest performance win is deleting a script nobody uses. A server with 25 well written resources beats a server with 90 resources of which 60 are free scripts installed once and never removed.

Frequently asked questions

What is a good resmon ms value in FiveM?

Cfx.re does not publish an official threshold, so the only honest benchmark is arithmetic: at 60 FPS a frame lasts 16.6 ms, and every client resource shares that budget with the game itself. In practice a resource sitting under 0.05 ms while idle and under 0.10 ms while active leaves room for thirty other scripts. Anything parked above 0.50 ms is consuming roughly 3 percent of every frame on its own and deserves a rewrite or a replacement.

Why does my FiveM server lag with no players online?

Lag with an empty server is almost always server side, not client side. Look for hitch warnings in the console: they tell you the server main thread stalled and by how many milliseconds. The usual causes are a scheduled loop with a Wait value that is too short, a database save running for every player in a single tick, and HTTP requests made without a callback. Run profiler record 500 while the console is spamming warnings and the trace will name the resource.

Does Citizen.Wait(0) cause lag in FiveM?

Citizen.Wait(0) is not a bug by itself, it just means the loop runs once per rendered frame, which is exactly what you want for drawing. It becomes a problem when a per-frame loop does work that does not need to happen per frame, like iterating five hundred config entries or calling distance natives. The fix is to split the thread: a slow scanner every 250 to 500 ms decides what is nearby, and the per-frame thread only draws that short list.

How do I find which resource is causing lag on my FiveM server?

On the client, press F8 and type resmon to see CPU milliseconds and memory per resource, sorted so the expensive ones float to the top. On the server, watch for thread hitch warnings, then run profiler record 500 followed by profiler view to open the capture in Chrome, where you hover a spike to get the resource name, the file and the line number. Do one measurement at a time and change one thing between measurements.

Does OneSync Infinity improve server performance?

It changes what the server has to do rather than making existing scripts faster. Infinity extends the object ID pool from 8192 to 65535 and culls entities outside a focus zone of roughly 424 units around each player, so the server stops syncing things nobody can see. That is what makes counts above 64 players viable at all. It will not rescue a badly written resource: a script that stalls the main thread stalls it the same way at 300 players as at 3.

Should I run the latest or the recommended FiveM artifact?

Run the latest Recommended build on a live server. The Recommended channel is vetted by Cfx.re before promotion, while Latest publishes master branch builds as soon as CI passes and can ship regressions or broken natives. Both channels are listed at the top of runtime.fivem.net. Keep a Latest build on a test server if you develop scripts, never on the box your players connect to.

-- var-fivem.com

Scripts written to stay under budget

Every Var script ships with split scanner threads, vector distance checks and adaptive sleeps rather than a single per-frame loop over the whole config. Escrow, Partially Open and Open Source versions are available.

Browse the shopTry it live

Related: How to make a FiveM server · Best FiveM scripts 2026

Keep reading

  • setupFiveM Server Hosting: What You Actually Need in 2026Why per-core clock beats core count on FXServer, how much RAM your asset pack really needs, the bandwidth cost of first-join downloads, datacenter latency, VPS vs dedicated vs managed FiveM host, and real monthly costs read from vendor pages in August 2026.read
  • setupHow to Make a FiveM Server in 2026From zero to first spawn in about 20 minutes: the free license key, txAdmin setup with the official screenshots, ESX vs QBCore recipes, server.cfg explained line by line, real hosting costs and the launch checklist.read
  • listicleBest FiveM Scripts in 2026The scripts server owners actually buy in 2026: player-run supermarkets, coin shops, casinos, paintball and more, with selection criteria.read
VAR
Var FiveM

Premium FiveM scripts. Low resmon, high quality. Built for ESX, QBCore & Standalone.

28scripts
1,500+sales

Scripts

  • Marketplace
  • Bundles
  • Subscriptions
  • Theme Customizer

Most Popular

  • Supermarket Simulator
  • FiveM Casino Script
  • FiveM Coin Shop
  • FiveM Emote Menu
  • FiveM Paintball Script
  • FiveM Clothing Shop Script
  • FiveM Interaction Script
  • FiveM Character Creator

Resources

  • Free Scripts
  • Guides
  • Documentation
  • Support

Company

  • About
  • Contact
  • Discord

Legal

  • Terms
  • Privacy
  • Refunds

© 2026 Var FiveM. All sales final.

Payments byTebex