Fix "Calling 'OnServerSave' took average ...ms [GARBAGE COLLECT]" in Oxide plugins
Oxide logged that OnServerSave on your plugin took over 100ms and a garbage collection ran during the call. The usual fix is to serialize much less data per save, move the work off OnServerSave, and stop building throwaway strings and lists on every save.
Calling 'OnServerSave' on 'StatsTracker v1.0.0' took average 1250ms [GARBAGE COLLECT]
Why it happens
Oxide times every hook call and prints this line when the call exceeds 100ms; the [GARBAGE COLLECT] suffix means a GC pause happened while your hook was running, so the number includes the garbage collector, not only your code. Server saves fire on a timer, so a hook that allocates heavily there regularly stalls the server. Common triggers in Oxide plugins:
- Writing a large data file on every save with
DataFileSystem.WriteObject, which converts the whole object graph to JSON and allocates strings for the full document. - Building a
DictionaryorListsnapshot for serialization inside the hook, then discarding it. - Concatenating log strings,
ToString()calls orstring.Formatfor every tracked entity on each save. - Keeping high-frequency counters in a collection that grows without bounds, so a save walks a huge structure.
The threshold (100ms) and the timing itself come from Oxide; the allocations are yours.
How to fix it
- Check whether the write is really needed on every save. Call
SaveData()from OnServerSave only if a dirty flag is set, and clear the flag afterwards. Most counters change once, not per player per second. - Shrink what you persist. Store only fields you need after a restart; drop derived values, timestamps you can recompute and per-frame statistics.
- OnServerSave is already the end of the save cycle: do not serialize inside it. Set a boolean, then write the file from a
timer.Onceor onOnServerShutdownif the data loss window is acceptable. - Avoid building intermediate lists or formatted strings in the save path. Serialize the dictionary you already hold instead of copying it.
- Reload with
oxide.reload StatsTracker. The warning disappears when the recorded time is under 100ms.
Confirm by watching the console after a few save cycles; a fixed plugin logs Loaded plugin ... on reload and no further "took average ...ms" lines.
Before and after
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
namespace Oxide.Plugins
{
[Info("StatsTracker", "RustPluginGenerator", "1.0.0")]
[Description("Counts chat messages per player and persists the totals on every server save")]
public class StatsTracker : RustPlugin
{
private Dictionary<ulong, int> counts = new Dictionary<ulong, int>();
private void OnPlayerChat(BasePlayer player, string message, Chat.ChatChannel channel)
{
if (player == null) return;
int current;
counts.TryGetValue(player.userID, out current);
counts[player.userID] = current + 1;
}
private void OnServerSave()
{
// Rebuild a fresh snapshot and string on every save cycle.
var snapshot = new Dictionary<string, object>();
foreach (var entry in counts)
{
var record = new Dictionary<string, object>
{
["id"] = entry.Key.ToString(),
["count"] = entry.Value,
["line"] = string.Format("player {0} sent {1} messages", entry.Key, entry.Value),
["saved"] = DateTime.UtcNow.ToString("o")
};
snapshot[entry.Key.ToString()] = record;
}
Interface.Oxide.DataFileSystem.WriteObject("StatsTracker", snapshot);
Puts("Saved " + snapshot.Count + " player records");
}
private void Unload()
{
OnServerSave();
}
}
}using System.Collections.Generic;
using Oxide.Core;
using Oxide.Core.Plugins;
namespace Oxide.Plugins
{
[Info("StatsTracker", "RustPluginGenerator", "1.0.0")]
[Description("Counts chat messages per player and saves totals when they change")]
public class StatsTracker : RustPlugin
{
private Dictionary<ulong, int> counts;
private bool dirty;
private void Init()
{
counts = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, int>>(Name)
?? new Dictionary<ulong, int>();
}
private void OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
{
if (player == null) return;
int current;
counts.TryGetValue(player.userID, out current);
counts[player.userID] = current + 1;
dirty = true;
}
private void OnServerSave()
{
// Persist only when something changed; the dictionary is written directly,
// no snapshot copy and no strings are built here.
if (dirty)
{
Interface.Oxide.DataFileSystem.WriteObject(Name, counts);
dirty = false;
}
}
private void Unload()
{
if (dirty)
{
Interface.Oxide.DataFileSystem.WriteObject(Name, counts);
dirty = false;
}
}
}
}FAQ
Is this an error or a warning?
It is a performance warning. Oxide prints it for any hook that runs longer than 100ms. The plugin stays loaded and the hook keeps being called; nothing throws because of this line.
Why does my hook time include the garbage collector?
Oxide measures wall time around the call. When a GC pause lands inside that window, the measured number includes it and the line gets the [GARBAGE COLLECT] suffix. Reducing allocations in the hook lowers both the allocation load and the chance of a pause.
Does OnServerSave fire once per autosave?
Yes, it runs during the server save cycle, roughly every few minutes depending on server settings. Doing file writes or large serialization there stalls the same cycle that saves the world, so keep the body short.
Can I just increase the threshold?
No. The 100ms limit is hardcoded in Oxide and is not a config option. The only way to remove the line is to make the hook finish faster.
Different error?
Paste the full console output and get the cause and a fix in plain English.
Open the error explainerSkip the debugging
Generated plugins are compiled before you get them, and compiler errors are fixed automatically.
Generate a plugin2 free plugins a month, no card.
Last updated Oct 26, 2026.
Get plugins that compile first time
Generate