Community Board
Share feedback, report bugs, suggest features, and track what we're building next.
Sign in to vote, post, and comment.
Pinned Announcements
9 FiveM Coding Anti-Patterns That Quietly Break Your Server
I spend a lot of time reading other people's FiveM scripts. Some of it is research, some of it is curiosity, and honestly some of it is just that I can't help opening a client.lua the way other people can't help checking their phone. And after enough of that, you start seeing the same handful of mistakes over and over — not exotic bugs, just patterns. Ways of writing code that technically work on an empty test server and then quietly fall apart the moment thirty real players show up. The tricky thing about anti-patterns is that most of them don't announce themselves. Your script loads, the feature works when you test it alone, the release thread gets a few likes. The damage shows up later — as server hitches nobody can trace, as an economy that slowly inflates, as a "why can't I get my car out of the garage" ticket that turns out to be three resources fighting over the same value. So I want to walk through the ones I see most, sorted roughly by how hard they are to catch. We'll start with the stuff you can spot by eye, move into the ones that need you to actually think about how the server behaves, and end with the quiet, critical ones that don't look like bugs at all until someone exploits them. Every point here is something I've either seen in a real release or run into myself. callout One honest disclaimer up front: I link real free forum releases as examples, because concrete beats hypothetical every time. This isn't about dunking on anyone — most of these authors gave their work away for free, which is more than a lot of us do. The point is the pattern, not the person. The three tiers, and why "easy to spot" isn't the same as "low impact" Before the list, one framing that's saved me a lot of grief. How visible a mistake is and how much damage it does are two completely different axes. A missing Wait crashes your client instantly — extremely visible, and you'll fix it in ten seconds. A missing server-side check does nothing at all until a cheater finds it six months in, and then it drains your economy overnight. So as we go from "easy" to "critical" here, we're not going from small to big. We're going from loud to silent. The loud ones train you. The silent ones are the ones that actually take servers down. callout TIER ONE — EASY TO SPOT. These are the anti-patterns you can catch just by reading, no server needed. If you're reviewing a script before you buy or install it, this is your first pass. 1. The unyielding loop (or the near-unyielding one) The most famous FiveM footgun. A loop that never yields hangs the entire thread — and because everything in a resource shares that scheduler, an infinite loop with no Wait can freeze your whole client or server solid. lua -- This will hang everything. while true do doSomething() -- no Wait() -- the thread never gives control back end Everyone learns this one fast, because it fails so hard you can't miss it. The [Cfx.re docs on Citizen.CreateThread](https://docs.fivem.net/docs/scripting-reference/runtimes/lua/functions/Citizen.CreateThread) spell it out: a CreateThread loop needs a Wait inside it or it starves the scheduler. But the sneakier version of this isn't the missing Wait — it's the too-frequent one. Wait(0) runs your loop every single frame. That's correct and necessary for things that genuinely must draw every frame (like DrawMarker or DrawText3D), but people reach for it reflexively for logic that has no business running 60-plus times a second. lua -- Checking a job status every frame? Why? CreateThread(function() while true do local job = getPlayerJob() -- runs ~144×/sec on a 144Hz monitor updateHud(job) Wait(0) end end) note A subtlety a lot of people get backwards: if something must run every frame, use Wait(0), not Wait(5) or Wait(10). On a high-refresh monitor, a fixed millisecond wait actually skips frames and makes per-frame work look janky. Wait(0) means "next frame, whatever the framerate is." Save the bigger waits for logic that genuinely doesn't need frame precision — a job check is fine at Wait(1000). The fix is just to ask "how often does this actually need to happen?" and set the wait accordingly. A HUD value that changes when your job changes can poll once a second, or better, only update when it's pushed a change. This is the single cheapest performance win in FiveM and it costs you nothing but a moment of thought. 2. Doing expensive work every frame instead of throttling it This is the same instinct as above but it shows up specifically around drawing and distance checks, and it's worth its own section because it's everywhere. DrawMarker and DrawText3D genuinely have to run every frame to appear — that part's unavoidable. What's avoidable is running the distance math and the loop over every marker every frame, including the markers that are 300 metres away and invisible.  The pattern that fixes it is the "dynamic sleep" loop: default to a long wait, and only drop to Wait(0) when the player is actually near something that needs per-frame drawing. lua CreateThread(function() while true do local sleep = 1000 local p = GetEntityCoords(PlayerPedId()) for , m in pairs(markers) do if (p - m.coords) < 20.0 then sleep = 0 DrawMarker(--[[ ...m.coords... ]]) end end Wait(sleep) end end) When nobody's near a marker, that loop sleeps a full second and costs essentially nothing on resmon. When you walk up to one, it snaps to per-frame drawing. Same visual result, a fraction of the cost. The [community performance guides](https://gist.github.com/nnsdev/c18a5684ad8c03bda882fcd775eaf695) have been preaching this for years and it still gets ignored constantly. One small companion habit while you're in here: use (vectorA - vectorB) for distance, not GetDistanceBetweenCoords. The vector-length operator is a native Lua operation and measurably faster than the native call — small on its own, but it's the kind of thing that runs thousands of times a second. 3. Re-fetching things that never change (or that you already have) The third eye-catchable one: calling the same native over and over when one call would do. The classic is PlayerPedId() — I've genuinely seen scripts call it a dozen-plus times inside a single function, when the ped handle is the same for the whole call. lua -- Anti-pattern: same handle, fetched five times SetEntityHealth(PlayerPedId(), 200) local coords = GetEntityCoords(PlayerPedId()) if IsPedInAnyVehicle(PlayerPedId(), false) then TaskLeaveVehicle(PlayerPedId(), GetVehiclePedIsIn(PlayerPedId()), 0) end lua -- Fix: fetch once, reuse local ped = PlayerPedId() SetEntityHealth(ped, 200) local coords = GetEntityCoords(ped) if IsPedInAnyVehicle(ped, false) then TaskLeaveVehicle(ped, GetVehiclePedIsIn(ped), 0) end Individually these calls are cheap. But cache them anyway — partly for the tiny performance gain, mostly because the cached version is easier to read and reason about, and readability is what stops the next three tiers of bugs from creeping in. Clean local variables are a load-bearing habit, not a cosmetic one. callout TIER TWO — MODERATE. Now it gets less visual. These don't look wrong on the page. You have to understand how the server actually behaves over time, across players, to see the problem. 4. Polling the server on a timer when an event would do Here's one straight from a real release. While reviewing a [free ESX plate-switcher script on the forums](https://forum.cfx.re/t/free-esx-nkhd-plate-switcher-v2/5218047/112), I found this on the client: lua while true do TriggerServerEvent('nkhdchangePlate:getIdentifier') TriggerServerEvent('nkhdchangePlate:getPlateData') Citizen.Wait(100) end Read that carefully. Every client, ten times a second, forever, fires two server events — and on the server those events run database queries. So you've built a system where a hundred connected players generate two thousand database round-trips per second, whether or not anything changed. The data it's fetching updates maybe once every few minutes when someone tapes a plate. It's polling a mailbox ten times a second for a letter that arrives twice a day. This is the pattern to internalise: don't poll for state that changes rarely — push it when it changes. The server already knows the moment the plate data changes, because it's the one changing it. So have it TriggerClientEvent the affected client(s) at that moment, and let the client sit quietly the rest of the time. You go from thousands of queries a second to a handful an hour. For truly shared state, this is also exactly what [state bags](https://docs.fivem.net/docs/scripting-manual/networking/state-bags/) exist for — set a value once and it syncs to whoever needs it. 5. Global state where per-player state belongs This one is genuinely subtle and it was hiding in that same plate-switcher. The client stored fetched data into plain globals: lua RegisterNetEvent('nkhdchangePlate:receivePlateSwitcherData') AddEventHandler('nkhdchangePlate:receivePlateSwitcherData', function(data) for , row in ipairs(data) do identifiern = row.identifier -- global, overwritten each row platen = row.plate modeln = row.model end end) That loop doesn't collect the data — it walks the whole dataset and keeps only the last row. Because those variables are global and unscoped, each client ends up tracking whatever the most recently taped vehicle on the entire server was. Play it forward. Player A tapes their car. Later, Player C tapes theirs. Player A's identifiern just got silently overwritten with Player C's identifier. So when Player A tries to un-tape their own vehicle, the ownership check: lua if identifiert == identifiern then now fails, because identifiern belongs to Player C. In practice only the last person to tape anything can un-tape. Everyone else is locked out of their own action, and nobody can figure out why because the code "looks fine." The lesson isn't about plates. It's that any time data belongs to a specific player or entity, it needs to be scoped to that player or entity — a table keyed by identifier, a state bag on the entity, something with a per-owner slot. The moment you stash per-player data in a shared global, you've built a race condition that only shows up with more than one player online. Which is exactly why it survives testing and dies on launch night. 6. Never cleaning up what you spawn Resources start and stop constantly on a live server — restarts, updates, crashes, txAdmin doing its thing. If your script spawns entities, opens threads, or creates blips and never cleans them up on stop, you leak. Restart a vehicle-spawner resource a few times and you'll find [orphaned vehicles piling up in the network object viewer](https://github.com/citizenfx/fivem/issues/1762), each one still eating a network slot. The habit that prevents it is an onResourceStop handler that tears down everything the resource created: lua local spawnedEntities = {} AddEventHandler('onResourceStop', function(res) if res ~= GetCurrentResourceName() then return end for , entity in ipairs(spawnedEntities) do if DoesEntityExist(entity) then DeleteEntity(entity) end end end) warn One real gotcha here, per the [Cfx docs on onResourceStop](https://docs.fivem.net/docs/scripting-reference/events/list/onResourceStop/): cleanup that relies on async operations or promises may not finish, because the resource is being torn down. Keep your stop-handler cleanup synchronous — delete entities, save critical data with a blocking call — don't kick off a promise chain and assume it resolves. You won't notice a missing cleanup handler in a quick test. You notice it three weeks in, when the server's been up for days and something feels heavier than it should. warn TIER THREE — CRITICAL. Here's where it stops being about performance and starts being about whether your server survives contact with a motivated player. These don't crash anything. They don't show up on resmon. They look completely normal right up until someone exploits them — and by then it's your economy, your database, or every player's security on the line. 7. Trusting the client — the mistake that costs servers their economy If you take one thing from this whole post, take this. The client is in the hands of the player, and the player might be running a cheat menu. Any event you register with RegisterNetEvent can be triggered by any client, with any arguments they choose. A Lua executor makes TriggerServerEvent('bank:deposit', 99999999) as easy as typing it.  So the anti-pattern is any server event that takes the client's word for something that matters. The [Cfx.re server-security guide](https://docs.fivem.net/docs/developers/server-security/) gives the canonical bad example: lua -- NEVER do this. The client picks the item and the count. RegisterNetEvent('job:givePlayerItem', function(item, count) local ply = FX.GetPlayerFromSource(source) ply.addItem(item, count) -- free anything, any amount end) Nothing stops a player from calling that with ('goldbar', 9999). The fix isn't a single line — it's a mindset. Every event that changes money, items, or state must re-derive the truth on the server: does this player actually have an active job? Are they physically where the action requires? Is the amount within sane bounds? Have they done this suspiciously often? The docs' secure example gates item-giving behind a server-tracked job state and a distance check, and resets that state after use so it can't be replayed. That's the shape you want. Three concrete habits that go a long way: First, register events in the right context. Use RegisterNetEvent only for events that genuinely need to cross client↔server. For internal events, AddEventHandler keeps them non-networked so a cheat client can't reach them at all. You can even hard-block same-context calls by checking if source ~= 65535 then return end on server events that should only ever come from the server itself. Second, validate every input as if it's hostile, because it might be. Never addItem(item, count) from raw client arguments. Third, rate-limit anything that touches the economy, so even a "valid" event can't be spammed a thousand times a second to duplicate money or farm items. 8. Building SQL by pasting strings together This is the one that keeps me up a little. SQL injection is ancient, well-understood, and still shows up in FiveM resources — usually anywhere user input reaches the database. The pattern looks harmless: lua -- Vulnerable: the player's input becomes part of the query. MySQL.Async.execute("UPDATE players SET name = '" .. newName .. "'") If newName is a normal name, fine. If newName is ', money = 999999 WHERE 1=1 -- , you've just handed a player the ability to rewrite your entire players table. Any input field — a character name, a /report reason box, an admin note — is a door if you concatenate it straight into SQL. There are [real FiveM admin panels that shipped with exactly this hole](https://github.com/BradyBots/FiveM-Administration-Panel/issues/1). The fix is total and simple: always use parameterized queries. Every modern FiveM database layer — oxmysql, mysql-async — supports ? placeholders that bind values safely instead of splicing them into the query text: lua -- Safe: the value can never be interpreted as SQL. MySQL.update('UPDATE players SET name = ? WHERE identifier = ?', { newName, identifier }) With placeholders, newName is always treated as a string value, never as query structure, no matter what characters it contains. There is essentially never a good reason to concatenate user input into a query. If you see .. someVariable .. inside a SQL string in a resource you're evaluating, treat it as a red flag and go read the rest of that resource carefully — where there's one, there are usually more. 9. Assuming entities and their sync state are always there The most invisible one, and the hardest to reason about, because it's about timing and ownership in a networked game. In OneSync, entities are owned by whichever client is closest / streaming them, ownership migrates as players move, and a network ID you got a moment ago might refer to an entity that no longer exists on your client — or that you don't own and therefore can't modify. The anti-pattern is code that assumes an entity handle is always valid and always yours: lua -- Fragile: assumes the entity exists and is controllable right now. local veh = NetworkGetEntityFromNetworkId(netId) SetVehicleDoorsLocked(veh, 2) -- may silently do nothing If that vehicle isn't streamed on your client, or another client owns it, this can quietly fail — no error, just nothing happens. And [state bags only sync reliably once an entity is properly networked and within range](https://docs.fivem.net/docs/scripting-manual/networking/state-bags/), so setting a state bag on an entity you've just requested, before it actually exists everywhere, leads to values that mysteriously don't propagate. There's no one-line fix, but the defensive habits are: always guard with DoesEntityExist before you touch a handle; if you must modify an entity you don't own, request control and wait for it rather than assuming; prefer server-side entity creation and state for things that must be authoritative; and remember that a network ID is a reference across the session, not a guarantee the thing is on your screen right now. This is the category of bug that produces "it works for me but not for my friend" reports, and the reason is almost always that two clients disagreed about who owned what, and when. note This tier is exactly where the gap between "works in my test" and "works on a 200-player server" lives. You physically cannot reproduce most ownership and sync issues alone, because they require multiple clients disagreeing about the same entity. If you build networked features, you have to test them networked. Zooming out: the pattern behind the patterns If you line all nine up, there's a single thread running through them. The easy ones are about respecting the frame — not doing work the game doesn't need done. The moderate ones are about respecting time and other players — data that's scoped correctly and pushed instead of polled. And the critical ones are all one idea wearing different clothes: decide where the truth lives, and never trust anything on the wrong side of that line. The client isn't the truth. A network ID isn't a guarantee. A raw string isn't safe input. None of this requires being a genius. It requires slowing down for a second and asking who owns this data, how often does this really need to run, and what happens when the person on the other end is hostile. That second of thought is, honestly, most of what separates a script that survives launch night from one that becomes a support ticket. > A resource that works when you test it alone is not a resource that works. It's a resource that hasn't been tested yet. That's the whole game. The engine has rules about frames, about ownership, about trust — and good FiveM development is mostly learning where those rules are and building with them instead of finding out the hard way. --- At [OsmFX Mods](https://osmfxmods.com) this is basically our whole philosophy — performance-first, server-authoritative scripts for QBCore, ESX, and QBOX that are built to hold up under real player load, not just pass a solo test. If you'd rather install things that already got this stuff right, [have a look around the store](https://osmfxmods.com). And if you've got a script you're not sure about, reviewing code like this is genuinely one of my favourite things to do — that's exactly the kind of thing we write about here.
How to Render a Live Web App onto a 3D Screen in FiveM (DUI Explained)
Walk up to an ATM in most FiveM servers and you get a menu that opens in front of the world — a fullscreen NUI panel floating over the game. It works, but it always breaks the illusion a little. Your camera is standing at the machine, and yet the interface is pasted across your whole monitor. So when we built [our ATM system](https://github.com/OsmFX-Mods/osm-atmdui), we wanted the interface to live on the machine — a real, animated banking UI rendered onto the actual ATM screen texture, warping and sitting in 3D space exactly where your eyes expect it. Card animations, a keypad, balance screens, the works, all glued to the prop. That's not a fancier NUI. It's a completely different rendering path called DUI, and it comes with one genuinely tricky problem nobody warns you about. Let me walk through how it actually works — and then the part that took the longest to get right. callout The short version: NUI draws a web page over your whole screen. DUI draws that same web page onto a texture, and you paint that texture onto any surface in the world — a screen, a billboard, a cinema. The catch is that a texture floating in 3D space has no mouse cursor, so making it clickable is a puzzle all its own. NUI vs DUI: same web page, very different destination If you've read [our post on how FiveM renders your UI](https://www.osmfxmods.com/blog/why-backdrop-filter-blur-doesnt-work-in-fivem-and-what-cef-i-1i26il), you already know the important bit: your UI is a web page running inside an embedded Chromium browser (CEF), and FiveM composites that on top of the game at the end of each frame. Normal NUI takes that browser output and stretches it across your entire screen. That's why an inventory or a HUD sits over everything. DUI — "direct-rendered UI" — takes the exact same kind of web page and renders it to a runtime texture instead of your screen. A texture is just an image the game engine can paint onto any surface. Once your UI is a texture, you can slap it onto an ATM screen, a TV, a billboard, a laptop prop — anywhere the engine draws pixels. Same React app. The only difference is where the final image lands. The pipeline: from URL to ATM screen Getting a web page onto a prop is a five-step handshake between CEF and the game's texture system. Here's the whole chain we use, start to finish.  First, you spin up the browser off-screen: lua local url = ('nui://%s/html/index.html?dui=true'):format(GetCurrentResourceName()) DUI.duiObject = CreateDui(url, 1326, 1004) DUI.duiHandle = GetDuiHandle(DUI.duiObject) CreateDui loads your page into a browser that renders nowhere yet, at a fixed resolution (1326×1004 in our config — pick something that matches the aspect ratio of the screen you're targeting). GetDuiHandle hands you back a reference to that browser's output — the live pixels. Next, you wrap those pixels as something the game can actually draw: lua local txd = CreateRuntimeTxd('atmduitxd') Wait(250) -- give the TXD a moment to register CreateRuntimeTextureFromDuiHandle(txd, 'atmduitex', DUI.duiHandle) CreateRuntimeTxd makes an empty texture dictionary — think of it as a named folder the engine can look textures up in. CreateRuntimeTextureFromDuiHandle then drops your live browser output into that folder as a texture called atmduitex. From here on, the game treats your React app as if it were any other image asset. Finally, the magic swap: lua AddReplaceTexture('propatm01', 'propcashpointscreen', 'atmduitxd', 'atmduitex') AddReplaceTexture tells the engine: anywhere you would have drawn the ATM's original screen texture, draw mine instead. Every propatm01 in the world now renders your live UI on its screen. Because it's a real texture on a real surface, it skews, lights, and sits in 3D space for free — you're not faking perspective, the engine is doing it. To actually update what's on screen, you don't reload anything. You message the page just like NUI, but with the DUI-specific native: lua SendDuiMessage(DUI.duiObject, json.encode({ action = 'SETSCREEN', data = { screen = 'balance' } })) Your React app listens for that message and re-renders. The texture updates automatically on the next frame. note One non-obvious gotcha we hit: CreateRuntimeTextureFromDuiHandle can fail silently if you call it the instant you make the TXD. A short Wait(250) after CreateRuntimeTxd — and another brief wait for the page to load before you rely on it — saved us a lot of "why is the screen black" debugging. The texture system needs a beat to catch up. The real problem: how do you click a screen that has no cursor? Here's the wall everyone hits. Your beautiful UI is now a texture warped onto a machine in 3D space. A player walks up… and there's no mouse. There's no cursor on that ATM screen. FiveM does expose SENDDUIMOUSEDOWN / SENDDUIMOUSEMOVE natives, but they take browser coordinates — an (x, y) inside the 1024×1024 page. You don't have that. You have a player looking at a skewed quad somewhere in Los Santos. Translating "the player is looking at the top-right of a tilted screen 40 metres away" into "pixel (812, 190) of the browser" is genuinely nasty geometry. So we flipped the problem around. Instead of turning a world look-point into a screen pixel, we project the buttons out into the world and check what the player is aiming at.  The trick works like this. Every button on the ATM face has a fixed offset from the prop's origin. When a session starts, we rotate each of those offsets by the ATM's heading and add it to the machine's world position — giving us the real 3D coordinate of every button in the world: lua local rotatedOffset = RotateVectorByQuaternion(btn.offset, headingQuat) local worldPos = atmCoords + rotatedOffset Then, every frame, we do the reverse of a mouse click. We lock the camera to the machine and put a small crosshair dead-center. For each button, we ask the engine where its world position lands on the screen, and measure how close that is to the center: lua local onScreen, screenX, screenY = GetScreenCoordFromWorldCoord(worldPos.x, worldPos.y, worldPos.z) local dist = math.sqrt((screenX - 0.5)^2 + (screenY - 0.5)^2) if dist < proximity then hoveredButton = btn.id end Whatever button is nearest the crosshair (within a small threshold) is the one the player is "hovering." We push that to the UI with a message so React can light it up, and if they left-click, we fire the button's logic and send a click animation back to the page: lua if hoveredButton ~= prevButtonId then SendDuiMessage(duiObject, json.encode({ action = 'HIGHLIGHTBUTTON', data = { buttonId = hoveredButton } })) end The player never actually touches the browser. They aim a crosshair at a machine; we do the 3D-to-2D math; the UI just receives "highlight button 5" and "click button 5" as plain messages. From the outside it feels like pointing at a real screen. Under the hood it's raycasting-by-projection, and the browser is blissfully unaware anyone is interacting with it at all. The details that bite A few honest caveats, because DUI is powerful but it's not a free lunch. AddReplaceTexture is experimental. The native is officially flagged as such, and it has a real history of breaking between client branches — it's worked reliably on Release while misbehaving on beta/latest builds at various points. If you ship something on DUI, test it across the branches your players actually run. You have to find the right texture name. Swapping the ATM screen means knowing the exact texture dictionary and texture name the game uses for that prop's screen — propcashpointscreen for propatm01, propfleeceemis for the Fleeca ATM, and so on. These aren't documented anywhere tidy; you dig them through model-viewing tools or lean on prior art. Get the name wrong and you're either replacing the machine's plastic housing or nothing at all. One browser, one texture — mind your resolution. A ~1300×1000 DUI is a full off-screen Chromium surface being uploaded to the GPU. It's cheap when it's a single ATM you only render during a session, but DUI is not something you want running on fifty props at once with no lifecycle management. We create the browser on demand and tear it down (DestroyDui, RemoveReplaceTexture) when the session ends. So when should you actually reach for DUI? Not for your HUD. Not for your inventory. Those want to be crisp, fullscreen, and mouse-driven — plain NUI is the right tool and always will be. DUI earns its keep the moment a UI should feel like it belongs to an object in the world: an ATM screen, a gas-pump display, a laptop, a TV showing a live feed, an in-world advertising board, a nightclub visualizer. Anything where "the interface is part of the scene" is the whole point. That immersion is exactly what a fullscreen panel can never give you, and it's why we went down this road for the ATM instead of shipping one more menu that covers the screen. > The difference between "a menu opened" and "I'm using that machine" is entirely in whether the UI lives in the world or on top of it. DUI is how you cross that line. Once you've seen a real, animated web app sitting on a screen in 3D space — reacting as you point at it — it's hard to go back to fullscreen panels for anything that's supposed to feel physical. --- We build this kind of thing for a living. The [OsmFX ATM system](https://github.com/OsmFX-Mods/osm-atmdui) is open source if you want to read the full implementation, and if you're after polished, performance-first scripts and UIs for your QBCore, ESX, or QBOX server, [have a look around the store](https://osmfxmods.com). We care about the details that make a server feel real.
Why backdrop-filter Blur Doesn't Work in FiveM (and What CEF Is Actually Doing)
Every FiveM developer hits this wall at some point. You've built a clean inventory or a HUD, you want that frosted-glass look everyone loves — a soft blur of the game showing through your panel — so you reach for the one line of CSS that does it everywhere else on the web: css .panel { backdrop-filter: blur(12px); } And… nothing. Or worse, a flickering mess, or a flat black rectangle where the blur should be. You didn't do anything wrong. This is one of those FiveM quirks that looks like a bug in your code but is actually baked into how the game renders your UI in the first place. I've shipped a lot of NUI over the years at OsmFX, and this is easily in the top five "why is this not working?" questions I get. So let me explain what's actually happening under the hood — not just "it doesn't work, use an image instead," but why, so the next time you design a UI you know exactly what you're fighting. callout The short version: backdrop-filter blurs the pixels behind an element within the browser's rendering context. In a standard FiveM NUI page, there are no game pixels behind your panel for CSS to sample. The game and your UI are rendered separately and only composited together at the very end, so backdrop-filter has nothing meaningful to blur. How FiveM actually draws a single frame When you play FiveM, what you see on screen isn't one image. It's two layers, rendered by two different systems and blended together right at the end:  The game view — Los Santos, cars, players, everything — is rendered by Rockstar's RAGE engine on the GPU. Your UI is something else entirely: FiveM embeds Chromium Embedded Framework (CEF), and your NUI resource is just a web page running inside it. CEF renders that page to its own transparent surface — think of a sheet of glass with your UI painted on it and transparency everywhere else. Finally, FiveM composites the browser surface over the game frame to produce what you see on screen. Here's the catch: a normal NUI page never receives the game's rendered image as part of its DOM or CSS rendering context. From the browser's point of view, behind your panel there is no street, no car, no sky — only transparency. So when backdrop-filter asks, "what's behind me so I can blur it?", CEF has no game pixels to sample. Depending on the Chromium version and GPU, that can result in no blur at all, black artifacts, or inconsistent rendering. The blur isn't broken. It's doing exactly what it's designed to do — there simply isn't a browser backdrop containing the game image. The second wall: every resource is its own sealed box Even if you got clever, there's another layer of isolation. Every NUI resource on your server runs in its own fullscreen iframe. Your HUD, your inventory, your phone — each is a separate browser context that can't directly access the DOM of another resource.  This is great for stability — a broken script can't take down every other UI — but it's also why you can't simply "grab the frame behind me and blur it" in JavaScript. There's no shared browser canvas containing the game framebuffer, no API that lets ordinary NUI pages read the rendered game image, and no direct DOM access between resources. Those boundaries are intentional. "But the FiveM main menu has a blur!" Right, and this is the part that drives people crazy. The FiveM main menu and some built-in interfaces do show a blurred game background. So it's clearly possible? It is, but not through ordinary backdrop-filter alone. note FiveM exposes special functionality such as x-cfx-game-view / CfxTexture that allows the live game view to be presented as an element inside certain CEF contexts. Once the game image exists as an actual browser-rendered element, CSS filters have real pixels to work with. The interface isn't blurring "whatever happens to be behind the page" — it's blurring a game texture that has been explicitly exposed to CEF. That functionality exists, but it's relatively specialized, sparsely documented, and not a drop-in replacement for the average inventory or HUD. What you can actually do about it Here's what holds up in production, roughly in order of how much pain each one saves you. Design around it — fake the blur Ninety percent of the time, the practical solution is to fake it. Use a pre-blurred background image, a blurred artwork, or another design treatment that gives the same visual feel. Pair it with a semi-transparent overlay like rgba(0,0,0,0.5) and most players will never notice the difference. It performs well, behaves consistently, and doesn't depend on browser quirks. Lean into solid, tinted, or gradient panels A well-designed opaque UI with good spacing, subtle borders, and tasteful transparency often looks more polished than an inconsistent live blur. Some of the cleanest FiveM UIs never use blur at all. Use the game-view APIs if you truly need live blur If a live, animated blurred background is genuinely a requirement, then it's worth exploring approaches built around x-cfx-game-view / CfxTexture. Some developers integrate these into more advanced rendering pipelines, including custom Three.js-based approaches. This is significantly more complex, may require maintenance across FiveM updates, and is best reserved for flagship interfaces where the visual effect justifies the engineering cost. Consider DUI for special cases For render targets and in-world screens, DUI (Direct-rendered UI) is a different rendering path with different capabilities. It's useful to understand, but it doesn't make ordinary HUD backdrop-filter effects suddenly work. warn One thing I'd avoid is spending an entire weekend trying to force backdrop-filter to behave in a normal NUI page. Chromium support inside FiveM has historically lagged behind the latest desktop Chrome releases, and rendering behavior can vary between versions and hardware. Even where the CSS property is recognized, it still lacks access to the game's framebuffer in a standard NUI context. The takeaway The reason backdrop-filter: blur() doesn't work in a normal FiveM NUI isn't because your CSS is wrong — it's because of how the rendering pipeline is designed. The game and your browser UI are rendered separately, and a standard NUI page has no access to the game's pixels while CSS is being evaluated. On top of that, every resource runs in its own isolated browser context. Once that clicks, a lot of other NUI quirks start making sense too: why you can't read the game's framebuffer from JavaScript, why resources can't directly style each other, and why certain effects that work perfectly on the web don't translate directly into FiveM. Good FiveM UI development is mostly this: understanding where the engine's boundaries are, and designing with them instead of fighting them. Blur is simply one of the first walls almost every developer runs into. > If you understand where the renderer's boundaries are, you stop writing CSS that fights them — and your UI ends up cleaner because of it. --- At [OsmFX Mods](https://osmfxmods.com) we build performance-first scripts and UIs for QBCore, ESX, and QBOX servers — the kind that look sharp without fighting the renderer. If you're putting together a server and want UI that actually holds up in production, [browse the store](https://osmfxmods.com). Got a FiveM quirk you want explained like this? That's exactly the kind of thing we write about here.

How Raycasting Actually Works in FiveM (Shapetests Explained)
Almost every interaction system you've ever used in a FiveM server is quietly asking the same question: what is the player looking at right now? Pointing at a door to open it, aiming at a ped to check their ID, placing a prop where your crosshair lands, the little "press E" that appears only when you face the right object — all of that comes down to firing an invisible line into the world and asking what it hit. That invisible line is a raycast. And the first time you go looking for a Raycast() function in FiveM, you won't find one. What you find instead is a family of natives with intimidating names like StartShapeTestLosProbe and GetShapeTestResult, a flags argument nobody explains, and forum threads full of people asking why their raycast "returns nothing." So let me actually walk through how this works under the hood — not just the copy-paste snippet, but why it's shaped the way it is, so the gotchas stop being mysterious. Here's the whole thing in a sentence, and then we'll unpack every word of it: FiveM doesn't give you an instant raycast — you start a shape test, then read the result once the engine has it (typically the following frame), and a set of flags decides what the ray is even allowed to hit. If that already clears things up, great, the rest of this post is the details. If it raised more questions than it answered, that's the right reaction — because two things in there trip up basically everyone: the "start now, read later" split, and the flags. Forget the native names for a second. Conceptually a raycast is dead simple. You have a start point and an end point — two sets of world coordinates — and the engine draws a straight line between them and tells you the first thing that line runs into. In GTA's engine (and therefore FiveM) this is called a shape test, because a ray is just one shape you can cast. You can also sweep a sphere, a box, or a capsule through the world, but a straight line ("LOS probe", for line-of-sight) is by far the most common, so that's what we'll focus on. When the ray hits something, the engine hands you four pieces of information: - hit — did it hit anything at all? (a boolean) - endCoords — the exact world position where it stopped - surfaceNormal — the direction the hit surface is facing (useful for placing props flush against walls) - entityHit — the handle of the entity it hit, or 0 if it hit the map itself (a wall, the road, terrain) That entityHit return is the quiet hero of interaction scripts. GetEntityType(entityHit) tells you instantly whether you hit a ped (1), a vehicle (2), or an object (3) — which is exactly how a targeting script knows whether to show you "talk to person" or "open trunk." Here's the design decision that confuses people. In most engines you'd call raycast(a, b) and get an answer back on the same line. FiveM's main shape test does not work that way. You call one native to kick off the test, and it immediately returns a handle — a little ticket number. The actual result isn't ready yet. You take that ticket to a second native, GetShapeTestResult, to collect the answer. FiveM officially documents this native as StartShapeTestLosProbe; you'll also see older examples and community wrappers refer to it as StartShapeTestRay. Notice the second native returns the values in Lua rather than filling in pointers like the raw C signature suggests; the FiveM Lua runtime handles that unpacking for you. The catch is that first return value, retval. It's a status code — the one you actually need to care about is 1, which means "not ready yet, ask again": So the correct pattern for an asynchronous shape test is to poll: start it, then keep calling GetShapeTestResult until the status is no longer 1, waiting a frame between tries. Don't assume a fixed number of frames — just loop until it's ready. Why the async split at all? Because a shape test isn't free — it runs against the physics world — and the engine wants to batch that work rather than stall your thread mid-frame every time you ask. Once you internalise "start now, read once it's ready," ninety percent of the "my raycast returns nothing" confusion disappears: people are reading the result on the same line they started it, before the engine has had a chance to answer. The single most common use is casting a ray straight out of the player's camera, so you can tell what they're aiming at. There's no native that does this in one call — you build the ray yourself from the camera's position and rotation. The origin is easy: GetGameplayCamCoord(). The direction takes a little trigonometry, converting the camera's rotation into a forward vector: Put those together and you've got a working "look at" cast. Here the ray starts at the camera, shoots 50.0 units forward, and ignores the player's own ped so you don't just hit yourself: That's the skeleton behind every third-eye / targeting resource you've used. qb-target, oxtarget, interaction menus — under all the polish, they're firing a camera ray and reacting to entityHit. This is the argument nobody explains and everyone copies blindly. The flags value is a bitmask that tells the shape test which categories of thing it should collide with. Leave out a category and the ray passes straight through it as if it weren't there — which is exactly why so many "my raycast ignores vehicles" posts exist. The ray isn't broken; the flag for vehicles just wasn't set. The values people rely on in practice: Flag Hits ------------ 1 Map / world geometry (buildings, terrain, roads) 2 Vehicles 4 Peds (non-player) 16 Objects & props -1 Everything Because it's a bitmask, you combine categories by adding them. Want only the map and vehicles? 1 + 2 = 3. Only props and peds? 4 + 16 = 20. And -1 is the "hit anything" catch-all, which is what most interaction casts use. A few things that aren't obvious until they bite you: This is client-side only. Shape tests run against the local physics/streaming world, so they live on the client. There's no server-side raycast — the server doesn't have the streamed collision geometry to test against. If the server needs to know what someone's looking at, the client casts the ray and reports the result (and then, per good server-authoritative design, the server validates that result rather than trusting it blindly). entityHit == 0 means you hit the world, not nothing. A common mix-up: people check entity = 0 to mean "hit something." But hit can be true with entityHit at 0 — that's a wall or the road. Use the hit boolean to know whether you hit, and entityHit to know what. The surface normal is not a world coordinate. surfaceNormal is a direction (which way the hit surface faces), not a position. It's gold for placing objects flush against a wall or aligning a decal, but if you try to treat it like endCoords you'll get nonsense. Don't fire one every single frame if you don't need to. A camera ray in a Wait(0) loop runs your shape test 60-plus times a second forever. For a "press E to interact" feature you only need to cast when the player presses E, or throttle a continuous highlight cast to something like every 200–300ms. This is the same dynamic-sleep discipline that keeps any FiveM loop off your resmon — casting rays is cheap individually and expensive in bulk. Raycasting feels like a big scary systems topic until you see the shape of it: it's a line, a handle you cash in a frame later, and a bitmask that decides what counts. Once those three ideas click, the whole family of interaction features — targeting, prop placement, "look at to interact," aiming logic, even parts of how weapons and cameras reason about the world — stops looking like magic and starts looking like the same small pattern reused. That's honestly the theme of most FiveM engine work. The natives have odd names and the docs are terse, but underneath there's usually one clean idea the engine is nudging you toward. Learn where that idea is, build with it, and your interaction systems end up both simpler and faster than the copy-pasted version. A raycast doesn't tell you what's there. It tells you what's there right now, on this client, along this exact line. Respect that, and half the weird bugs never happen. --- At OsmFX Mods we build interaction-heavy scripts for QBCore, ESX, and QBOX — targeting, placement, and "look at it to use it" systems that are cast efficiently and validated server-side, not fired blindly every frame. If you'd rather install things that already handle this cleanly, take a look around the store. And if you're building your own targeting system and something's behaving strangely, this is exactly the kind of problem I enjoy digging into — that's most of what we write about here.

9 FiveM Coding Anti-Patterns That Quietly Break Your Server
I spend a lot of time reading other people's FiveM scripts. Some of it is research, some of it is curiosity, and honestly some of it is just that I can't help opening a client.lua the way other people can't help checking their phone. And after enough of that, you start seeing the same handful of mistakes over and over — not exotic bugs, just patterns. Ways of writing code that technically work on an empty test server and then quietly fall apart the moment thirty real players show up. The tricky thing about anti-patterns is that most of them don't announce themselves. Your script loads, the feature works when you test it alone, the release thread gets a few likes. The damage shows up later — as server hitches nobody can trace, as an economy that slowly inflates, as a "why can't I get my car out of the garage" ticket that turns out to be three resources fighting over the same value. So I want to walk through the ones I see most, sorted roughly by how hard they are to catch. We'll start with the stuff you can spot by eye, move into the ones that need you to actually think about how the server behaves, and end with the quiet, critical ones that don't look like bugs at all until someone exploits them. Every point here is something I've either seen in a real release or run into myself. Before the list, one framing that's saved me a lot of grief. How visible a mistake is and how much damage it does are two completely different axes. A missing Wait crashes your client instantly — extremely visible, and you'll fix it in ten seconds. A missing server-side check does nothing at all until a cheater finds it six months in, and then it drains your economy overnight. So as we go from "easy" to "critical" here, we're not going from small to big. We're going from loud to silent. The loud ones train you. The silent ones are the ones that actually take servers down. The most famous FiveM footgun. A loop that never yields hangs the entire thread — and because everything in a resource shares that scheduler, an infinite loop with no Wait can freeze your whole client or server solid. Everyone learns this one fast, because it fails so hard you can't miss it. The Cfx.re docs on Citizen.CreateThread spell it out: a CreateThread loop needs a Wait inside it or it starves the scheduler. But the sneakier version of this isn't the missing Wait — it's the too-frequent one. Wait(0) runs your loop every single frame. That's correct and necessary for things that genuinely must draw every frame (like DrawMarker or DrawText3D), but people reach for it reflexively for logic that has no business running 60-plus times a second. The fix is just to ask "how often does this actually need to happen?" and set the wait accordingly. A HUD value that changes when your job changes can poll once a second, or better, only update when it's pushed a change. This is the single cheapest performance win in FiveM and it costs you nothing but a moment of thought. This is the same instinct as above but it shows up specifically around drawing and distance checks, and it's worth its own section because it's everywhere. DrawMarker and DrawText3D genuinely have to run every frame to appear — that part's unavoidable. What's avoidable is running the distance math and the loop over every marker every frame, including the markers that are 300 metres away and invisible. The pattern that fixes it is the "dynamic sleep" loop: default to a long wait, and only drop to Wait(0) when the player is actually near something that needs per-frame drawing. When nobody's near a marker, that loop sleeps a full second and costs essentially nothing on resmon. When you walk up to one, it snaps to per-frame drawing. Same visual result, a fraction of the cost. The community performance guides have been preaching this for years and it still gets ignored constantly. One small companion habit while you're in here: use (vectorA - vectorB) for distance, not GetDistanceBetweenCoords. The vector-length operator is a native Lua operation and measurably faster than the native call — small on its own, but it's the kind of thing that runs thousands of times a second. The third eye-catchable one: calling the same native over and over when one call would do. The classic is PlayerPedId() — I've genuinely seen scripts call it a dozen-plus times inside a single function, when the ped handle is the same for the whole call. Individually these calls are cheap. But cache them anyway — partly for the tiny performance gain, mostly because the cached version is easier to read and reason about, and readability is what stops the next three tiers of bugs from creeping in. Clean local variables are a load-bearing habit, not a cosmetic one. Here's one straight from a real release. While reviewing a free ESX plate-switcher script on the forums, I found this on the client: Read that carefully. Every client, ten times a second, forever, fires two server events — and on the server those events run database queries. So you've built a system where a hundred connected players generate two thousand database round-trips per second, whether or not anything changed. The data it's fetching updates maybe once every few minutes when someone tapes a plate. It's polling a mailbox ten times a second for a letter that arrives twice a day. This is the pattern to internalise: don't poll for state that changes rarely — push it when it changes. The server already knows the moment the plate data changes, because it's the one changing it. So have it TriggerClientEvent the affected client(s) at that moment, and let the client sit quietly the rest of the time. You go from thousands of queries a second to a handful an hour. For truly shared state, this is also exactly what state bags exist for — set a value once and it syncs to whoever needs it. This one is genuinely subtle and it was hiding in that same plate-switcher. The client stored fetched data into plain globals: That loop doesn't collect the data — it walks the whole dataset and keeps only the last row. Because those variables are global and unscoped, each client ends up tracking whatever the most recently taped vehicle on the entire server was. Play it forward. Player A tapes their car. Later, Player C tapes theirs. Player A's identifiern just got silently overwritten with Player C's identifier. So when Player A tries to un-tape their own vehicle, the ownership check: now fails, because identifiern belongs to Player C. In practice only the last person to tape anything can un-tape. Everyone else is locked out of their own action, and nobody can figure out why because the code "looks fine." The lesson isn't about plates. It's that any time data belongs to a specific player or entity, it needs to be scoped to that player or entity — a table keyed by identifier, a state bag on the entity, something with a per-owner slot. The moment you stash per-player data in a shared global, you've built a race condition that only shows up with more than one player online. Which is exactly why it survives testing and dies on launch night. Resources start and stop constantly on a live server — restarts, updates, crashes, txAdmin doing its thing. If your script spawns entities, opens threads, or creates blips and never cleans them up on stop, you leak. Restart a vehicle-spawner resource a few times and you'll find orphaned vehicles piling up in the network object viewer, each one still eating a network slot. The habit that prevents it is an onResourceStop handler that tears down everything the resource created: You won't notice a missing cleanup handler in a quick test. You notice it three weeks in, when the server's been up for days and something feels heavier than it should. If you take one thing from this whole post, take this. The client is in the hands of the player, and the player might be running a cheat menu. Any event you register with RegisterNetEvent can be triggered by any client, with any arguments they choose. A Lua executor makes TriggerServerEvent('bank:deposit', 99999999) as easy as typing it. So the anti-pattern is any server event that takes the client's word for something that matters. The Cfx.re server-security guide gives the canonical bad example: Nothing stops a player from calling that with ('goldbar', 9999). The fix isn't a single line — it's a mindset. Every event that changes money, items, or state must re-derive the truth on the server: does this player actually have an active job? Are they physically where the action requires? Is the amount within sane bounds? Have they done this suspiciously often? The docs' secure example gates item-giving behind a server-tracked job state and a distance check, and resets that state after use so it can't be replayed. That's the shape you want. Three concrete habits that go a long way: First, register events in the right context. Use RegisterNetEvent only for events that genuinely need to cross client↔server. For internal events, AddEventHandler keeps them non-networked so a cheat client can't reach them at all. You can even hard-block same-context calls by checking if source = 65535 then return end on server events that should only ever come from the server itself. Second, validate every input as if it's hostile, because it might be. Never addItem(item, count) from raw client arguments. Third, rate-limit anything that touches the economy, so even a "valid" event can't be spammed a thousand times a second to duplicate money or farm items. This is the one that keeps me up a little. SQL injection is ancient, well-understood, and still shows up in FiveM resources — usually anywhere user input reaches the database. The pattern looks harmless: If newName is a normal name, fine. If newName is ', money = 999999 WHERE 1=1 -- , you've just handed a player the ability to rewrite your entire players table. Any input field — a character name, a /report reason box, an admin note — is a door if you concatenate it straight into SQL. There are real FiveM admin panels that shipped with exactly this hole. The fix is total and simple: always use parameterized queries. Every modern FiveM database layer — oxmysql, mysql-async — supports ? placeholders that bind values safely instead of splicing them into the query text: With placeholders, newName is always treated as a string value, never as query structure, no matter what characters it contains. There is essentially never a good reason to concatenate user input into a query. If you see .. someVariable .. inside a SQL string in a resource you're evaluating, treat it as a red flag and go read the rest of that resource carefully — where there's one, there are usually more. The most invisible one, and the hardest to reason about, because it's about timing and ownership in a networked game. In OneSync, entities are owned by whichever client is closest / streaming them, ownership migrates as players move, and a network ID you got a moment ago might refer to an entity that no longer exists on your client — or that you don't own and therefore can't modify. The anti-pattern is code that assumes an entity handle is always valid and always yours: If that vehicle isn't streamed on your client, or another client owns it, this can quietly fail — no error, just nothing happens. And state bags only sync reliably once an entity is properly networked and within range, so setting a state bag on an entity you've just requested, before it actually exists everywhere, leads to values that mysteriously don't propagate. There's no one-line fix, but the defensive habits are: always guard with DoesEntityExist before you touch a handle; if you must modify an entity you don't own, request control and wait for it rather than assuming; prefer server-side entity creation and state for things that must be authoritative; and remember that a network ID is a reference across the session, not a guarantee the thing is on your screen right now. This is the category of bug that produces "it works for me but not for my friend" reports, and the reason is almost always that two clients disagreed about who owned what, and when. If you line all nine up, there's a single thread running through them. The easy ones are about respecting the frame — not doing work the game doesn't need done. The moderate ones are about respecting time and other players — data that's scoped correctly and pushed instead of polled. And the critical ones are all one idea wearing different clothes: decide where the truth lives, and never trust anything on the wrong side of that line. The client isn't the truth. A network ID isn't a guarantee. A raw string isn't safe input. None of this requires being a genius. It requires slowing down for a second and asking who owns this data, how often does this really need to run, and what happens when the person on the other end is hostile. That second of thought is, honestly, most of what separates a script that survives launch night from one that becomes a support ticket. A resource that works when you test it alone is not a resource that works. It's a resource that hasn't been tested yet. That's the whole game. The engine has rules about frames, about ownership, about trust — and good FiveM development is mostly learning where those rules are and building with them instead of finding out the hard way. --- At OsmFX Mods this is basically our whole philosophy — performance-first, server-authoritative scripts for QBCore, ESX, and QBOX that are built to hold up under real player load, not just pass a solo test. If you'd rather install things that already got this stuff right, have a look around the store. And if you've got a script you're not sure about, reviewing code like this is genuinely one of my favourite things to do — that's exactly the kind of thing we write about here.

How to Render a Live Web App onto a 3D Screen in FiveM (DUI Explained)
Walk up to an ATM in most FiveM servers and you get a menu that opens in front of the world — a fullscreen NUI panel floating over the game. It works, but it always breaks the illusion a little. Your camera is standing at the machine, and yet the interface is pasted across your whole monitor. So when we built our ATM system, we wanted the interface to live on the machine — a real, animated banking UI rendered onto the actual ATM screen texture, warping and sitting in 3D space exactly where your eyes expect it. Card animations, a keypad, balance screens, the works, all glued to the prop. That's not a fancier NUI. It's a completely different rendering path called DUI, and it comes with one genuinely tricky problem nobody warns you about. Let me walk through how it actually works — and then the part that took the longest to get right. If you've read our post on how FiveM renders your UI, you already know the important bit: your UI is a web page running inside an embedded Chromium browser (CEF), and FiveM composites that on top of the game at the end of each frame. Normal NUI takes that browser output and stretches it across your entire screen. That's why an inventory or a HUD sits over everything. DUI — "direct-rendered UI" — takes the exact same kind of web page and renders it to a runtime texture instead of your screen. A texture is just an image the game engine can paint onto any surface. Once your UI is a texture, you can slap it onto an ATM screen, a TV, a billboard, a laptop prop — anywhere the engine draws pixels. Same React app. The only difference is where the final image lands. Getting a web page onto a prop is a five-step handshake between CEF and the game's texture system. Here's the whole chain we use, start to finish. First, you spin up the browser off-screen: CreateDui loads your page into a browser that renders nowhere yet, at a fixed resolution (1326×1004 in our config — pick something that matches the aspect ratio of the screen you're targeting). GetDuiHandle hands you back a reference to that browser's output — the live pixels. Next, you wrap those pixels as something the game can actually draw: CreateRuntimeTxd makes an empty texture dictionary — think of it as a named folder the engine can look textures up in. CreateRuntimeTextureFromDuiHandle then drops your live browser output into that folder as a texture called atmduitex. From here on, the game treats your React app as if it were any other image asset. Finally, the magic swap: AddReplaceTexture tells the engine: anywhere you would have drawn the ATM's original screen texture, draw mine instead. Every propatm01 in the world now renders your live UI on its screen. Because it's a real texture on a real surface, it skews, lights, and sits in 3D space for free — you're not faking perspective, the engine is doing it. To actually update what's on screen, you don't reload anything. You message the page just like NUI, but with the DUI-specific native: Your React app listens for that message and re-renders. The texture updates automatically on the next frame. Here's the wall everyone hits. Your beautiful UI is now a texture warped onto a machine in 3D space. A player walks up… and there's no mouse. There's no cursor on that ATM screen. FiveM does expose SENDDUIMOUSEDOWN / SENDDUIMOUSEMOVE natives, but they take browser coordinates — an (x, y) inside the 1024×1024 page. You don't have that. You have a player looking at a skewed quad somewhere in Los Santos. Translating "the player is looking at the top-right of a tilted screen 40 metres away" into "pixel (812, 190) of the browser" is genuinely nasty geometry. So we flipped the problem around. Instead of turning a world look-point into a screen pixel, we project the buttons out into the world and check what the player is aiming at. The trick works like this. Every button on the ATM face has a fixed offset from the prop's origin. When a session starts, we rotate each of those offsets by the ATM's heading and add it to the machine's world position — giving us the real 3D coordinate of every button in the world: Then, every frame, we do the reverse of a mouse click. We lock the camera to the machine and put a small crosshair dead-center. For each button, we ask the engine where its world position lands on the screen, and measure how close that is to the center: Whatever button is nearest the crosshair (within a small threshold) is the one the player is "hovering." We push that to the UI with a message so React can light it up, and if they left-click, we fire the button's logic and send a click animation back to the page: The player never actually touches the browser. They aim a crosshair at a machine; we do the 3D-to-2D math; the UI just receives "highlight button 5" and "click button 5" as plain messages. From the outside it feels like pointing at a real screen. Under the hood it's raycasting-by-projection, and the browser is blissfully unaware anyone is interacting with it at all. A few honest caveats, because DUI is powerful but it's not a free lunch. AddReplaceTexture is experimental. The native is officially flagged as such, and it has a real history of breaking between client branches — it's worked reliably on Release while misbehaving on beta/latest builds at various points. If you ship something on DUI, test it across the branches your players actually run. You have to find the right texture name. Swapping the ATM screen means knowing the exact texture dictionary and texture name the game uses for that prop's screen — propcashpointscreen for propatm01, propfleeceemis for the Fleeca ATM, and so on. These aren't documented anywhere tidy; you dig them through model-viewing tools or lean on prior art. Get the name wrong and you're either replacing the machine's plastic housing or nothing at all. One browser, one texture — mind your resolution. A 1300×1000 DUI is a full off-screen Chromium surface being uploaded to the GPU. It's cheap when it's a single ATM you only render during a session, but DUI is not something you want running on fifty props at once with no lifecycle management. We create the browser on demand and tear it down (DestroyDui, RemoveReplaceTexture) when the session ends. Not for your HUD. Not for your inventory. Those want to be crisp, fullscreen, and mouse-driven — plain NUI is the right tool and always will be. DUI earns its keep the moment a UI should feel like it belongs to an object in the world: an ATM screen, a gas-pump display, a laptop, a TV showing a live feed, an in-world advertising board, a nightclub visualizer. Anything where "the interface is part of the scene" is the whole point. That immersion is exactly what a fullscreen panel can never give you, and it's why we went down this road for the ATM instead of shipping one more menu that covers the screen. The difference between "a menu opened" and "I'm using that machine" is entirely in whether the UI lives in the world or on top of it. DUI is how you cross that line. Once you've seen a real, animated web app sitting on a screen in 3D space — reacting as you point at it — it's hard to go back to fullscreen panels for anything that's supposed to feel physical. --- We build this kind of thing for a living. The OsmFX ATM system is open source if you want to read the full implementation, and if you're after polished, performance-first scripts and UIs for your QBCore, ESX, or QBOX server, have a look around the store. We care about the details that make a server feel real.

Why backdrop-filter Blur Doesn't Work in FiveM (and What CEF Is Actually Doing)
Every FiveM developer hits this wall at some point. You've built a clean inventory or a HUD, you want that frosted-glass look everyone loves — a soft blur of the game showing through your panel — so you reach for the one line of CSS that does it everywhere else on the web: And… nothing. Or worse, a flickering mess, or a flat black rectangle where the blur should be. You didn't do anything wrong. This is one of those FiveM quirks that looks like a bug in your code but is actually baked into how the game renders your UI in the first place. I've shipped a lot of NUI over the years at OsmFX, and this is easily in the top five "why is this not working?" questions I get. So let me explain what's actually happening under the hood — not just "it doesn't work, use an image instead," but why, so the next time you design a UI you know exactly what you're fighting. When you play FiveM, what you see on screen isn't one image. It's two layers, rendered by two different systems and blended together right at the end: The game view — Los Santos, cars, players, everything — is rendered by Rockstar's RAGE engine on the GPU. Your UI is something else entirely: FiveM embeds Chromium Embedded Framework (CEF), and your NUI resource is just a web page running inside it. CEF renders that page to its own transparent surface — think of a sheet of glass with your UI painted on it and transparency everywhere else. Finally, FiveM composites the browser surface over the game frame to produce what you see on screen. Here's the catch: a normal NUI page never receives the game's rendered image as part of its DOM or CSS rendering context. From the browser's point of view, behind your panel there is no street, no car, no sky — only transparency. So when backdrop-filter asks, "what's behind me so I can blur it?", CEF has no game pixels to sample. Depending on the Chromium version and GPU, that can result in no blur at all, black artifacts, or inconsistent rendering. The blur isn't broken. It's doing exactly what it's designed to do — there simply isn't a browser backdrop containing the game image. Even if you got clever, there's another layer of isolation. Every NUI resource on your server runs in its own fullscreen iframe. Your HUD, your inventory, your phone — each is a separate browser context that can't directly access the DOM of another resource. This is great for stability — a broken script can't take down every other UI — but it's also why you can't simply "grab the frame behind me and blur it" in JavaScript. There's no shared browser canvas containing the game framebuffer, no API that lets ordinary NUI pages read the rendered game image, and no direct DOM access between resources. Those boundaries are intentional. Right, and this is the part that drives people crazy. The FiveM main menu and some built-in interfaces do show a blurred game background. So it's clearly possible? It is, but not through ordinary backdrop-filter alone. That functionality exists, but it's relatively specialized, sparsely documented, and not a drop-in replacement for the average inventory or HUD. Here's what holds up in production, roughly in order of how much pain each one saves you. Ninety percent of the time, the practical solution is to fake it. Use a pre-blurred background image, a blurred artwork, or another design treatment that gives the same visual feel. Pair it with a semi-transparent overlay like rgba(0,0,0,0.5) and most players will never notice the difference. It performs well, behaves consistently, and doesn't depend on browser quirks. A well-designed opaque UI with good spacing, subtle borders, and tasteful transparency often looks more polished than an inconsistent live blur. Some of the cleanest FiveM UIs never use blur at all. If a live, animated blurred background is genuinely a requirement, then it's worth exploring approaches built around x-cfx-game-view / CfxTexture. Some developers integrate these into more advanced rendering pipelines, including custom Three.js-based approaches. This is significantly more complex, may require maintenance across FiveM updates, and is best reserved for flagship interfaces where the visual effect justifies the engineering cost. For render targets and in-world screens, DUI (Direct-rendered UI) is a different rendering path with different capabilities. It's useful to understand, but it doesn't make ordinary HUD backdrop-filter effects suddenly work. The reason backdrop-filter: blur() doesn't work in a normal FiveM NUI isn't because your CSS is wrong — it's because of how the rendering pipeline is designed. The game and your browser UI are rendered separately, and a standard NUI page has no access to the game's pixels while CSS is being evaluated. On top of that, every resource runs in its own isolated browser context. Once that clicks, a lot of other NUI quirks start making sense too: why you can't read the game's framebuffer from JavaScript, why resources can't directly style each other, and why certain effects that work perfectly on the web don't translate directly into FiveM. Good FiveM UI development is mostly this: understanding where the engine's boundaries are, and designing with them instead of fighting them. Blur is simply one of the first walls almost every developer runs into. If you understand where the renderer's boundaries are, you stop writing CSS that fights them — and your UI ends up cleaner because of it. --- At OsmFX Mods we build performance-first scripts and UIs for QBCore, ESX, and QBOX servers — the kind that look sharp without fighting the renderer. If you're putting together a server and want UI that actually holds up in production, browse the store. Got a FiveM quirk you want explained like this? That's exactly the kind of thing we write about here.

Interior Detection
I've mention this on the discord but I'll put it here as well. The need, optionally, is for interior detection. The point is to allow players to finish their robberies (shop, liquor store) and heists (bobcat, casino) before police are dispatched. For example, some shop robberies dispatch police immediately when the first register is broken open and players are unable to complete the rest of the robbery because AI Police are shooting them through the window. The other example are heists like Casino where players are so deep into a shell that the police have been evaded before they even leave the shell.
The idea is to detect when the player is inside interiors (i.e. inside a shop, or a shell like the casino heist) and only dispatch police when players exit the interior. This allows players to finish their robberies and exit the interiors before needing to evade police. This also solves the issue of evading police by entering interiors.
All of this can be achieved using state bags and player metadata. A thread containing a loop that monitors interior detection when players are wanted is all that is needed. When players are inside, suppress police presence, as soon as players leave the interior, dispatch the police. While inside interiors or if a player disconnects, their wanted status (stars) are safely stored in player metadata so upon existing interiors or returning online, the police can engage the player.
I can share a snippet I have for this if needed. TIA.

Update 2.1.0
Changelog
Features and Bugs Resolved
New Features
- Revive players immediately upon arrest instead of at the jail, preventing them from exploiting the interaction to be transported to the hospital when the bleed-out timer is shorter than the transport time.
- Remove specified items from a player's inventory upon jailing, based on configurable settings. Item removal is permanent, supports wildcards (for example
weapon_*), and works with ox_inventory. - Support configurable jail durations for different arrest events.
- Add configurable delayed wanted-level activation for specific scenarios, such as heists (for example, dispatch the police a set number of seconds after the crime).
- Optional on-screen countdown warnings before a delayed wanted level is triggered.
- Provide both client-side and server-side exports, plus a simple trigger event, to enable seamless integration with external resources — including a dedicated guide (
INTEGRATION.md) for escrowed/closed-source scripts.
Bugs Resolved
- Fixed an issue where wanted levels could still be applied to a player after they had already surrendered, were being transported, or were jailed. No new crimes are registered once a player is in custody.
- Fixed players getting stuck and unable to move after being handcuffed, and their arrest animation glitching, caused by other police scripts trying to apply their own handcuff behaviour at the same time.
- Fixed the arrest sequence so a living, surrendering player now correctly walks to the transport vehicle and gets in, instead of standing still and being teleported inside.
- Fixed the death arrest so players are no longer teleported to a spawn point the moment they die while wanted. The transport now arrives, the officer revives them, and the arrest continues normally.
- Fixed police voice lines not playing during pursuits and arrests.
⚠️ Caution — please read before updating This update includes changes to the bridge files (
client/bridge.luaandserver/bridge.lua). If you have customized your own bridge files (for example, to fit a custom framework, jail, hospital, or inventory system), do not blindly overwrite them with the new versions. Back up your current bridge files first, then merge the changes in manually so your customizations are preserved.