Technical deep-dive

What FiveM Enhanced actually changes under the hood

A source-level walkthrough — and a correction to the performance number everyone repeats.

Overtick · August 9, 2026

FiveM for GTA V Enhanced entered Early Access on 21 July 2026. The migration guide tells you what changed. It doesn't tell you what that means for the resources you're already running, and it doesn't tell you which of the performance advice circulating in this community was never true in the first place.

I spent a while reading the FiveM and txAdmin source to answer both. Some of what I found contradicts things I had assumed, and one thing contradicts advice I see repeated weekly. Everything below cites the file it came from. Where I could not verify a claim first-hand, I say so.

01

The performance budget everyone quotes is made up

You have probably read some version of this: keep resources under 0.10 ms idle, investigate anything sustained above 0.25 ms.

I went looking for the source. There isn't one. Those numbers trace to community forum posts with no staff reply. They are not in the Cfx documentation and they are not in the code.

Server script ticks run on svMain at 20 Hz. Every resource on your server shares that 50 ms window. That is the real denominator, and it is 500× larger than the number people quote.

If you want a defensible way to talk about resource cost, use percentage of the 50 ms svMain frame consumed. It is derivable from the source, it scales correctly with player count, and it doesn't require anyone to trust a number from a forum post in 2021.

Server thread intervals (GameServer.cpp)
ThreadIntervalRate
svMain1000 / 2050 ms — 20 Hz
svNetwork1000 / 100, send timer 1000 / 4010 ms / 25 ms
svSync1000 / 120~8.3 ms (Enhanced; sv_syncTickRate, default 60, range 1–120)
  • ResourceMonitor.cppFires OnWarning for a resource at 6 ms.
  • ResourceTimeWarnings.cppClient-side colour ramp: green under 1 ms, yellow-green 1–4, yellow-red 4–8, red above 8.
  • /perf histogramBucket boundaries: 0.001, 0.002, 0.004, 0.006, 0.008, 0.010, 0.015, 0.020, 0.030, 0.050, 0.070, 0.100, 0.150, 0.250 seconds.
02

A resource cannot measure another resource's CPU. At all.

The engine tracks per-resource CPU time properly. ResourceMonitor.cpp maintains a stopwatch stack with correct attribution for nested calls — if resource A calls into resource B, B's time is not charged to A. The data exists, it's accurate, and it's right there.

That file registers zero script natives. The complete set of profiler natives available to a script (Profiler.cpp): PROFILER_ENTER_SCOPE(name), PROFILER_EXIT_SCOPE(), PROFILER_IS_RECORDING(). All three are self-scoped. You can instrument yourself. There is no GetResourceTickTime, no enumeration, no read-back. A pure-Lua resource cannot report on its neighbours. Every "per-resource monitor" that claims otherwise is either measuring something else or reading it out of band.

The out-of-band route does exist. /profileData.json is registered in InfoHttpHandler.cpp and serves what profiler view generates. The profiler's event model emits BEGIN_TICK, ENTER_RESOURCE, EXIT_RESOURCE, ENTER_SCOPE, EXIT_SCOPE, END_TICK — each carrying a thread, a microsecond timestamp, a name, and a byte count. Pair up ENTER_RESOURCE/EXIT_RESOURCE and you have per-resource attribution for everything on the box.

Two things to know before you build on it. ExecuteCommand is ACE-gated, so you need add_acl resource.<yours> command.profiler allow. And a Cfx moderator has noted publicly that the profiler has attribution issues unless running in 'resource' mode. Validate against a resource you know is heavy before trusting the numbers.

On Enhanced the profiler backend is Perfetto, so the output format differs. If you write a parser, put it behind an interface.

bash
ExecuteCommand('profiler record 500')
  → ExecuteCommand('profiler view')
  → GET http://127.0.0.1:30120/profileData.json?token=<sv_profileDataToken>
03

/perf went from one metric to eighty

PerfHttpHandler.cpp serves http://<host>:<port>/perf/ in Prometheus text format on the same port as the game.

On Legacy it exposes exactly one metric: a tickTime histogram labelled name ∈ {svMain, svNetwork, svSync}. Counters are cumulative, so you diff consecutive scrapes and guard against resets on restart.

