On this page
Write and test resources on a server your players are not on. Everything else on this page follows from that: a development server is what lets you restart resources, break things, and read errors without a hundred people watching.
This page covers setting that server up, how a resource is put together, where the security line between client and server sits, the performance mistakes that show up as hitch, and a deployment routine that has a way back.
Run a separate development server
Testing on live is not a shortcut, it is a bet. Restarting a resource drops the state it was holding for connected players. A script error in a loop hitches everyone. A database change that turns out to be wrong is wrong for real player data. None of that is recoverable at 9pm on a Saturday.
Three setups work, in descending order of safety.
- A second server. Same operating system, same FXServer build, its own database. The closest match to live, and nothing you do on it can touch your players.
- A second FXServer instance on the same machine. Its own directory, its own port, its own database schema. Cheaper, but it shares CPU with your live server, so a runaway script competes with real players. Never load-test on this one.
- FXServer on your own machine. Fine for pure script work, and zero risk to anything. Furthest from what live actually looks like.
Whichever you pick:
Give it its own license key
Generate a separate key in the Cfx.re keymaster rather than sharing the live server's. A key is meant for one running server.
Keep it off the public server list
Setting
sv_master1 ""in the development server's config stops it advertising itself. People find test servers, join them, and report the bugs you already know about.Restrict who can reach it
The firewall inside your server is separate from the protection rules in the panel, and it is the right place to limit a development port to your own address. On Ubuntu or Debian with
ufw:sudo ufw allow from 203.0.113.10 to any port 30125 proto udp sudo ufw allow from 203.0.113.10 to any port 30125 proto tcpEnabling
ufwdenies everything you have not allowed. Before you enable it, allow your SSH port, or you lock yourself out of the machine, and on a box that also runs your live server allow TCP and UDP on the live game port, or you disconnect every player on it. Any port you do open to the public still needs a rule in the panel: see Port rules.Give it its own database
A separate schema, loaded from a dump of live, never a connection to live.
Make the two consoles impossible to confuse
A distinct
sv_hostname, a smallsv_maxclients, a different terminal colour, whatever it takes. Running a command in the wrong window is the classic way this setup fails. See The server.cfg file.
How a resource is put together
Resources live under resources/ in your server data directory. A folder whose
name is in square brackets, such as [local] or [jobs], is a category rather
than a resource: it groups resources and is not started itself. FXServer
discovers what is there, and ensure name in your server.cfg is what actually
starts one. Order matters when one resource depends on another.
Every resource needs fxmanifest.lua in its root. That file is the contract
between your code and the server:
fx_version 'cerulean'
game 'gta5'
author 'Your name'
description 'What this resource does'
version '1.0.0'
shared_script 'config.lua'
client_scripts {
'client/main.lua',
'client/ui.lua'
}
server_scripts {
'server/main.lua'
}
files {
'html/index.html',
'html/style.css'
}
ui_page 'html/index.html'Three things about that file catch people out:
client_scriptsare sent to the player automatically. Everything else is not. An HTML file, an image, or a sound the client needs must be listed underfiles, or the client will not have it. This is the usual cause of a resource that works for you and not for anyone else.shared_scriptruns on both sides. It is for configuration and helpers, not for anything you would not hand to a player, because everything in it ships to the client.- The manifest is read when the resource list is refreshed. After adding a
new resource folder or editing
fxmanifest.lua, runrefreshin the server console beforeensure. After editing script contents only,ensure nameon its own is enough.
The console commands you will use constantly:
refresh
ensure myresource
restart myresource
stop myresourceClient and server: where the trust line is
A client script runs on the player's computer. They can read it, change what it sends, and send things it would never send. Executors exist for exactly that purpose and they are not rare. So:
Treat every client script as published, and every message from a client as something an attacker wrote.
The server is the only place where something becomes true. Money, items, jobs, permissions, and positions of record are decided server side, checked server side, and stored server side. The client asks, the server decides, the server tells the client what happened.
Anything you must not leak stays out of client scripts entirely: API keys, Discord webhook URLs, and the list of admin identifiers all ship to every player if you put them in a client file.
Events any client can trigger
RegisterNetEvent makes an event callable by every connected client, with any
arguments, as often as they like. That is its whole purpose, and it is also the
most common hole in a FiveM server.
Inside a server handler, source is the player who triggered the event and is
set by the server, so it is trustworthy. A player id passed as an argument is
not.
-- Unsafe: the client chooses who gets paid and how much.
RegisterNetEvent('bank:deposit')
AddEventHandler('bank:deposit', function(playerId, amount)
addBankBalance(playerId, amount)
end)-- The server decides. The client only asks.
RegisterNetEvent('bank:deposit')
AddEventHandler('bank:deposit', function(amount)
local src = source
amount = tonumber(amount)
if not amount or amount <= 0 or amount ~= math.floor(amount) then return end
if not isNearBank(src) then return end
if getCash(src) < amount then return end
takeCash(src, amount)
addBankBalance(src, amount)
end)Four habits cover most of it:
- Capture
sourceinto a local on the first line of the handler. It is not preserved across aWaitor an asynchronous database call, and code that readssourceafter one is reading whatever happens to be there. - Validate every argument. Type, sign, whole number, in range, and that the thing it names exists. A number a client sent is a number a client chose.
- Check permission on the server for anything privileged. ACE principals
configured in
server.cfgare the built-in way to do it, checked withIsPlayerAceAllowed(src, 'command.something'). A client-side check on whether someone is an admin is decoration. - Rate-limit anything expensive. A client can trigger an event in a loop, so a per-player cooldown belongs on anything that writes to the database or makes an HTTP request.
In the other direction, TriggerClientEvent('event', -1, data) sends to every
player. Send to one source when only that player needs it, both for privacy and
because broadcasting at high frequency is bandwidth you are choosing to spend.
Performance mistakes that show up as hitch
Most FiveM performance problems are written into resource code, not caused by the machine. Optimising a FiveM server covers diagnosis and the server-wide picture. These are the ones you introduce while writing:
A loop with no wait. A thread that never yields blocks everything behind it.
-- Hangs the thread. Nothing else in this resource runs.
CreateThread(function()
while true do
doSomething()
end
end)Wait(0) for work that does not need every frame. Wait(0) runs your loop
sixty or more times a second. Use the longest wait that still feels correct, and
vary it by distance so the fast path only happens when the player is somewhere it
matters:
CreateThread(function()
while true do
local sleep = 1000
local coords = GetEntityCoords(PlayerPedId())
if #(coords - shopCoords) < 15.0 then
sleep = 0
DrawMarker(--[[ ... ]])
end
Wait(sleep)
end
end)Drawing and native calls with no distance gate. Markers, 3D text, and help text drawn for every location on the map, every frame, whether or not the player can see them.
Per-tick loops on the server. Iterating every player every frame to check something that changes rarely. Drive it from an event instead, or run it on a timer measured in seconds.
Database work inside a loop. One query per player per tick will find the limit of your database before it finds the limit of your server. Batch the query, and use your MySQL wrapper's asynchronous form so the main thread does not wait on it.
Errors you have stopped reading. A script erroring every frame is doing that work every frame. A clean console is a performance feature.
Measure instead of guessing
Open the client console with F8 and run resmon. It shows CPU time per resource
in milliseconds. Watch your resource while the player is idle and again while
they are in the situation it handles: a resource that costs nothing at rest and
climbs when a menu opens is telling you exactly where to look.
For a breakdown inside a resource, use the built-in profiler. Recording and
viewing are separate commands, and the subcommands vary between builds, so run
profiler with no arguments in the console to see what yours supports before
relying on a particular one. Profile on the development server, where you can
reproduce the problem deliberately.
Keep your resources in source control
The server is not your backup, and "the version that is running" is not a version you can go back to.
- One git repository per resource is the common shape, and it makes updating a
single resource on live a
git pullin one directory. A single repository for a set of resources you wrote together is also fine. Pick one and be consistent. - Never commit secrets.
sv_licenseKey,rcon_password, and your database connection string belong in a separate config file that your main one pulls in withexec, and that file belongs in.gitignore. Anything you have already pushed should be treated as public and changed. - Ignore what you can regenerate. Build output,
node_modules/, editor directories, and any local cache. - Tag or branch what is live. Knowing exactly which commit is running is what makes a rollback a command rather than an investigation.
- Read the diff before you deploy, including on third-party resources. A "small update" that adds an HTTP call to a host you do not recognise is something you find by reading the diff and by no other means.
Deploying to the live server
Prove it on the development server first
Including the restart. Some resources hold state for connected players, and restarting them mid-session logs people out of an inventory or a job. If that is true of yours, you need to know before you find out on live.
Dump the database
mysqldump -u fivem -p fivem_live > backup-$(date +%F-%H%M).sqlThe same command works on MariaDB. Keep the dump somewhere that is not the server you are about to change.
Deploy at your quietest hour
Not during an event, not during your peak. A deploy with twelve people online is a different event from a deploy with a hundred and twenty.
Copy the new version across
A
git pullin the resource directory, orrsyncor SFTP from your machine. See Connecting to your server for the address and credentials.Restart the one resource, not the server
refresh ensure myresourcerefreshfirst if the resource is new or its manifest changed. Restarting the whole server disconnects everybody, so keep that for changes that genuinely require it.Watch the console, then watch a client
Read the console as the resource starts and for a few minutes after. Then join and run
resmonon F8 to confirm the new version costs what you expected.Know your way back before you need it
Check out the previous tag and
ensurethe resource again. If the change touched the database, the dump from step two is the way back, which is why it is step two.
Related
Diagnosing where the milliseconds actually go, server-wide.
The server.cfg fileConvars, exec, ACE permissions, and keeping secrets out of the file you share.
Ports, rate limits, and the resource-level abuse a filter cannot see.
BackupsWhat the panel lists, and how a restore is requested.