Copies this lesson plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.
Module 01 · Lesson 04
Loops: for, while, and when to stop
If you copy-paste the same line over and over, you probably need a loop. A loop runs a block of code again and again until you stop it. In FiveM, knowing when to stop matters more than anywhere else you have coded, because a loop that never yields does not just slow down, it freezes the entire server thread. By the end you will know for loops, while loops, break, and the one habit, Citizen.Wait, that prevents your first crash.
You'll build
A qu_loops resource with three commands: a numeric for countdown, a while countdown, and a safe ticking thread that uses Citizen.Wait.
Time
~22 minutes
You need
Lessons 01–03 done. A working FiveM server you can restart.
You'll learn
Numeric for -> step values -> while loops -> break -> off-by-one errors -> the FiveM Wait rule that stops your first server freeze
No lua54 'yes' line. As of June 2025 that directive is deprecated and ignored: Lua 5.4 is the only Lua runtime, so you leave it out.
4
Write the lesson code
The topic is now represented by runnable code.
Open server.lua and paste this:
lua
-- 1. Numeric for: counts 5, 4, 3, 2, 1 then launches.RegisterCommand('countdown', function()for i = 5, 1, -1 do print('[qu_loops] ' .. i)endprint('[qu_loops] Launch!')end, true)-- 2. while + break: stops early the first time it hits 3.RegisterCommand('whilecount', function()local i = 5while i > 0 do if i == 3 then print('[qu_loops] hit 3, stopping early') break end print('[qu_loops] while ' .. i) i = i - 1endend, true)-- 3. A ticking thread that YIELDS. Citizen.Wait stops it freezing the server.RegisterCommand('tick', function()Citizen.CreateThread(function() for tick = 1, 3 do print('[qu_loops] tick ' .. tick) Citizen.Wait(1000) -- yield 1 second between ticks end print('[qu_loops] thread done')end)end, true)
5
Start and test it
The expected proof appears in the correct console.
Open server.cfg and add this line:
text
ensure qu_loops
Save, then run:
text
restart qu_loops
Run this test in the txAdmin Live Console:
text
countdown
Then run whilecount and tick to see the other two loops. The tick output appears one line per second, not all at once, because the thread yields between each print.
You ran it and the numbers fell. Now take the loops apart so the next time you write one you are deciding, not copying. Three commands, three loop shapes, and one rule that is unique to FiveM and will eventually save your server.
A numeric for is the loop you reach for when you know exactly how many times to run. The header has three numbers separated by commas, and reading them in order is the whole trick:
5 is the start. The counter variable i begins at 5.
1 is the stop. The loop keeps running as long as i has not passed 1. Lua includes the stop value, so 1 itself does run. This is an inclusive range, which is why you see five lines, not four.
-1 is the step. After each pass, Lua adds the step to i. A negative step counts down, so i goes 5, 4, 3, 2, 1. Leave the step out, like for i = 1, 5 do, and Lua assumes +1, counting up.
The variable i only exists inside the loop. You do not declare it with local and you do not change it by hand. Lua owns the counter and moves it for you. That is the difference between a for and a while: a for manages its own counter, a while does not. After the five numbers print, the loop is finished and control falls through to the print('[qu_loops] Launch!') line, which sits outside the loop body and runs exactly once.
local i = 5while i > 0 do print('[qu_loops] while ' .. i) i = i - 1end
A while loop runs as long as its condition is true. Here the condition is i > 0. Before each pass Lua checks it. While it holds, the body runs. The moment it is false, the loop stops and the program moves on.
Nothing moves the counter for you here. You set i = 5 yourself, and the line i = i - 1 is what makes the loop end. Delete that line and i stays 5 forever, the condition i > 0 is always true, and you have an infinite loop. On the client that stutters the game. On the server, as you will see in a moment, it freezes everything.
This is also where off-by-one errors live. The condition i > 0 means the loop runs for 5, 4, 3, 2, 1 and stops before printing 0. If you had written i >= 0 it would also print 0, one extra pass you did not want. If you decremented before printing instead of after, you would skip 5 and start at 4. The fix is always the same: read the condition out loud and count the first and last value by hand. A while gives you full control, and full control means the bug is yours to make.
while i > 0 do if i == 3 then print('[qu_loops] hit 3, stopping early') break end print('[qu_loops] while ' .. i) i = i - 1end
Sometimes you want out before the condition naturally goes false. break does exactly that: it stops the nearest loop immediately and jumps to the first line after it. In the whilecount command, the loop is built to run from 5 down to 1, but the if i == 3 check fires break on the third pass, so you only ever see while 5, while 4, then the stop message. The numbers 2 and 1 never print. break works the same inside a for loop. It is how you write loops that scan for something and quit the instant they find it, instead of grinding through every remaining item.
This is the part of the lesson nothing else teaches you. A loop that never pauses is fine in plain Lua. In FiveM it is a weapon pointed at your own server.
FiveM runs your script on a shared thread. When your code is running, nothing else on that side gets a turn: not other resources, not player connections, not the next frame. A normal loop like the countdown finishes in microseconds and hands the thread back, so you never notice. But a loop that runs forever never hands it back. This is the broken version, do not paste it into a live server:
lua
-- BROKEN: never yields. Freezes the entire server thread.Citizen.CreateThread(function() while true do print('[qu_loops] spinning') endend)
while true is always true, so this loop never ends on its own, and there is no break. It prints as fast as the CPU allows and never gives the thread back. On the server that means every player times out and the whole server appears to hang. The fix is one line, the tick command you already built:
lua
Citizen.CreateThread(function() for tick = 1, 3 do print('[qu_loops] tick ' .. tick) Citizen.Wait(1000) -- hand the thread back for 1 second endend)
Citizen.Wait(ms) is the yield. It pauses your loop for the given milliseconds and, crucially, hands the thread back to FiveM so everything else can run. After the wait, your loop picks up where it left off. The rule is absolute: any loop that could run forever must call Citizen.Wait somewhere inside it.Citizen.CreateThread is what lets a loop keep living after the command handler returns, which is exactly why an unyielded one is so dangerous, it would spin forever with no command to stop it.
On the client, the loop you write most often is a per-frame loop with Citizen.Wait(0), which yields for the shortest possible time and resumes next frame. That is how you draw a marker or watch for a key press sixty times a second. The cost is paid on one player's machine.
On the server there are no frames and there is no per-player machine. A server loop runs once for everyone, so a server-side Citizen.Wait(0) is far more expensive than it looks: it wakes up constantly on the one thread the whole server shares. Server loops should yield generously, Citizen.Wait(1000) or more, and most server logic should not poll in a loop at all. It should react to events, which is the next module. Use a for to process a known list, use a while with a real exit condition, and reach for an always-running thread only when you genuinely need a heartbeat.
That trailing true is the restricted flag, the same one from Lesson 01, and it is worth naming clearly here because it is a silent gotcha. With true, the command requires an ace permission to run. The server console has full rights, so countdown works the instant you type it there. But a connected player typing /countdown in their chat box has no such permission by default, so for them the command does nothing and gives no error. That silence is the trap: the command is not broken, it is gated. If you wanted any player to run it, you would pass false instead, and to grant a specific admin the right while keeping true, you would add an ace rule in server.cfg. For a server-side proof you do not want random players firing, true is the safe default.
✓You write a Citizen.CreateThread with while true inside and no Citizen.Wait, then restart the resource. What happens, and what is the one-line fix?
The loop never yields, so it holds the shared server thread forever. Other resources stop responding, player connections time out, and the server appears frozen, it does not crash with a clean error, it just hangs. The fix is to add a Citizen.Wait(ms) call inside the loop so it hands the thread back on every pass. Any loop that can run forever, especially a while true, must yield with Citizen.Wait. A finite loop like the for countdown does not need a Wait because it ends on its own in microseconds.
Check the folder is inside resources and the ensure line uses the exact same name.
The server hangs or all players time out after restart
A loop is missing its Citizen.Wait. Find any while true or unbounded while and add Citizen.Wait(ms) inside it, then restart qu_loops.
The loop prints one too many or one too few lines
Off-by-one. Read the for range or the while condition and count the first and last value by hand. Remember a for stop value is inclusive and the default step is +1.
whilecount prints forever and never stops
The counter is not changing. Make sure i = i - 1 is inside the while body so the condition eventually goes false.
A player typing /countdown in chat sees nothing happen
The restricted flag is true, so only the server console and aces may run it. Run countdown from the txAdmin Live Console, or pass false to RegisterCommand to open it to players.
No server needed for this part. Edit the Lua below and press RUN to execute it in your browser on real Lua 5.4. This is the same countdown from the countdown command, the for i = 5, 1, -1 you just dissected. The sandbox has no FiveM around it, so Citizen.Wait, Citizen.CreateThread, and RegisterCommand are not available here. The plain for loop, print, and string joining run exactly as they would on your server.