START HERE·THINK LIKE A DEVELOPER·Verified June 2026 · Lua 5.4 · ox_lib 3.x
Learning with an AI assistant?
Copies this whole lesson - every step, code block, and the exact console errors - plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.
Start Here · Think like a developer

Performance and project structure

Two small habits separate a server that runs clean from one that stutters for everyone the moment it gets busy: writing cheap loops and keeping tidy files. Neither is advanced. Both are decisions you make while you type, and once they are habits you never think about them again. This is the last lesson of the section, the on-ramp before the paid Tracks. Track B teaches deep profiling hands-on. Here you build the instincts that mean you rarely need it.

You'll learn
Why every script shares one frame budget, the loop rule that keeps a server smooth, how to check cheap things first, how to read resmon, and how to lay out a resource so it stays sane to work on.
Time
~10 minutes
Difficulty
Beginner
You need
Nothing to install. You should already understand a CreateThread loop and a Wait, from the earlier loops and threads lessons.

Watch

RED: keeping your scripts fast.
BEFORE YOU START

Why performance matters in FiveM

Picture the server as a single stage with a strict clock. Every frame, that clock gives out a small slice of time, and every running script has to share it. The game itself needs some. Your scripts need some. The scripts other people downloaded need some. There is no separate lane for each one. They all draw from the same slice.

That changes how you think about your code. On a normal program, a slow function just makes itself slow. On a FiveM server, a greedy script makes everyone slow, because the time it hogs is time no other script gets. One badly written loop, sitting in one resource, can drop the frame rate for fifty players who never installed it. That is why "it works on my machine with one script" is a trap. The test is whether it stays cheap when forty other scripts are running beside it.

The good news is that the fix is almost always the same small idea, repeated. You will see it three times in this lesson.

Vocabulary

thread
A background loop your script starts with CreateThread. It runs on its own, over and over, until the resource stops. A server can have hundreds of these alive at once.
Citizen.Wait
The line inside a loop that pauses it and hands the time slice back to the engine. Citizen.Wait(500) means "leave me asleep for about 500 milliseconds." You can also write it as plain Wait; they are the same function.
tick
One pass of the shared clock. On a client running at 60 frames per second that is about 16 milliseconds. Each tick, the engine wakes up every thread that is due and lets it run.
resmon
The resource monitor. A live table that shows how many milliseconds of the tick each resource is eating, so you can find the greedy one instead of guessing.
hot path
Code that runs very often, like the body of a loop with a tiny Wait. A small cost in a hot path is multiplied by how often it runs, so this is where waste hurts.

The loop rule

Here is the one rule that prevents most beginner disasters. A loop that runs forever needs a Citizen.Wait inside it. No exceptions.

A while true do loop has no natural end. Without a Wait, it never pauses, so it never hands the time slice back. The engine hands the loop its turn and never gets it back, so it cannot draw the next frame or run anything else. The whole game freezes solid, not slows down, freezes, until the runtime kills your resource. This is the single most common way a beginner takes a server down.

lua
-- BAD: no Wait. This freezes the entire game the instant it starts.
CreateThread(function()
    while true do
        doSomething()
        -- nothing hands the frame back, so nothing else can run
    end
end)

The fix is one line. Add a Citizen.Wait inside the loop. And here is the habit on top of the rule: use the largest Wait you can get away with. The body of the loop only runs when the loop wakes up, so a bigger Wait means fewer wakeups, which means less work taken from the shared slice. A heartbeat that checks something once a second has no reason to wake up sixty times a second.

lua
-- GOOD: the loop pauses every pass, so the game keeps running.
CreateThread(function()
    while true do
        doSomething()
        Wait(1000) -- once a second is plenty for a slow check
    end
end)

A useful default is Wait(1000). Only drop lower when something genuinely needs to happen more often, like reading a key press that lasts a single frame. When in doubt, sleep longer. The cost of a thread is not how much code it holds. It is how often that code is allowed to run.

Cheap checks first

The second habit is about what you do inside the loop once it is awake. The idea is simple: do the cheap test before the expensive work, and bail out early when there is nothing to do.

Gate expensive work behind a cheap test

Most loops only need to do their real job in a specific situation: the player is near a shop, a menu is open, a flag is on. The trick is to check that cheap condition first, and only then pay for the expensive part. A distance check or a boolean flag costs almost nothing. Drawing on screen, reading from the database, or sending a network message costs a lot. Put the cheap guard in front so the expensive line almost never runs.

Do not compute or draw when no one is near

The classic example is anything tied to a place in the world. If the player is a kilometre away from your shop, there is no reason to draw its marker or check for a key press. So the loop sleeps long by default, and only speeds up and does the real work when the player is actually close.

lua
CreateThread(function()
    local shop = vector3(25.0, -1347.0, 29.5)
    while true do
        local wait = 1000 -- default: sleep a full second, do nothing
        local here = GetEntityCoords(PlayerPedId())
 
        if #(here - shop) < 20.0 then
            wait = 0 -- close enough to matter: now do the per-frame work
            -- draw the marker, watch for the key press, etc.
        end
 
        Wait(wait) -- far away the loop is nearly free; close, it earns its cost
    end
end)

The #(here - shop) part measures the straight-line distance between two points, so < 20.0 reads as "within 20 metres." Far away, the loop wakes once a second, runs one cheap subtraction, and goes back to sleep. Standing on the shop, it runs every frame, exactly when that is worth paying for. Same loop, two speeds, picked by one cheap check.

Measuring it

You do not have to guess which resource is greedy. resmon tells you. Open the in-game console with F8 and type:

text
resmon

You get a live table, one row per resource, showing how many milliseconds of the tick each one is eating. A resource that is asleep costs almost nothing. A resource doing real work every frame costs more. As a rough target, a healthy resource sits under about 0.5 ms while idle. If one row is sitting much higher than the rest while nothing special is happening, that is your suspect, and it almost always traces back to a loop with too small a Wait doing work it does not need to do.

Project structure

Tidy files are the other half of a smooth project. This is not about looking neat. It is about being able to find and change things six months from now without breaking something across the room.

Keep client, server, shared, and config in separate files

A resource usually has code that runs on the player's machine, code that runs on the server, values shared by both, and settings you tune. Put each in its own file: client.lua, server.lua, shared.lua, and config.lua. When you go looking for "the thing that decides who gets paid," you know it lives in server.lua, because money decisions must be made on the server. The split is also a safety habit. Secrets and trusted logic stay in server.lua, where the player cannot see or edit them.

Name things clearly

A name is a tiny piece of documentation. Config.ShopRadius tells you what it is the instant you read it; r makes you go hunting. Give resources, files, and variables names that say what they hold. Future you, reading this at midnight, is the person you are being kind to.

One resource equals one feature

Resist the urge to pile everything into one giant resource. One resource should do one job: a fuel system, a fishing job, a phone. When a feature is its own resource, you can restart it, update it, or delete it without touching anything else. A single resource that does ten things is ten things that all break together.

Common beginner mistakes

SymptomFix
while true do with no Wait freezes the whole serverThe loop never hands the frame back, so nothing else can run. Add a Citizen.Wait inside every while-true loop. This is the rule with no exceptions.
Running a heavy check every frame instead of every secondA loop on Wait(0) doing distance maths or drawing burns the shared slice 60 times a second for no reason. Raise the Wait, and gate the expensive part behind a cheap distance or flag check so it only runs when it matters.
One giant file with the whole resource in itSplit into client.lua, server.lua, shared.lua, and config.lua. Keep trusted logic on the server, keep one resource to one feature, and you can change one part without breaking the rest.
What single line keeps a while-true loop from freezing the server?

A Citizen.Wait inside the loop body, for example Wait(500). It pauses the loop and hands the time slice back to the engine, so the game can draw the next frame and run every other thread. Without it, the loop never yields, the engine never gets its turn back, and the whole game freezes until the runtime kills the resource. Bigger Wait values are cheaper, so use the largest one the job allows.

Try it yourself

What you can do now

  • Explain why every script shares one frame budget, so one greedy loop lags everyone on the server.
  • State the loop rule: every while-true loop needs a Citizen.Wait, and you use the largest Wait the job allows.
  • Gate expensive work behind a cheap check, like a distance or a flag, so you only pay when there is real work to do.
  • Open resmon, read a resource's ms cost, and treat under about 0.5 ms idle as a healthy target.
  • Lay a resource out with separate client, server, shared, and config files, clear names, and one feature per resource.

You now think like a developer: you weigh what your code costs the shared clock, and you keep your files in a shape you can grow into. Next you put it all together and build a complete resource. The next module is "Ship it," and it opens with the capstone where everything you have learned becomes one working thing you made.