Lua, JavaScript or C#: Choosing Your FiveM Scripting Language

Lua is the right default for most resources, and it isn’t a close call. But a default is not a verdict, and picking a FiveM scripting language badly costs you in ways that don’t show up until month three. The two failure modes I see most: a solo dev who forces C# onto a QBCore stack and burns a month writing bridge code, and a web team that writes everything in TypeScript and then finds out the client runtime has no fetch, no DOM, and no npm package that touches either.

All three runtimes ship with FXServer. All three will run a job script. What separates them is what happens around the code — the libraries you can reach, the people who can help you at 2am, the build step between you and a restart, and whether you can sell the thing when it’s done.

What each FiveM runtime actually is

FiveM runs three script runtimes side by side: CitizenFX Lua, a customised Node.js/V8 stack for JavaScript, and Mono for C#. Resources declare their own runtime through the file extension in fxmanifest.lua, and every runtime can call every native. The differences are in tooling, standard library, and how much of each runtime is available on the client versus the server.

Runtime Server side Client side Build step
Lua CitizenFX Lua; add lua54 'yes' to opt the resource into 5.4 Identical None
JavaScript Customised Node.js 16, or node_version '22' in the manifest Bare V8 with the ES2017 library — no Node, no browser APIs Bundler, in practice
C# Mono targeting .NET Framework, loaded as *.net.dll Same Mono sandbox MSBuild on every change

That third column is the one people skim past, and it’s the one that generates the most confused forum posts.

The client/server split nobody warns you about

JavaScript on the FiveM server is real Node. You can require built-ins, you get a node_modules/ folder, and the yarn resource will install your dependencies as part of startup. Pull in a rate limiter, a Discord library, a proper HTTP client — it works.

JavaScript on the client is not Node. It’s a V8 sandbox with the ES2017 standard library and nothing else: no DOM, no localStorage, no IndexedDB, no WebGL, no npm package that assumes any of those exist. Half the “why doesn’t this library work” threads in the CFX forums are someone importing a browser-targeted package into a client script. Your NUI page is a real browser and can use all of that. Your client script is not your NUI page.

Two more Node-specific traps worth knowing before you commit:

  • Thread affinity. Natives can only be invoked on the main game thread. Call one from inside an async callback or a promise resolution and you get “No current resource manager”. The fix is to wrap the native call in setImmediate() so it lands back on the right thread — obvious once you’ve hit it, mystifying before.
  • The exports collision. Bundlers like webpack rewrite exports for CommonJS, which quietly shadows FiveM’s global of the same name. Reach for globalThis.exports.resourceName.method() instead of the bare identifier and the problem disappears.

C# gets a cleaner story here — the same Mono sandbox on both sides — but the sandbox targets .NET Framework, not modern .NET. Support for net6/net8 has been discussed in the CitizenFX repo for a long while without anyone actively building it, and the realistic proposal is a server-only runtime, because sandboxing a modern .NET runtime on the client is a much harder problem. If your plan involves modern .NET libraries, check whether they still ship a Framework-compatible target before you architect around them.

Your integration surface is Lua whether you like it or not

This is the argument that actually decides it for most servers, and it has nothing to do with syntax preference.

Every escrowed script you buy, every framework you build on, and every config file you edit is Lua. ox_lib is Lua-first. oxmysql is Lua-first. QBCore and ESX are Lua top to bottom. When you install a paid inventory or dispatch resource from a FiveM script marketplace and it needs a bridge to your framework, you are writing that bridge in Lua — the vendor’s config.lua and their exposed exports are the seam you’re working against.

ox_lib does publish a JS/TS wrapper on npm as @overextended/ox_lib, with real TypeScript definitions and TSDoc that function as the API reference. It’s genuinely useful. It also doesn’t cover every Lua function — only the ones with a documented JS example — and you still need the ox_lib resource running regardless. So TypeScript buys you types over a subset, not independence from Lua.

The hiring and support angle follows from the same fact. Paste a Lua snippet in a development Discord and you’ll have three answers before you finish your coffee. Paste a C# BaseScript subclass and you’ll get silence, because the pool of people who write C# and know FiveM natives is small. If you run a QBCore server and expect community contributors to touch your code, Lua is the language they already read.

Performance: what actually shows up in resmon

The honest version: for the kind of code server owners write, language choice is not what’s on your resmon graph.

