Entity IDs, Pools, and netId
How the bridge assigns RAGE:MP-style entity IDs on top of FiveM's global netId, and the cross-side rules that come with it.
Entity IDs, Pools, and netId
The bridge exposes RAGE:MP-style entity IDs on top of FiveM's networking. Because the two engines identify entities very differently, every entity carries three distinct identifiers. Knowing which one to use prevents the most common cross-side bugs.
The three identifiers
| Identifier | Scope | Same on all sides? | Use it for |
|---|---|---|---|
entity.id | Local to the current side, per pool | No | Local lookups: mp.vehicles.at(id) |
entity.remoteId | Shared across server + every client | Yes | RAGE:MP cross-side references: mp.vehicles.atRemoteId(remoteId) |
entity.netId | FiveM network ID (raw) | Yes | FiveM interop and native calls |
Never send entity.id to the other side
id is a local pool index. The same car is almost always a different id on the server than on each client. To reference an entity across the network, send remoteId (RAGE:MP style) or netId (FiveM style) — never id.
How FiveM identifies entities
FiveM gives every networked entity a netId (network ID), returned by NetworkGetNetworkIdFromEntity. Two facts drive everything below:
- It is a 16-bit integer, so it ranges from
0to65535(2¹⁶ − 1). - It is one global pool shared by every networked entity: vehicles, peds, and objects are all assigned IDs from the same space, so no two networked entities ever hold the same
netIdat the same time. It is not a separate counter per entity type.
A netId stays fixed for the lifetime of its entity, but the moment that entity is destroyed the number is released back into that one global pool and reused by a future entity of any type. On a long-running server that spawns and removes thousands of entities, these values churn constantly and cycle around the 65535 ceiling.
A player's own server ID (its source) is handed out by a separate registry, but the ped that represents that player is an ordinary entity drawn from this same global pool.
That works for FiveM, but it is nothing like RAGE:MP, where each pool hands out small, ordered IDs starting at 0.
Straight from the FiveM source
On the server, FiveM keeps a single, global pool of entity IDs. The object-ID space is MaxObjectId = (1 << 16) - 1 (so 65535), tracked by one shared bitset, and GetFreeObjectIds hands out the lowest free ID to whichever entity asks — it never branches on the entity type, so vehicles, peds, and objects all compete for the same numbers:
// MaxObjectId = (1 << 16) - 1 → 65535, one shared space for every entity
uint16_t id = 1;
for (; id < static_cast<uint16_t>(MaxObjectId); id++)
{
// m_objectIdsSent / m_objectIdsUsed are single, global bitsets
if (!m_objectIdsSent.test(id) && !m_objectIdsUsed.test(id))
{
data->objectIds.insert(id);
freeIds.push_back(id);
m_objectIdsSent.set(id);
break; // first free ID in the global pool
}
}When an entity is destroyed, its ID is returned to that same pool — the source literally comments "we want to make this object ID return to the global pool" — so the numbers churn and get reused across all entity types.
Reference
Sources: ServerGameState::GetFreeObjectIds and MaxObjectId — citizenfx/fivem.
The same wrap-and-reuse pattern shows up in the player (client) registry, which is the clearest place to see the 0xFFFF sentinel and the scan for a free slot. When a client connects, FiveM walks a 16-bit counter, treats 0xFFFF (65535) as the "invalid" value, wraps back to 1 at the top, and scans forward until it finds a free ID:
auto incrementId = [this]()
{
m_curNetId++;
// 0xFFFF is a sentinel value for 'invalid' ID
// 0 is not a valid ID
if (m_curNetId == 0xFFFF)
{
m_curNetId = 1;
}
};
// in case of overflow, ensure no client is currently using said ID
while (m_clientsByNetId[m_curNetId].lock())
{
incrementId();
}
client->SetNetId(m_curNetId);This second snippet is the client (player) NetID path, separate from entity IDs, but it shows the same model: a 16-bit space, 0xFFFF reserved as invalid, and a linear search for the next free slot.
Reference
Source: ClientRegistry::HandleConnectingClient — citizenfx/fivem.
How the bridge re-thinks IDs
To stay faithful to RAGE:MP, the bridge decouples its own IDs from netId and keeps netId as an internal/interop detail only.
Per-pool, 0-based, recyclable
Every pool (mp.vehicles, mp.peds, mp.objects, mp.players, …) owns its own ID space, handed out by a shared lowest-free allocator:
- IDs start at
0and only count up when no freed ID is available. - When an entity is destroyed, its ID is freed and reused by the next entity that needs one — the lowest free value first.
- IDs stay small and dense instead of growing forever or wrapping at a hard ceiling.
// Each pool has its own allocator.
ids.allocate(); // 0
ids.allocate(); // 1
ids.allocate(); // 2
ids.free(1); // 1 is now reusable
ids.allocate(); // 1 ← lowest free value, reusedPools are independent, so a vehicle and a ped can both have id === 0 at the same time — exactly like RAGE:MP.
id vs remoteId
id— the local pool index above. On the server,id === remoteId. On a client,idis generated locally and will differ from the server's value.remoteId— the shared identity, identical on the server and on every client. Resolve it withatRemoteId()to land on the same entity on the other side.
mp.events.add('giveCar', (player) => {
const veh = mp.vehicles.new(mp.joaat('sultan'), player.position);
player.call('carReady', [veh.remoteId]); // send remoteId, not id
});mp.events.add('carReady', (remoteId) => {
const veh = mp.vehicles.atRemoteId(remoteId); // same car, resolved locally
if (veh) mp.gui.chat.push(`local id ${veh.id}, remoteId ${veh.remoteId}`);
});netId is still there for FiveM interop
When you call a raw FiveM native, or talk to a non-bridge resource, use entity.netId — the value FiveM understands. For RAGE:MP-style code inside the bridge, prefer remoteId + atRemoteId().
Players
Players follow the same model. On join, the server allocates a 0-based recyclable remoteId for the player and maps it to FiveM's volatile source. That mapping is synced to every client, so player.id and player.remoteId are clean, small, stable numbers rather than the raw source.
Recycling caveats
A freed ID can belong to a different entity later
Because IDs are reused, do not cache an id (or remoteId) and assume it still points to the same entity after that entity was destroyed — a freed value can be reassigned to a brand-new entity. Resolve entities fresh each time, or keep the entity reference itself and guard with mp.<pool>.exists(entity).
65535 is the 'no remote ID' sentinel
A purely local, client-only entity (for example a non-networked mp.objects.new) has no shared identity. Internally its remoteId is set to 65535 (INVALID_REMOTE_ID) to mark "no network identity", so atRemoteId() will not resolve it on another side. This deliberately mirrors FiveM, which reserves the same 0xFFFF (65535) value as its "invalid" sentinel in the server client registry.
Quick reference
- Local lookup →
mp.<pool>.at(entity.id) - Cross-side, RAGE:MP style → send
remoteId, resolve withmp.<pool>.atRemoteId(remoteId) - Cross-side, FiveM interop / natives → use
entity.netId - Never send
entity.idacross the network.