Why teams move from ESX to QBCore
ESX and QBCore both give you the same core: a player loads in, the server knows who they are, they have money, a job, and an inventory. The difference is how each framework stores and exposes that data, and that difference is the entire migration. ESX grew up as the older, flatter system. QBCore is newer, leans on a structured charinfo and metadata model, and ships a more consistent export pattern.
Most teams switch for one of three reasons: the scripts they want are QBCore-only (a large share of newer FiveM releases ship QBCore-first), they want multicharacter support that feels native instead of bolted on, or they are tired of fighting ESX legacy quirks across the esx_* resource family. None of those reasons make the migration trivial. Treat it as a rebuild of your data layer, not a find-and-replace.
The honest scope
A small roleplay server with 15 to 25 resources and one or two custom scripts is realistically a 20 to 40 hour migration for one experienced developer. A large server with 150-plus resources, custom jobs, and a heavily modified economy is a multi-week project. The work is not hard line by line. It is the volume of small, correct changes that adds up, plus the testing.
Step 1: Map the concepts before you touch code
The single most useful thing you can do first is build a translation table in your head and on paper. Almost every ESX call has a QBCore equivalent, but the data underneath is shaped differently.
Mechanically, ESX exposes a global ESX object you grab with exports['es_extended']:getSharedObject(). QBCore exposes QBCore via exports['qb-core']:GetCoreObject(). From there the method names diverge. In ESX you write local xPlayer = ESX.GetPlayerFromId(source) and read xPlayer.getMoney(). In QBCore you write local Player = QBCore.Functions.GetPlayer(source) and read Player.PlayerData.money.cash.
Concrete example: paying a player 500 dollars cash. ESX is xPlayer.addMoney(500). QBCore is Player.Functions.AddMoney('cash', 500). Note QBCore forces you to name the money account (cash, bank, crypto), while ESX defaults to the main account. That naming requirement shows up everywhere and is the number one source of silent bugs.
Step 2: Understand the database differences
This is where data gets lost if you rush. Both frameworks use MySQL through oxmysql, but the table layout is not the same.
In ESX, the canonical player table is users, keyed by identifier (the license or steam identifier). Money lives in columns like accounts (a JSON blob) or sometimes flat columns. Job is stored as job and job_grade. Inventory, in modern ESX with ox_inventory, lives in a separate ox_inventory table or in the inventory column depending on your setup.
In QBCore, the canonical table is players, keyed by citizenid (a generated short ID like ABC12345) plus license. Identity lives in a JSON charinfo column (firstname, lastname, birthdate, gender, phone). Money lives in a JSON money column like {"cash":500,"bank":5000,"crypto":0}. Job lives in a JSON job column. Arbitrary extra state lives in metadata.
The practical consequence: one ESX users row maps to one or more QBCore players rows, and you must invent a citizenid and a charinfo for each, because ESX often has no first/last name split. Write a SQL or Lua migration script that reads each users row, parses the ESX accounts JSON, and writes a players row with a freshly generated citizenid, a charinfo built from whatever name data you have, and a money JSON assembled from the old accounts.
A realistic migration query shape
You will not get this in one clean INSERT ... SELECT because of the JSON reshaping. The reliable pattern is a one-time Lua script run inside a temporary resource that uses oxmysql to MySQL.query every users row, transforms each in Lua (where JSON is easy), and MySQL.inserts into players. Keep the original users table untouched until you have verified the new data. Never drop it on day one.
Step 3: Convert resources one family at a time
Do not convert everything at once. Group your resources and convert by family, testing each group before moving on.
Start with qb-core itself installed and booting clean. Then bring over the spine: spawn, multicharacter, and the HUD. QBCore ships qb-spawn, qb-multicharacter, and qb-hud, which replace whatever your ESX setup used for spawning and multicharacter (commonly esx_multicharacter plus the base spawnmanager) and your ESX HUD resource. These must work before anything else, because every other resource assumes a loaded PlayerData.
Next convert jobs. An ESX job script listens for esx:setJob and reads xPlayer.job.name. The QBCore equivalent uses QBCore.Functions.GetPlayer, reads Player.PlayerData.job.name, and fires QBCore:Client:OnJobUpdate. A police script like esx_policejob has a direct counterpart in qb-policejob; in most cases you replace the resource entirely rather than convert it line by line, because the community QBCore version is already built and maintained.
When to replace versus convert
For any common resource that has a maintained QBCore version (police, ambulance, banking, shops, garages), replace it. Converting esx_policejob by hand when qb-policejob exists is wasted effort. Reserve hand-conversion for your genuinely custom scripts, the ones that make your server yours. For those, the work is: swap the core object getter, rename every money and job call, and re-point every database query at the new tables.
Step 4: Handle the inventory carefully
Inventory is the highest-risk piece because it is where players keep value. The good news: many ESX and QBCore servers both run ox_inventory, which is framework-agnostic. If you are already on ox_inventory under ESX, you can keep it under QBCore because ox_inventory detects the active framework (esx, qb, or qbox) at startup based on which core resource is running. Once es_extended is gone and qb-core is started ahead of it, ox_inventory binds to the QBCore bridge on its own. That alone removes a huge chunk of risk.
If you are on the older qb-inventory or a stock ESX inventory, expect more work. Item definitions move from the ESX items database table to the QBCore qb-core/shared/items.lua file (or the ox_inventory data/items.lua). Usable items registered with ESX.RegisterUsableItem('bread', ...) become QBCore.Functions.CreateUseableItem('bread', ...). The item names should be kept identical across the move so player inventories survive.
Step 5: Update server.cfg and start order
The server.cfg start order matters more than people expect. QBCore must start before any resource that calls GetCoreObject. A correct block ensures oxmysql starts first, then qb-core, then shared dependencies like ox_lib and ox_inventory, then everything else.
A minimal correct ordering looks like: ensure oxmysql, ensure ox_lib, ensure qb-core, ensure ox_inventory, then your resource folders. Remove every ensure es_extended and ensure esx_* line as you retire those resources. Leaving a stray esx_* resource running will throw errors on boot and can crash dependent scripts. Search your server.cfg for esx and es_extended and confirm each removed line has a QBCore replacement actually started.
Step 6: Test against real player data
Do your final verification on a copy of production data, not an empty database. Boot the QBCore server pointed at a cloned database, then log in as several migrated players and check: correct cash and bank balances, correct job and grade, intact inventory, and a working character that can spawn. A migration that works on a fresh test character but corrupts a real one is the failure mode that actually hurts.
Keep the old ESX server bootable and the original users table intact for at least a week after you go live. Rollback capability is the cheapest insurance you can buy.
Performance and the long view
Neither framework is meaningfully faster than the other at the core level; both are thin Lua layers over the same FiveM server. Performance problems almost always live in the resources, not the framework. The migration is a good moment to drop resources you no longer use and to standardize on ox_lib, ox_inventory, and oxmysql, which are the modern shared foundation both ecosystems are converging on. A clean QBCore install running ten well-written resources will idle around 0.01 to 0.05 ms per script in the txAdmin resource monitor, the same as a clean ESX install.