Six of you have been on the test server for three weeks. Resmon is green, every resource idles around 0.01ms, and nothing has ever gone wrong. Then launch night arrives, ninety people pour through the queue, and by minute four the city is hitching every couple of seconds and the inventory takes nine seconds to open. Nothing changed except the number of humans in it. FiveM load testing is the practice of finding that cliff before your players do, and the annoying part is that the standard answer from every other kind of software, point a swarm of bots at it, mostly does not work here.
So this post covers what scales badly and why, the honest limits of load testing a FiveM server, and what you genuinely can stress without a single extra player: synthesising the work rather than the bodies, reading the numbers that matter, seeding a database that tells the truth, and testing the boot path under a full one. Then the staged real-player test, and the three things to have on your desk before you open the doors.
Why a server that's fine with six players falls over at ninety
Four things scale badly, and at small player counts all four are invisible.
Per-player loops. The common shape is a thread that walks every connected player on a tick and does a bit of work for each one. Six iterations, ninety iterations, whatever, the server can count. The problem is the nested version: for every player, check every other player. Six players is thirty six comparisons. Ninety players is eight thousand one hundred. That is not a gentle curve, that is a wall with a curve painted on the front of it.
Queries with no index. SELECT * FROM player_vehicles WHERE citizenid = ? against a table holding forty rows is instant whether or not an index exists, because MySQL reads the whole table faster than it could plan anything smarter. At sixty thousand rows the same unindexed query reads sixty thousand rows, and it does it every time somebody opens a garage. Your six testers had eleven cars between them. Your launch crowd will have hundreds by the end of week one.
Events that fan out. TriggerClientEvent('someEvent', -1, payload) serialises and sends to everybody. Six clients is six sends, ninety is ninety, and if the trigger is a common action like picking up an item, you are multiplying by ninety on an event that already fires too often. Hunt for broadcasts that could have been aimed only at the players who can see the thing.
Entity count. Parked vehicles that persist across restarts, dropped items that spawn real props, abandoned cars nobody despawns. Six players leave six cars in a car park. Ninety players leave a scrapyard, and the server syncs all of it forever. Check what sv_entityLockdown is set to while you are in there, because inactive on a public server is an invitation.
Why you cannot just throw bots at a FiveM server
The FiveM client is GTA V. There is no supported headless client, no official test harness, no --server-fill 100 flag hiding in the docs. That constraint is the whole reason FiveM load testing looks different from load testing a web app.
People try anyway. Running several GTA instances on one machine gets you four or five clients before your own box gives up, and what you measured was mostly your own box. Protocol-level fake clients exist in the wild, but they break on every server build and do not run your client scripts, so they exercise almost none of the code you care about.
Invert the problem instead. Stop simulating players and start simulating the work that players cause. Ninety humans are a machine for generating queries, events, entities and ticks. You can generate those directly, and unlike the humans they will do it at 3am on a Tuesday without asking for a staff role.
What you can stress test without a single player
Write a small resource full of admin-only commands that each do one horrible thing. Keep it out of your live resource list and ensure it manually when you want it.
Loop the queries. Take the five that run most often (garage list, inventory load, character select, bank balance) and run each five hundred times against a seeded database, timing every call. You want the worst single call and roughly where the top five percent sit, not just an average. Averages are where slow queries go to hide.
RegisterCommand('stress_garage', function(source)
if source ~= 0 then return end
local t = {}
for i = 1, 500 do
local start = os.clock()
MySQL.query.await('SELECT * FROM player_vehicles WHERE citizenid = ?', { 'TEST00001' })
t[#t+1] = (os.clock() - start) * 1000
end
table.sort(t)
print(('n=%d median %.1fms p95 %.1fms max %.1fms')
:format(#t, t[250], t[475], t[#t]))
end, true)
Fire the events at volume. Take your chattiest broadcast and trigger it a few thousand times in a burst while you sit in game watching the server console. You will learn quickly whether the payload is small enough to survive being sent ninety times a second.
Spawn the entities. Create two or three hundred vehicles server-side, scattered across the map, then go for a drive. Cheapest test on the list, and it catches a lot, because entity sync cost lands on the server thread and on every client at once.
Run the payday tick. Whatever your framework does on its money timer, run it manually against a seeded database of five hundred fake characters. A paycheck loop that does one query per player is fine at six. At five hundred it is five hundred queries in the same tick, and that is the shape of a lot of real launch-night hitching.
Reading resmon and server thread time without fooling yourself
Client-side resmon 1 gives you per-resource CPU cost and it is the thing everyone already knows about. It is also the least useful number during a load test, because most of what breaks under population breaks on the server.
The server console tells you directly. When the main thread takes too long it prints a hitch warning with the interval it stalled for, and those lines are the best signal you have. A tidy server produces none. One producing them every few seconds under synthetic load will produce them constantly under real load. txAdmin's performance chart shows the same story as a distribution over time, which beats scrolling back through console for spotting a slow creep.
For per-query visibility, oxmysql logs every query with its execution time when you set mysql_debug, and warns on slow ones by itself. Drop the warning threshold to something aggressive while you test, then put it back. You are trying to make the server complain, not have a peaceful evening.
set mysql_debug "true"
set mysql_slow_query_warning 50
profiler record 500 followed by profiler save writes out a trace you can open in a Chrome tracing viewer, which is how you find out the 40ms tick belongs to a resource you never suspected.
One caveat before you blame your code. FXServer leans hard on single-thread performance, so a cheap shared VPS with plenty of cores and mediocre per-core speed will produce hitch warnings no amount of Lua tuning will fix. Work out whether you are CPU-bound on the host before you rewrite anything, and if the answer is the box, the difference between a shared VPS and a dedicated game host is a more productive place to spend the afternoon.
Seed the database first, because 200 rows hides everything
This is the part people skip, and it invalidates most of the testing that happens in this hobby.
MySQL's optimiser makes different decisions at different table sizes. On a two hundred row table a full scan genuinely is the fastest plan, so the optimiser picks it, the query returns in under a millisecond, and you conclude the query is fine. It is not fine. It is small. Those are different things, and only one of them survives launch.
Before you profile anything, seed realistic row counts. Five hundred characters, a few thousand vehicles, tens of thousands of inventory rows carrying real JSON blobs rather than empty arrays, whatever your property tables look like after a month. Write the seeder as a script so you can regenerate it after every wipe.
Then run EXPLAIN on your hot queries. A type of ALL means a full table scan, and the rows column tells you how many rows it expects to touch. Turn on the MySQL slow query log with a low long_query_time while you stress, and read what falls out. Almost every time the fix is one index on the column you join or filter on, and the effect is measured in orders of magnitude rather than percentages. The full walkthrough of indexing and query tuning for FiveM databases covers the tables that go bad first.
Test the boot and restart path on a full database
An empty database boots in seconds. That tells you nothing, because you will never boot an empty database again after opening night.
Plenty of resources load their entire world state on start: every property, every vehicle, every gang territory, every stash. At six characters that query returns before the resource finishes printing its startup banner. At scale it is a real stall, and because it happens during startup the symptom is not a hitch, it is a server that sits there looking broken while everyone in Discord asks if it is down.
So restart against the seeded database and time it. Then do the harder version: kill the process without a clean shutdown and let it come back, because that is what actually happens on launch night. A restart is also a reconnection storm, ninety clients all doing character select and inventory load inside the same ninety seconds, a spike that never appears in steady-state testing. Run your query loop during a restart and watch what it does to the numbers.
The stress test that beats all the synthetic ones
Sixty real humans doing unpredictable things for an hour is worth more than everything above combined, and you can just organise one.
Invite your Discord, offer something for turning up (a priority queue slot, a starter bonus on the real launch, a role), and run a scheduled stress event a week before you open. The part people get wrong is treating it as a party. Give it a script. Send everyone to Legion Square at once. Have them open their inventories on a count of three, then spawn a vehicle each on the same signal, then scatter to Sandy so you can see what happens when the population thins out and then suddenly does not. Take a reading between stages.
Put one person on nothing but the console and the txAdmin chart. Scatter staff across districts to report what they see locally, because a client-side hitch in Paleto looks nothing like one downtown. Decide in advance what number ends the test and triggers a rollback, and honour it.
Launch night: what to watch and what to have ready
The last round of FiveM load testing runs live, with an audience. Watch four things. Server thread hitch warnings in the console. The slow query log. The join rate rather than the concurrent count, because a hundred people arriving over ten minutes is a different load from a hundred arriving over ninety seconds. And your entity totals creeping up as the night goes on.
Launch night is also when your server is most visible, which is when the attacks start, so have your DDoS mitigation already configured rather than discovering it as a topic at 11pm.
Three things sit on your desk before you open. A rollback you have rehearsed, meaning a database dump taken minutes before launch and a tagged commit of your resources folder, both restored once already so you know how long it takes. A way to move the slot cap in both directions, remembering sv_maxclients needs a restart, so your live lever is a queue resource that can gate the soft cap without one. And a person whose only job for three hours is watching graphs, not roleplaying, not fixing things, just reading numbers out loud.
Your first real launch is the load test. Ninety strangers cannot be simulated, so aim for a server that survives being wrong about them rather than one you feel confident about. Seed the tables, synthesise the work, rehearse the rollback, and keep those six testers online for the postmortem. Nobody has told them yet that the last three weeks were the rehearsal.