Introduction

Async Operations & Callbacks

Non-blocking database operations with callbacks to prevent server lag

Async Operations & Callbacks

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.

Why Async Operations Matter

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!
A single synchronous query might take 50-200ms. With 20 players joining simultaneously, that's 1-4 seconds of server freeze! Async operations eliminate this completely.

Every Operation Has an Async Version

All CRUD operations have async counterparts that accept callbacks:

Sync OperationAsync OperationCallback 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)

Callback Pattern

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 failure
  • result: The operation result on success, nil on failure

Basic Examples

Insert with Callback

-- 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)

Find with Callback

-- 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 with Callback

-- 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

Delete with Callback

-- 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

Aggregate with Callback

-- 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

Chaining Async Operations

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.

Error Handling Best Practices

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)

Centralized Error Logging

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))

Performance Tips

1. Batch Operations When Possible

-- ❌ 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

2. Don't Overuse Async

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)

3. Avoid Callback Spam

-- ❌ 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)

Common Use Cases

Player Join

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)

Player Disconnect

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)

Periodic Auto-Save

-- 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)

Sync vs Async Decision Guide

Use Sync When...Use Async When...
Server initializationPlayer connects
One-time configurationPlayer disconnects
Admin console commandsGameplay events
Low-frequency operationsHigh-frequency operations
Acceptable to waitMust stay responsive
If a player triggers it, make it async. If it's a one-time setup or admin action, sync is acceptable.

Next Steps

Now that you understand async operations: