How to optimize an MTA:SA server: performance, tickrate and the end of lag
Practical guide to optimizing an MTA:SA server: tune fps_limit, tickrate, Lua scripts, database and network to eliminate lag and keep 100 players stable.
An MTA:SA server with lag isn’t a single problem — it’s a set of bottlenecks that manifests as rubberbanding, shot desync, client-side FPS drops and timeouts. Most guides on the internet recommend “add more RAM” or “switch hosts”, which rarely solves it. The real bottleneck is almost always CPU saturated by badly written Lua scripts, sync rate misaligned with available bandwidth, or a database blocking the main thread.
This tutorial is for anyone administering an MTA:SA server — roleplay, deathmatch, freeroam or racing — and who wants to stop treating symptoms and attack the cause. You’ll learn to identify where the bottleneck is, tune mtaserver.conf based on real data, optimize the heaviest Lua resources, and configure the operating system to deliver UDP packets with low and consistent latency. Estimated time: 40-60 minutes with the server already installed.
Before changing any parameter, let’s be clear: optimization without measurement is guesswork. Each section below includes the diagnostic command before the change and how to validate the result.
Prerequisites
MTA:SA 1.6+ server running on Linux (Ubuntu 22.04 LTS or Debian 12), SSH access with sudo, and at least one test session with 10-20 players or a stress bot to generate representative load. Optimization based on an empty server is useless — behavior changes completely under load.
22003 UDP 22005 TCP 22126 UDP mta-server64 You also need access to the panel or shell where the server runs and to the mtaserver.conf file. If you administer multiple servers sharing the same hardware, isolate the metrics — CPU steal from a noisy neighbor can look like your problem.
Measuring the current state
Before tuning anything, record a baseline. Without before/after numbers, you won’t know whether the change helped.
Connect to the server console (via local terminal or panel) and enable the internal performance monitor:
debugscript 3
showperfThe showperf command displays the average processing time per frame in milliseconds, separated by category (sync, scripts, database). Note the values on an empty server and with at least 50% of capacity occupied.
On the Linux shell, capture CPU usage per thread of the process:
top -H -p $(pgrep -f mta-server64)The main thread (usually the first one listed with the most CPU) is where Lua scripts execute. If it’s at 95-100% and the server is multi-core, you have a single-threaded bottleneck — common in MTA, which does not parallelize Lua.
View network usage in real time:
sudo iftop -i eth0 -P -BNote the bandwidth used per connected player. A healthy server uses 8-15 KB/s per player in normal state. Above 25 KB/s indicates a sync rate that’s too high or unnecessary broadcast in scripts.
Save these three snapshots in a text file. They are your reference to validate each change.
Tuning mtaserver.conf
The mtaserver.conf controls the sync intervals between client and server. Values that are too aggressive saturate bandwidth and CPU; values that are too conservative create the “rubber” feeling in other players’ movement.
Locate the file (usually at /home/mta/mods/deathmatch/mtaserver.conf) and open with the editor of your choice. Look for the sync sections.
Configure the intervals for a 60-100 player server aimed at fluid gameplay:
<player_sync_interval>100</player_sync_interval>
<light_sync_interval>1500</light_sync_interval>
<camera_sync_interval>500</camera_sync_interval>
<ped_sync_interval>400</ped_sync_interval>
<unoccupied_vehicle_sync_interval>1000</unoccupied_vehicle_sync_interval>
<keysync_mouse_sync_interval>100</keysync_mouse_sync_interval>
<keysync_analog_sync_interval>100</keysync_analog_sync_interval>These values are the sweet spot for DM and roleplay. For competitive racing servers, reduce player_sync_interval to 80 and keysync_mouse_sync_interval to 80 — you gain responsiveness at the cost of ~25% more bandwidth per player.
Adjust the server FPS limit and the physics simulator resource allocation:
<fps_limit>0</fps_limit>
<server_logic_fps>100</server_logic_fps>
<bandwidth_reduction>none</bandwidth_reduction>fps_limit=0 disables the client-side limit — this doesn’t affect the server directly but prevents players from complaining about an artificial cap. server_logic_fps=100 is the default and almost never needs changing. bandwidth_reduction set to “medium” only pays off if your total bandwidth is bursting.
When restarting the server to apply these changes, connected players lose the session. Do it in a previously announced maintenance window or use refresh + refreshall in the console to reload resources without dropping connections — although mtaserver.conf requires a full restart.
Restart the server and measure again:
sudo systemctl restart mta-serverRepeat showperf and iftop after 10 minutes of operation with players. Frame times should be consistently below 12ms on the main thread.
Optimizing Lua scripts
Badly written scripts are responsible for 70% of cases of MTA server lag — regardless of hardware. The good news is that you can identify and fix the worst offenders quickly.
In the server console, enable the built-in profiler for 60 seconds with the server under real load:
debugscript 3
debug-resources --top 10The output lists the 10 resources that consume the most CPU in ms per frame. Focus on the first 3 — they generally account for 50%+ of total consumption.
Identify and eliminate setTimer with short intervals. This is the most common anti-pattern:
setTimer(function()
for _, player in ipairs(getElementsByType("player")) do
local money = getPlayerMoney(player)
end
end, 50, 0)This code runs 20 times per second iterating over all players. On a server with 80 players, that’s 1600 iterations per second just for this timer. Replace it with on-demand events:
addEventHandler("onPlayerMoneyChange", root, function()
end)Cache results of getElementsByType and similar calls. Each call walks the server’s element tree:
local cachedPlayers = {}
addEventHandler("onPlayerJoin", root, function()
table.insert(cachedPlayers, source)
end)
addEventHandler("onPlayerQuit", root, function()
for i, p in ipairs(cachedPlayers) do
if p == source then
table.remove(cachedPlayers, i)
break
end
end
end)Iterating cachedPlayers is 10-30x faster than getElementsByType("player") in a tight loop.
Firing an event to root sends it to all clients, costing bandwidth proportional to the number of players. Use triggerClientEvent(targetPlayer, ...) or triggerClientEvent(getPlayersInRange(x, y, z, 200), ...) to limit broadcast. Roleplay servers with inventory UI spend half their bandwidth solely because of undirected events.
Database and persistence
SQLite is the MTA default and is generally the best choice. But the default configuration doesn’t enable Write-Ahead Logging, which causes blocking on simultaneous writes.
On the first database load, enable WAL mode via SQL:
executeSQLQuery("PRAGMA journal_mode=WAL")
executeSQLQuery("PRAGMA synchronous=NORMAL")
executeSQLQuery("PRAGMA cache_size=-20000")WAL allows concurrent reads with writes. synchronous=NORMAL reduces fsync without catastrophically compromising durability — accepts losing the last 1 second in case of a server crash. cache_size=-20000 allocates 20 MB of cache for SQLite.
Identify slow queries by enabling temporary logging:
local startTime = getTickCount()
local result = executeSQLQuery("SELECT * FROM accounts WHERE last_login > ?", os.time() - 86400)
local elapsed = getTickCount() - startTime
if elapsed > 50 then
outputDebugString("Slow query: " .. elapsed .. "ms")
endAny query above 50ms blocks the main thread and is a candidate for optimization — add indexes on search columns or pagination on large result sets.
Network and operating system
The Linux network layer has conservative defaults that weren’t designed for game UDP. Correct adjustments reduce perceived latency without changing anything in the game.
Enable fq_codel as the qdisc on the main interface:
sudo tc qdisc replace dev eth0 root fq_codelfq_codel eliminates bufferbloat, keeping latency consistent even with saturated bandwidth. The practical difference: players at peak hours no longer see ping go from 30ms to 200ms when total traffic grows.
Increase the kernel UDP buffers:
sudo sysctl -w net.core.rmem_max=8388608
sudo sysctl -w net.core.wmem_max=8388608
sudo sysctl -w net.core.netdev_max_backlog=5000Persist them in /etc/sysctl.d/99-mta.conf to survive a reboot. Larger buffers absorb bursts without dropping packets, which is what causes rubberbanding when the link fills up.
Some old guides recommend tcp_congestion_control=bbr or disabling GRO/LRO on game servers. On modern hardware, this can make performance worse. Always measure before and after — don’t blindly copy configurations from forums.
Verification
After applying the changes, validate with real load:
showperf
Frame times should be consistently below 12ms on the main thread, even with a full server. Use iftop to confirm that bandwidth per player has dropped (if you reduced unnecessary broadcast) or remained stable (if you increased sync rate for gameplay).
On the client side, ask 3-5 players to run Net Stats (F11 in the MTA menu) and report jitter and packet loss during 10 minutes of normal gameplay. Jitter below 5ms and zero packet loss is the target.
Troubleshooting
Server restarts by itself under load
Usually it’s the OOM killer killing the process. Check with dmesg | grep -i "killed process". The solution is to add RAM or create a 4 GB swap. Lua scripts leaking memory also cause this — use getResourceUsedMemory to identify the guilty resource.
High latency only for some players
It’s not the server. Ask them to run mtr or WinMTR to the server’s IP during the problem. It’s almost always congestion on the player’s ISP route. If the problem is consistent for clients of a single Brazilian ISP, consider moving the server to a region with better peering with that ISP.
”Server is full” but there are free slots
Check MaxPlayers in mtaserver.conf and maxplayers in the master server file. Both need to match. In some cases, custom scripts implement their own limit via cancelEvent() in onPlayerConnect — look for that in the custom resources.
Next steps
Optimization is a continuous process — measure periodically, especially after adding new resources. Some directions to dig deeper:
- Set up long-term monitoring with an external metrics system plotting frame time and player count throughout the day
- Study Lua JIT and efficient data structures for scripts that process large volumes (inventory, leaderboards)
- Implement rate limiting on events coming from the client to prevent malicious flooding
- Document the baseline of each server hash to detect regression after resource updates
If you’re growing the server beyond 100 concurrent players or running multiple MTA servers from the same community, a dedicated MTA hosting plan from Hostini comes with a kernel tuned for game UDP, DDoS protection at the edge and a network with optimized Brazilian peering — three things that remove 80% of common causes of lag before the first player even connects.
Frequently asked questions
What is the ideal tickrate for an MTA:SA server?
MTA:SA doesn't have a single "tickrate" — the server operates with several configurable intervals in mtaserver.conf. The main ones are player_sync_interval (default 100ms), light_sync_interval (1500ms) and camera_sync_interval (500ms). Reducing player_sync_interval to 80ms improves shooting fluidity but increases bandwidth used by ~25%. Don't go below 60ms without a dedicated network.
How many players can an MTA:SA server handle on a 4 vCPU VPS?
A stock vanilla server with few custom scripts handles 80-120 players on 4 vCPUs and 4 GB of RAM. Roleplay servers (DayZ, Brazilian RPG with inventory, garage, etc) drop to 40-60 players on the same hardware because the bottleneck shifts from network to CPU running Lua. Profile before scaling hardware.
Why does my MTA server sit at 60% CPU even when empty?
Badly written Lua timers are the #1 cause. Any setTimer with an interval below 100ms running in an infinite loop consumes CPU even with no players. Use debugscript 3 in-game to see which resources are consuming CPU in ms per frame. Poorly written resources can burn 30-40% on their own.
Is it worth using an external database or is internal SQLite enough?
For fewer than 100 concurrent players and simple queries, SQLite is faster by eliminating network latency. External MySQL/MariaDB only pays off when you have multiple servers sharing data or complex queries with JOIN on large tables. For a single server, SQLite with WAL mode is the best choice.
MTU 1500 or MTU optimized for MTA:SA?
MTA:SA uses small UDP packets (60-400 bytes most of the time). The default MTU of 1500 works well — there's no practical gain in reducing it. What matters is eliminating bufferbloat: enabling fq_codel or cake as the Linux qdisc makes more difference than tweaking MTU.
How do I know if player lag is the server's fault or their network's?
Use the /showperf command in the server console to see the time of each frame in ms. If it's consistently below 16ms, the server is healthy. Lag reported in that scenario is a player route problem. Use mtr or WinMTR from complaining players' IPs to confirm packet loss along the path.