Your MySQL database is the backbone of your FiveM server—player data, inventory, vehicles, everything lives here. One corrupted database can wipe months of progress. Learn the essential database management practices that prevent disasters, optimize performance, and keep your server running smoothly even with 100+ concurrent players.
Here's a story that happens more often than it should:
It's Saturday night. Your server is packed with 120 players. Then, suddenly, nobody can save their inventory. Cars are disappearing. Money isn't persisting. Your Discord is exploding with angry messages.
You check your database. It's corrupted. Three months of player data: gone.
No backups. No recovery plan. Just... gone.
This happens to server owners every single week. Not because they're stupid—because nobody teaches proper database management for FiveM servers.
This guide will teach you everything you need to know: from basic setup to advanced optimization, backup strategies, and disaster recovery. By the end, you'll manage your database like a professional.
💾 Before You Continue
If you don't have automated backups set up RIGHT NOW, stop reading and set them up first. Seriously. Everything else can wait. Backups are not optional—they're the difference between a recoverable problem and a dead server.
MySQL is a database management system—think of it as an organized filing cabinet for all your server data:
FiveM frameworks (ESX, QBCore, QBox) all use MySQL because it's:
You'll hear both terms. Here's the quick explanation:
Bottom line: They're interchangeable for FiveM. Use whichever your hosting provider offers. MariaDB is slightly better, but the difference is minimal.
Most server owners use Windows. Here's the proper setup:
C:\xampp (avoid Program Files)Default MySQL settings are NOT optimized for FiveM. Here's what to change:
Location: C:\xampp\mysql\bin\my.ini or /etc/mysql/my.cnf
[mysqld]
# Connection settings
max_connections = 200              # Allow more simultaneous connections
max_connect_errors = 1000000      # Prevent lockouts
# Performance optimization
innodb_buffer_pool_size = 2G      # Use 50-70% of available RAM
innodb_log_file_size = 256M       # Larger log files for better write performance
innodb_flush_log_at_trx_commit = 2  # Better performance, minimal risk
# Query cache (only for MySQL 5.7 and older)
query_cache_type = 1
query_cache_size = 128M
# Character set (CRITICAL for FiveM)
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# Timeouts
wait_timeout = 600                # 10 minutes before idle timeout
interactive_timeout = 600
After editing: Restart MySQL service for changes to take effect.
⚠️ RAM Warning
Don't set innodb_buffer_pool_size higher than your available RAM. If you have 4GB RAM total, set this to 1G-2G maximum. Check your server's RAM with Task Manager before adjusting.
Consistent naming prevents errors and makes management easier:
user_accountsusers, vehicles, itemsplayer_inventory not just inventoryfirst_name, created_atuser_id instead of just id in junction tablescreated_at, updated_at, deleted_atid as primary key nameINT AUTO_INCREMENT for most casesCHAR(40) for identifier strings (like Steam IDs)Wrong data types waste space and hurt performance:
VARCHAR(255) for everything (wastes space)TEXT for short strings (slower queries)FLOAT (precision errors)INT for large numbers like money (overflow)Indexes are like a book's index—they help MySQL find data faster. Without indexes, your server will lag.
WHERE clausesORDER BYJOIN conditions-- Add index to identifier (used in WHERE clauses constantly)
CREATE INDEX idx_identifier ON users(identifier);
-- Add composite index (multiple columns)
CREATE INDEX idx_user_char ON characters(user_id, character_id);
-- Add unique index (ensures no duplicates)
CREATE UNIQUE INDEX idx_unique_license ON users(license);
-- See which indexes exist on a table
SHOW INDEX FROM users;
-- Analyze query performance (see if indexes are used)
EXPLAIN SELECT * FROM users WHERE identifier = 'steam:1100001';
If "type" column shows "ALL", your query is scanning the entire table (BAD). Add an index.
FiveM has evolved. Here's the current standard:
resources/[standalone]/oxmysqlserver.cfg:ensure oxmysql
set mysql_connection_string "mysql://root:password@localhost/your_database?charset=utf8mb4"
mysql://username:password@host:port/database?charset=utf8mb4
Example:
mysql://fivem:MySecurePass123@localhost:3306/fivem_server?charset=utf8mb4
Never use root user for your FiveM server. Create a dedicated user with limited permissions:
-- Create new MySQL user for FiveM
CREATE USER 'fivem_user'@'localhost' IDENTIFIED BY 'SecurePassword123!';
-- Grant only necessary permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON fivem_database.* TO 'fivem_user'@'localhost';
-- Apply changes
FLUSH PRIVILEGES;
If your database is on a separate server:
Always use prepared statements with oxmysql:
-- ❌ VULNERABLE (Don't do this!)
local query = string.format("SELECT * FROM users WHERE identifier = '%s'", identifier)
MySQL.query(query)
-- ✅ SAFE (Use this!)
MySQL.query('SELECT * FROM users WHERE identifier = ?', {identifier})
The ? placeholder is automatically sanitized by oxmysql, preventing SQL injection attacks.
Create a batch file to automate backups:
@echo off
REM backup_database.bat
REM Set variables
set MYSQL_PATH="C:\xampp\mysql\bin"
set DB_NAME=fivem_server
set DB_USER=root
set DB_PASS=your_password
set BACKUP_PATH=D:\FiveM_Backups
set TIMESTAMP=%date:~-4,4%%date:~-10,2%%date:~-7,2%_%time:~0,2%%time:~3,2%%time:~6,2%
set TIMESTAMP=%TIMESTAMP: =0%
REM Create backup directory if it doesn't exist
if not exist "%BACKUP_PATH%" mkdir "%BACKUP_PATH%"
REM Perform backup
%MYSQL_PATH%\mysqldump -u %DB_USER% -p%DB_PASS% %DB_NAME% > "%BACKUP_PATH%\backup_%TIMESTAMP%.sql"
REM Delete backups older than 30 days
forfiles /P "%BACKUP_PATH%" /S /M *.sql /D -30 /C "cmd /c del @path"
echo Backup completed: backup_%TIMESTAMP%.sql
Schedule this with Windows Task Scheduler:
backup_database.bat#!/bin/bash
# /root/backup_mysql.sh
DB_NAME="fivem_server"
DB_USER="root"
DB_PASS="your_password"
BACKUP_DIR="/backups/mysql"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# Create backup directory
mkdir -p $BACKUP_DIR
# Perform backup
mysqldump -u$DB_USER -p$DB_PASS $DB_NAME | gzip > $BACKUP_DIR/backup_$TIMESTAMP.sql.gz
# Delete backups older than 30 days
find $BACKUP_DIR -name "*.sql.gz" -type f -mtime +30 -delete
echo "Backup completed: backup_$TIMESTAMP.sql.gz"
Add to crontab:
# Run daily at 4 AM
0 4 * * * /root/backup_mysql.sh >> /var/log/mysql_backup.log 2>&1
A backup you haven't tested is not a backup.
Once a month, restore from backup to a test database:
mysql -u root -p test_database < backup_20250101_040000.sql
If it works, your backups are good. If not, fix your backup process NOW.
Enable slow query logging to find performance bottlenecks:
# Add to my.ini / my.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-queries.log
long_query_time = 2   # Log queries taking longer than 2 seconds
Review the log weekly and optimize slow queries.
-- ❌ Returns 50,000 rows (slow)
SELECT * FROM transactions;
-- ✅ Returns only what you need
SELECT * FROM transactions ORDER BY created_at DESC LIMIT 100;
-- ❌ Returns 20 columns you don't need
SELECT * FROM users WHERE id = 1;
-- ✅ Returns only what you need
SELECT id, username, money FROM users WHERE id = 1;
-- ❌ Two separate queries (slow)
local user = MySQL.query('SELECT * FROM users WHERE id = ?', {userId})
local vehicles = MySQL.query('SELECT * FROM vehicles WHERE owner_id = ?', {userId})
-- ✅ One query with JOIN (fast)
local result = MySQL.query([[
    SELECT u.*, v.plate, v.model 
    FROM users u
    LEFT JOIN vehicles v ON u.id = v.owner_id
    WHERE u.id = ?
]], {userId})
-- ❌ Multiple INSERT queries
for i, item in pairs(items) do
    MySQL.insert('INSERT INTO inventory (item_name, quantity) VALUES (?, ?)', {item.name, item.qty})
end
-- ✅ Single batch INSERT
MySQL.insert([[
    INSERT INTO inventory (item_name, quantity) VALUES 
    ('bread', 5), ('water', 10), ('bandage', 3)
]])
Run these commands monthly:
-- Analyze tables (updates statistics)
ANALYZE TABLE users, vehicles, inventory;
-- Optimize tables (defragment)
OPTIMIZE TABLE users, vehicles, inventory;
-- Check for corruption
CHECK TABLE users, vehicles, inventory;
-- Check current connections
SHOW STATUS WHERE Variable_name = 'Threads_connected';
-- Check slow queries
SHOW STATUS WHERE Variable_name = 'Slow_queries';
-- Check database size
SELECT table_schema AS "Database", 
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS "Size (MB)"
FROM information_schema.TABLES 
GROUP BY table_schema;
Create a simple script to alert you when backups fail:
-- Add to your backup script
if [ $? -eq 0 ]; then
    curl -H "Content-Type: application/json" \
    -d '{"content":"✅ Database backup successful"}' \
    YOUR_DISCORD_WEBHOOK_URL
else
    curl -H "Content-Type: application/json" \
    -d '{"content":"❌ DATABASE BACKUP FAILED - CHECK IMMEDIATELY"}' \
    YOUR_DISCORD_WEBHOOK_URL
fi
Database corrupted? Follow this checklist:
# Create new database
mysql -u root -p -e "CREATE DATABASE fivem_server_restored;"
# Restore from backup
mysql -u root -p fivem_server_restored < backup_20250101_040000.sql
# If successful, swap databases
mysql -u root -p -e "DROP DATABASE fivem_server_old;"
mysql -u root -p -e "RENAME DATABASE fivem_server TO fivem_server_old;"
mysql -u root -p -e "RENAME DATABASE fivem_server_restored TO fivem_server;"
📝 Note
MySQL doesn't support RENAME DATABASE directly. Use the approach above to dump and restore, or rename tables individually.
If you need to recover to a specific time (not just the latest backup):
log-bin = mysql-bin
expire_logs_days = 7
mysqlbinlog mysql-bin.000001 mysql-bin.000002 | mysql -u root -p fivem_server
This is advanced, but can save you when you need to recover data from between backups.
Cause: Your server is hitting the max_connections limit.
Solution:
-- Temporarily increase limit
SET GLOBAL max_connections = 300;
-- Make permanent in my.ini
max_connections = 300
Also check for connection leaks in your scripts. Every query should properly close connections.
Cause: Fragmentation and missing table optimization.
Solution:
-- Run monthly maintenance
OPTIMIZE TABLE users, vehicles, inventory, transactions;
ANALYZE TABLE users, vehicles, inventory, transactions;
Cause: Long-running transactions blocking other queries.
Solution:
-- Find blocking queries
SHOW PROCESSLIST;
-- Kill problematic query
KILL [process_id];
-- Increase timeout temporarily
SET GLOBAL innodb_lock_wait_timeout = 120;
Cause: Binary logs or old backups filling disk.
Solution:
# Check MySQL data directory size
du -sh /var/lib/mysql/
# Purge old binary logs
mysql -u root -p -e "PURGE BINARY LOGS BEFORE NOW() - INTERVAL 3 DAY;"
# Delete old backups
find /backups -name "*.sql*" -mtime +30 -delete
For servers with 200+ concurrent players, consider MySQL replication:
Benefits:
Basic Setup:
Master Server (my.ini):
server-id = 1
log-bin = mysql-bin
binlog-do-db = fivem_server
Replica Server (my.ini):
server-id = 2
relay-log = mysql-relay-bin
This is a complex topic that deserves its own guide, but it's worth knowing about for large servers.
For tables with millions of rows (like transaction logs), partitioning can dramatically improve performance:
-- Partition transactions by month
ALTER TABLE transactions
PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
    PARTITION p202401 VALUES LESS THAN (202402),
    PARTITION p202402 VALUES LESS THAN (202403),
    PARTITION p202403 VALUES LESS THAN (202404),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);