On Enhanced — per Development Update #3 — it exposes 80+: UDP packet counts, per-type packet pool usage, peer counts, invalid packets, OneSync entity and sync-tree and blip and state-bag counts, world grid, routing buckets, handshake counts and failures, rate limiting, auth ticket state, JS and Lua memory, HTTP endpoint request counts, KVP DB size, NetID usage, TCP connections, event-loop queue depths, RCON activity. Nobody has productised any of that yet. It landed a week ago.

One gotcha that will cost you an afternoon. Unauthenticated requests from non-proxy addresses hit a rate limiter keyed http_perf, defaulting to rate 2.0 / burst 5.0. Exceed it and you get 429s.

cfg
set sv_prometheusBasicAuthUser "yourname"
set sv_prometheusBasicAuthPassword "something-long"
04

What txAdmin actually stores — and for how long

txAdmin is first-party now (Cfx.re acquired it in April 2025) and it is the tool everyone already has. It is worth knowing precisely what it keeps, because the answer is less than most people assume.

From core/modules/Metrics/svRuntime/: scrapes /perf/ every 60 seconds, persists a snapshot every 5 minutes (PERF_DATA_INITIAL_RESOLUTION = 300000). Stores it in a flat JSON file: txData/<profile>/data/stats_svRuntime.json. STATS_LOG_SIZE_LIMIT = 720 snapshots → 60 hours, then hard truncation. The dashboard chart window is 30 hours.

config.ts defines a tiered downsampling table — 5-minute resolution for 0–12h, 15-minute for 12–24h, 30-minute for 24–96h. It is not implemented. The optimizer is a splice and a FIXME comment.

So: no per-resource attribution, no baselines, no regression detection, no performance alerting (FxMonitor is a liveness watchdog — it pings /dynamic.json and never evaluates tick times), and nothing older than 60 hours.

None of that is a criticism. txAdmin does a great deal extremely well. But if you have ever wanted to answer "was this worse than last Saturday?" — that is why you couldn't. Per-resource attribution has been open since January 2020. tabarra's reply at the time: "Currently all methods for us to do this is extremely hacky at best." Given §2, he was right.

ts
export const optimizeSvRuntimeLog = async (statsLog: SvRtLogType) => {
     statsLog.splice(0, statsLog.length - STATS_LOG_SIZE_LIMIT);
     for (let i = 0; i < statsLog.length; i++) {
          //FIXME: write code
          //FIXME: somehow prevent recombining the 0~12h snaps
          if (i % YIELD_INTERVAL === 0) { await new Promise((r) => setImmediate(r)); }
     }
}
05

The global event tap, and the trap in it

You can see every inbound event on your server. ResourceEventComponent.cpp dispatches against a wildcard key "*", and REGISTER_RESOURCE_AS_EVENT_HANDLER documents "*" as "disable HLL event filtering for this resource."

Here's the trap. The stock Lua scheduler does an exact-match lookup. So AddEventHandler('*', fn) registers you at the C++ level and then silently drops everything in Lua. You have to replace the routine.

Never decode the payload on the hot path. Count events and sum #eventPayload. Decode on a sampling basis if you need shape analysis. A monitoring tool that becomes the top resource in its own report is worse than no tool.

Scope limits worth knowing: inbound TriggerServerEvent and server-local TriggerEvent are visible. Outbound TriggerClientEvent is not — it routes through TRIGGER_CLIENT_EVENT_INTERNAL, a server→client net path that raises no local event. There is no hook. The closest proxy is the Enhanced /perf UDP counters.

Also: playerEnteredScope and playerLeftScope scale O(n) under OneSync. Thirty-two players in scope means thirty-two dispatches. On a busy server those two events alone can dominate your event volume and drown everything else. Special-case them.

lua
RegisterResourceAsEventHandler('*')

local realRoutine -- capture the scheduler's existing routine first
Citizen.SetEventRoutine(function(eventName, eventPayload, eventSource)
  -- HOT PATH. Do not msgpack-decode here.
  counters[eventName] = (counters[eventName] or 0) + 1
  bytes[eventName]    = (bytes[eventName] or 0) + #eventPayload
  return realRoutine(eventName, eventPayload, eventSource)
end)
06