Community benchmarks — the ones AvarianKnight maintains and argues from in the CFX runtime threads — put C# under the mono v2 runtime ahead of Lua on raw native throughput, with JavaScript trailing both. That ordering is real and worth knowing. It is also swamped, in every production server I’ve looked at, by tick design: a Citizen.Wait(0) loop doing a distance check on every frame will cost you more than any runtime ever will.

Two runtime characteristics that do matter in practice:

  1. Managed runtimes collect garbage. C# and JavaScript both pause to clean up, so their cost profile is spikier — lower average, occasional jumps — where Lua tends to sit flatter. If you’re chasing frame hitches rather than average ms, that shape matters more than the mean.
  2. mono v2 is opt-in and has setup cost. It swaps the standard .NET Task scheduler for a Coroutine scheduler built for the game loop, and you ship CitizenFX.FiveM.Native.dll alongside your resource for compatibility. The performance win is real; it’s not free.

If a resource is heavy, profile the tick before you blame the runtime. Nobody has ever fixed a 4ms script by rewriting it in C#.

The dev loop, minute by minute

Iteration speed is the trade-off that compounds hardest, because you pay it several hundred times a week.

  • Lua: save the file, restart myresource in the console, test. No build, no artefacts, no toolchain. Sub-second.
  • TypeScript/JavaScript: keep a bundler in watch mode, wait for the rebuild, then restart the resource. Add a second or two per cycle, plus a node_modules tree per resource that your server tarball now carries.
  • C#: build with MSBuild — the dotnet new cfx-resource template from CitizenFX.Templates gives you a build.cmd that emits client and server DLLs into dist — then restart. Compile time on a cold build is measured in seconds, not milliseconds, and you’re context-switching to an IDE to get it.

None of this is fatal. But when you’re tuning a heist’s timing values by feel, the Lua loop lets you make thirty small adjustments in the time C# gives you ten. That’s a real difference in how the finished thing feels.

Escrow and shipping paid resources

If you plan to sell what you build, check escrow support before you write a line. The official asset escrow documentation has long listed Lua, YFT, YDD and YDR as the protected file types, while the Fall 2025 release notes described escrow being extended to cover JavaScript and C# as well. Documentation and shipped behaviour have drifted apart here more than once.

Treat it as a build-blocking question, not a detail. Push a test resource in your chosen language through the escrow flow on your Keymaster account and confirm the files actually come back protected. Discovering that your C# DLL ships in the clear after three months of work is an expensive way to learn. The same caution applies to anything you distribute through a storefront — worth reading up on packaging and protection alongside the other FiveM scripting guides before your first release.

Mixing languages in one resource stack

Here’s the part that dissolves most of the argument: you don’t pick a language for your server. You pick one per resource.

Exports are runtime-agnostic. A function exported from a Lua resource is callable from C# as Exports["myresource"].MyFunction(), from JavaScript as exports.myresource.myFunction(), and from Lua as exports.myresource:MyFunction(). Net events cross runtime boundaries the same way. Your framework can be Lua, your analytics collector can be TypeScript, and your one CPU-bound simulation resource can be C#, all in the same server.

The boundary isn’t completely free. Values crossing between runtimes get serialised, so keep the interface to plain data — strings, numbers, tables and arrays. Passing callbacks, class instances or anything with behaviour across a runtime boundary either fails or degrades into something you’ll debug badly. Design cross-runtime exports the way you’d design a small HTTP API: flat payloads, explicit fields, no cleverness.

Which language should you actually write?

  • Lua — anything touching your framework, your store scripts, or your configs. That’s 90% of what a server owner writes. Zero build step, the largest example corpus in the ecosystem, and the language your contributors already know.
  • TypeScript — server-side services and NUI-heavy resources. If a resource is mostly data handling, external APIs, or a large UI, types earn their keep and npm saves you writing HTTP and validation by hand. Keep it server-side where Node actually exists.
  • C# — only when you already have .NET people. A team that ships .NET daily will be productive immediately, and mono v2 gives them a real performance ceiling. A solo dev learning C# to write FiveM scripts is choosing the smallest community and the slowest dev loop at the same time.

Start with Lua, earn your way out

Write your first dozen resources in Lua. Not because it’s the best language in the abstract, but because it’s the one your framework, your paid scripts and your helpers all speak — and because a build step you don’t need is friction you don’t need. When you eventually hit a resource where types would have saved you, or where a profiler genuinely points at runtime overhead, you’ll know exactly which one to reach for, and exports will let you bolt it on without rewriting anything else.