This allows MySQL to scan only relevant partitions, making queries 10x faster on large datasets.
For maximum performance, use separate connections for reads and writes:
-- Write connection (master)
local writeDb = 'mysql://user:pass@master-server/db'
-- Read connection (replica or same server)
local readDb = 'mysql://user:pass@read-server/db'
-- Heavy read queries use read connection
MySQL.query('SELECT * FROM users WHERE identifier = ?', {identifier}, readDb)
-- Writes use master
MySQL.insert('INSERT INTO logs ...', params, writeDb)
Your database is the heart of your FiveM server. Treat it with respect:
Daily: Automated backups running without fail
Weekly: Review slow query logs and optimize
Monthly: Test backup restoration, run table maintenance
Quarterly: Review security, update passwords, audit permissions
If you take away just ONE thing from this guide, make it this:
SET UP AUTOMATED BACKUPS RIGHT NOW.
Not tomorrow. Not next week. Right now.
Your future self will thank you when—not if—something goes wrong.
Initial Setup:
Security:
Performance:
Backups:
Monitoring:
Need help? Join the FiveM development Discord communities. Database issues are common, and experienced developers are usually happy to help.
Found this guide useful? Share it with other server owners. The FiveM community thrives when we help each other avoid catastrophic mistakes.
Good luck, and may your backups always be recent! 🚀