The migration itself

It will not boot

  • C# on Mono. Enhanced replaced Mono with .NET 10. Assemblies targeting .NET Framework, Mono, or older .NET Core will not load. If you don't have the source, you need the author.
  • Escrowed resources. Asset escrow is listed as "not implemented yet" on Enhanced. Escrowed resources cannot be decrypted. This is not something you can work around.
  • Removed convars: sv_netHttp2 (HTTP/2 removed), onesync_automaticResend (ARQ removed, replaced by retry logic in the new raw-UDP layer).
  • OneSync non-big-mode. Removed entirely. Big mode is the only mode.
  • svgui. The server-side ImGui interface is gone. If anything you run drives or scrapes it, that stops.

It will run, and behave differently, and not tell you

This is the category that costs people a weekend.

  • State bag callbacks now require the entity to exist. Handlers that previously fired for not-yet-created entities silently stop firing.
  • Replicated values must be set explicitly. A bare state.foo = x may no longer replicate. Use Entity(e).state:set('foo', x, true) with the flag.
  • Client-side player iteration is incomplete. Under big mode, clients don't receive player data outside a hardcoded 424-unit focus zone. GetActivePlayers() on the client returns a partial list.
  • Culling natives are deprecated with, in Cfx's words, "known, unfixable issues." Use routing buckets and SET_ENTITY_ORPHAN_MODE.
  • Mumble natives are deprecated — still routed through the new voice system, but going away. sv_mumble true restores compatibility at the cost of channel privacy.
  • Remote commands no longer return logs automatically. Call PrintRemoteCommandLog() where you need output.
  • Pure Mode is permanently on and cannot be disabled during Early Access. .ysc, .asi, injectors, modified archives — all rejected. Graphics mods have no Enhanced-compatible path right now.

Deprecated, still working

  • sv_useAccurateSends → sv_syncTickRate. onesync_enableBeyond (implied by big mode). sv_enhancedHostSupport. sv_protectServerEntities → sv_entityLockdown, which gains a full mode.
  • The -cl2 dual-client flag is gone. Only the latest gamebuild is supported — older pins error out.
  • The KVP database format changed; back it up and verify after your first Enhanced boot.
  • FXServer.exe is now cfx-server.exe, which breaks start scripts and systemd units.

Actually good news

  • sv_syncTickRate is configurable from 1 to 120.
  • sv_resourceFileDownloadTimeout is new.
  • RegisterCommand now returns an id you can pass to UnregisterCommand, so resources stop leaking commands across restarts.
  • There's a proper server-authoritative Voice API — CreateVoiceChannel, AddPlayerToVoiceChannel, SetPlayerMutedInVoiceChannel, SetPlayerDeafInVoiceChannel — with noise and echo cancellation and better packet-loss handling.
07

One claim I could not verify

Several third-party migration guides state that Lua 5.3 is deprecated on Enhanced and that manifests need lua54 'yes'.

I could not find this in Cfx first-party documentation. It may well be true. But if you are about to edit two hundred manifests on the strength of it, test one resource first. I'd rather flag the uncertainty than pass it along as fact.

A scanner, if you want one

I turned all of the above into a static analyser. It reads your fxmanifest.lua files and server.cfg, classifies scripts as client or server so the client-only rules don't fire on server code, strips comments before matching, and opens .dll files to read the actual TargetFrameworkAttribute out of the assembly metadata rather than guessing.

MIT, zero dependencies, no account, no telemetry unless you explicitly pass --share-stats. It writes an HTML report you can hand to whoever maintains the resource.

It is static analysis, so be clear-eyed about the limits: it cannot see runtime behaviour, natives called through string indirection, or anything inside an escrowed resource. A clean scan is necessary, not sufficient. Bench on Enhanced before you migrate production.

bash
npx @overtickgg/enhanced-scan ./resources

Sources

Everything above is from these. Where I've quoted, the quote is verbatim.

OVERTICK

Overtick is not affiliated with Cfx.re, Rockstar Games, or Take-Two Interactive.

[email protected] · © 2026 Overtick