Always use async operations in production! Synchronous operations block the server thread and cause lag spikes. Async operations with callbacks keep your server responsive even during heavy database loads.
When you perform a synchronous database operation, your entire Garry's Mod server freezes until the operation completes:
-- ❌ BAD: This freezes the server for 50-200ms!
local player = collection:FindOne({ steamid = ply:SteamID() })
-- Server is frozen here... players experience lag
ProcessPlayer(player)
With async operations, the database query runs in the background while your server continues running:
-- ✅ GOOD: Server stays responsive!
collection:FindOneAsync({ steamid = ply:SteamID() }, function(err, player)
-- This runs when the query completes
if not err then
ProcessPlayer(player)
end
end)
-- Server continues immediately - no lag!
All CRUD operations have async counterparts that accept callbacks:
| Sync Operation | Async Operation | Callback Signature |
|---|---|---|
InsertOne() | InsertOneAsync() | function(err, insertedId) |
InsertMany() | InsertManyAsync() | function(err, insertedIds) |
FindOne() | FindOneAsync() | function(err, document) |
Find() | FindAsync() | function(err, documents) |
UpdateOne() | UpdateOneAsync() | function(err, modifiedCount) |
UpdateMany() | UpdateManyAsync() | function(err, modifiedCount) |
DeleteOne() | DeleteOneAsync() | function(err, deletedCount) |
DeleteMany() | DeleteManyAsync() | function(err, deletedCount) |
Count() | CountAsync() | function(err, count) |
Aggregate() | AggregateAsync() | function(err, results) |
All async operations follow the Node.js-style error-first callback pattern:
collection:OperationAsync(args..., function(err, result)
if err then
-- Handle error
print("Database error:", err)
return
end
-- Handle success
print("Operation succeeded:", result)
end)
Callback Arguments:
err: nil on success, error message string on failureresult: The operation result on success, nil on failure-- Save player data asynchronously
hook.Add("PlayerInitialSpawn", "SavePlayer", function(ply)
DB.players:InsertOneAsync({
steamid = ply:SteamID(),
name = ply:Nick(),
first_join = os.time()
}, function(err, insertedId)
if err then
ErrorNoHalt("Failed to save player: " .. err .. "\n")
return
end
print("✓ Saved player " .. ply:Nick() .. " with ID: " .. insertedId)
ply.DatabaseId = insertedId
end)
end)
-- Load player data asynchronously
local function LoadPlayerData(ply)
DB.players:FindOneAsync({
steamid = ply:SteamID()
}, function(err, playerData)
if err then
ErrorNoHalt("Database error: " .. err .. "\n")
return
end
if playerData then
ply:SetNWInt("Money", playerData.money or 0)
ply:SetNWInt("Level", playerData.level or 1)
print("✓ Loaded data for " .. ply:Nick())
else
-- New player - create default data
CreateNewPlayer(ply)
end
end)
end
-- Update player stats asynchronously
local function AddMoney(ply, amount)
DB.players:UpdateOneAsync(
{ steamid = ply:SteamID() },
{ ["$inc"] = { money = amount } },
false, -- upsert
function(err, modifiedCount)
if err then
ErrorNoHalt("Failed to update money: " .. err .. "\n")
return
end
if modifiedCount > 0 then
ply:SetNWInt("Money", ply:GetNWInt("Money") + amount)
ply:ChatPrint("Received $" .. amount)
end
end
)
end
-- Clear old logs asynchronously
local function CleanupOldLogs()
local oneWeekAgo = os.time() - (7 * 24 * 60 * 60)
DB.logs:DeleteManyAsync(
{ timestamp = { ["$lt"] = oneWeekAgo } },
function(err, deletedCount)
if err then
ErrorNoHalt("Failed to cleanup logs: " .. err .. "\n")
return
end
print("✓ Cleaned up " .. deletedCount .. " old log entries")
end
)
end
-- Get leaderboard asynchronously
local function GetTopPlayers(callback)
DB.players:AggregateAsync({
{ ["$match"] = { banned = false } },
{ ["$sort"] = { score = -1 } },
{ ["$limit"] = 10 },
{ ["$project"] = {
name = 1,
score = 1,
level = 1
} }
}, function(err, results)
if err then
ErrorNoHalt("Failed to get leaderboard: " .. err .. "\n")
callback({})
return
end
callback(results)
end)
end
You can chain multiple async operations by nesting callbacks:
-- Create player, then add to a guild
local function CreatePlayerAndJoinGuild(ply, guildName)
-- Step 1: Create player
DB.players:InsertOneAsync({
steamid = ply:SteamID(),
name = ply:Nick()
}, function(err, playerId)
if err then
ErrorNoHalt("Failed to create player: " .. err .. "\n")
return
end
-- Step 2: Find the guild
DB.guilds:FindOneAsync({
name = guildName
}, function(err, guild)
if err then
ErrorNoHalt("Failed to find guild: " .. err .. "\n")
return
end
if not guild then
ply:ChatPrint("Guild not found!")
return
end
-- Step 3: Add player to guild
DB.players:UpdateOneAsync(
{ steamid = ply:SteamID() },
{ ["$set"] = { guild_id = guild._id } },
false,
function(err, count)
if err then
ErrorNoHalt("Failed to join guild: " .. err .. "\n")
return
end
ply:ChatPrint("Successfully joined " .. guildName .. "!")
end
)
end)
end)
end
Deep nesting can make code hard to read. Consider breaking complex chains into separate functions or using a promise-like pattern with your own helper functions.
Always handle errors in your callbacks:
-- ❌ BAD: No error handling
collection:FindOneAsync({ id = 123 }, function(err, doc)
ProcessDocument(doc) -- Might fail if err is set!
end)
-- ✅ GOOD: Proper error handling
collection:FindOneAsync({ id = 123 }, function(err, doc)
if err then
ErrorNoHalt("Database error: " .. err .. "\n")
-- Fallback behavior
UseDefaultData()
return
end
if doc then
ProcessDocument(doc)
else
-- Handle "not found" case
CreateNewDocument()
end
end)
Create a helper function for consistent error handling:
-- Helper function
local function DbCallback(operation, callback)
return function(err, result)
if err then
ErrorNoHalt("[DB Error] " .. operation .. ": " .. err .. "\n")
-- Log to file or external service
DB.errors:InsertOneAsync({
operation = operation,
error = err,
timestamp = os.time()
})
return
end
if callback then
callback(result)
end
end
end
-- Usage
DB.players:FindOneAsync({ steamid = steamid }, DbCallback("LoadPlayer", function(player)
-- Only runs on success
ProcessPlayer(player)
end))
-- ❌ BAD: Multiple separate async calls
for _, ply in ipairs(player.GetAll()) do
DB.players:UpdateOneAsync({ steamid = ply:SteamID() }, data)
end
-- ✅ GOOD: Single batch operation
local updates = {}
for _, ply in ipairs(player.GetAll()) do
table.insert(updates, {
steamid = ply:SteamID(),
data = data
})
end
-- Use UpdateMany or BulkWrite
Not everything needs to be async:
-- ✅ GOOD: One-time setup can be synchronous
hook.Add("Initialize", "SetupDB", function()
DB.players:CreateIndex({ steamid = 1 }, true) -- Sync is fine here
end)
-- ✅ GOOD: Player operations should be async
hook.Add("PlayerInitialSpawn", "LoadPlayer", function(ply)
DB.players:FindOneAsync({ steamid = ply:SteamID() }, LoadCallback)
end)
-- ❌ BAD: Creating callbacks in a fast loop
hook.Add("Think", "BadIdea", function()
DB.players:CountAsync({}, function(err, count)
-- This creates hundreds of callbacks per second!
end)
end)
-- ✅ GOOD: Debounce or use timers
timer.Create("CountPlayers", 60, 0, function()
DB.players:CountAsync({}, function(err, count)
-- Only runs once per minute
end)
end)
hook.Add("PlayerInitialSpawn", "LoadPlayerData", function(ply)
local steamid = ply:SteamID()
DB.players:FindOneAsync({ steamid = steamid }, function(err, data)
if err then
ply:Kick("Database error, please rejoin")
return
end
if data then
-- Existing player
ply:SetNWInt("PlayTime", data.playtime or 0)
ply:SetNWInt("Money", data.money or 0)
else
-- New player - create default data
DB.players:InsertOneAsync({
steamid = steamid,
name = ply:Nick(),
playtime = 0,
money = 1000,
joined = os.time()
}, function(err, id)
if not err then
ply:SetNWInt("Money", 1000)
end
end)
end
end)
end)
hook.Add("PlayerDisconnected", "SavePlayerData", function(ply)
local steamid = ply:SteamID()
DB.players:UpdateOneAsync(
{ steamid = steamid },
{
["$set"] = {
last_seen = os.time(),
last_name = ply:Nick()
},
["$inc"] = {
playtime = ply:GetNWInt("PlayTime", 0)
}
},
true, -- upsert
function(err, count)
if err then
ErrorNoHalt("Failed to save player on disconnect: " .. err .. "\n")
end
end
)
end)
-- Auto-save all player data every 5 minutes
timer.Create("AutoSave", 300, 0, function()
for _, ply in ipairs(player.GetAll()) do
DB.players:UpdateOneAsync(
{ steamid = ply:SteamID() },
{
["$set"] = {
money = ply:GetNWInt("Money"),
level = ply:GetNWInt("Level")
}
},
true,
function(err, count)
if err then
ErrorNoHalt("Auto-save failed for " .. ply:Nick() .. "\n")
end
end
)
end
end)
| Use Sync When... | Use Async When... |
|---|---|
| Server initialization | Player connects |
| One-time configuration | Player disconnects |
| Admin console commands | Gameplay events |
| Low-frequency operations | High-frequency operations |
| Acceptable to wait | Must stay responsive |
Now that you understand async operations: