Skip to main content
TRACK B·DATA AND PERSISTENCE·Verified June 2026 · Lua 5.4 · ox_lib 3.x
Learning with an AI assistant?
Copies this lesson plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.

Persistence: cache + write-back

Reading and writing the database on every high-frequency action adds avoidable load. The cache plus write-back pattern keeps working state in RAM, marks it dirty when it changes, and flushes snapshots on a timer and at graceful lifecycle points. It reduces writes in exchange for a bounded crash-loss window, so it is a deliberate tradeoff rather than a guarantee that no data can ever be lost.

You'll learn
In-memory cache, dirty flags, periodic flush, playerDropped save, txAdmin graceful shutdown
Time
~24 minutes
Prereqs
P1 (oxmysql), P4 (promises)
Outcome
A wallet that loads from the database, changes in memory, and writes back at most once every 30 seconds, with explicit crash-loss tradeoffs
BEFORE YOU START

Build it

Make the resource folder

The server has one folder for this lesson.

Inside your server's resources folder, create this folder:

code
resources/qu_persistence

Create the files

Every file named in the manifest exists.

Create this exact file layout:

code
resources/qu_persistence/
fxmanifest.lua
server.lua

Write fxmanifest.lua

FiveM knows which files to load.

Open fxmanifest.lua and paste this:

code
fx_version 'cerulean'
game 'gta5'

server_script '@oxmysql/lib/MySQL.lua'
server_script 'server.lua'

dependencies {
'oxmysql'
}

oxmysql is the database library this lesson talks to. It is an Overextended (community) resource, not something Cfx.re ships in the box, so you install and start it yourself. The dependencies { 'oxmysql' } block tells FiveM to load oxmysql before this resource, so the MySQL global exists by the time your server.lua runs. You will see the singular dependency 'oxmysql' in older tutorials. It still works; the docs list both forms, and the plural block is the one to reach for when a resource has more than one dependency.

Now create the table. This lesson assumes MariaDB, which is the 2026-preferred database for FiveM servers. oxmysql speaks to MariaDB and MySQL identically, so the SQL below is the same either way. Run it once in a database client that is connected to the same database your mysql_connection_string points at (for example HeidiSQL or DBeaver), before you start the resource:

code
CREATE TABLE IF NOT EXISTS qu_persistence_rows (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
amount INT NOT NULL DEFAULT 0,
UNIQUE KEY uniq_name (name)
);

The UNIQUE KEY uniq_name (name) line is load-bearing and the next section explains why. Without it, the write-back silently breaks.

Write the lesson code

The topic is now represented by runnable code.

Open server.lua and paste this:

code
local cache = {
cash = 0,
dirty = false,
version = 0,
loaded = false,
flushing = false,
}

local function flush()
if not cache.loaded or not cache.dirty or cache.flushing then return false end

cache.flushing = true
local amount = cache.cash
local version = cache.version
local ok, err = pcall(MySQL.query.await,
    'INSERT INTO qu_persistence_rows (name, amount) VALUES (?, ?) ON DUPLICATE KEY UPDATE amount = VALUES(amount)',
    { 'wallet', amount }
)
cache.flushing = false

if not ok then
    print('[qu_persistence] flush failed: ' .. tostring(err))
    return false
end

-- Do not clear a newer change that happened while the query was awaiting.
if cache.version == version then cache.dirty = false end
print('[qu_persistence] flushed ' .. amount)
return true
end

RegisterCommand('cashadd', function(_, args)
if not cache.loaded then
    print('[qu_persistence] wallet is still loading, try again')
    return
end

local amount = tonumber(args[1])
if not amount then
    print('[qu_persistence] usage: cashadd <number>')
    return
end

cache.cash = cache.cash + amount
cache.version = cache.version + 1
cache.dirty = true
print('[qu_persistence] cached ' .. cache.cash)
end, true)

CreateThread(function()
local saved = MySQL.scalar.await(
    'SELECT amount FROM qu_persistence_rows WHERE name = ?',
    { 'wallet' }
)
cache.cash = tonumber(saved) or 0
cache.loaded = true
print('[qu_persistence] loaded ' .. cache.cash)

while true do
    Wait(30000)
    flush()
end
end)

AddEventHandler('playerDropped', function()
flush()
end)

AddEventHandler('onResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end
flush()
end)

Start and test it

The expected proof appears in the correct console.

Open server.cfg and add this line:

code
ensure qu_persistence

Save the file. The ensure qu_persistence line in server.cfg makes the server start this resource on boot. To apply your change now without rebooting, type the next command into your live server console (the txAdmin Live Console, or the black FXServer console window if you run it directly):

code
restart qu_persistence

Run this test. In the same server console, type just this command and press Enter:

code
cashadd 25

Then leave the server running and wait about 30 seconds for the timer to flush.

Now run restart qu_persistence again and wait for [qu_persistence] loaded 25. That line proves the new Lua cache hydrated from the saved row instead of resetting to zero. Run SELECT amount FROM qu_persistence_rows WHERE name = 'wallet' in your database tool and confirm there is exactly one row, not a new one for every flush.

Keep reading the full lesson

Sign in to start, then unlock every step of this lesson and the full FiveM School with a membership.

Still ahead in this lesson
  • How it works
  • If something went wrong
  • What you can do now
  • Try it yourself

The remainder of Persistence: cache and write-back is available to FiveM School members.