Oxide Timer Guide: timer.Once, timer.Every, timer.Repeat and NextTick in Rust Plugins
In Oxide plugins you schedule delayed or repeating code with `timer.Once`, `timer.Every` and `timer.Repeat`, and defer work to the next frame with `NextTick`. Store the returned `Timer` if you need to stop it, and destroy every long-lived timer in `Unload`. An exception inside a timer callback destroys that timer and logs "Failed to run a <seconds> timer".
6 min readUpdated Oct 26, 2026
An Oxide timer runs a piece of plugin code later or repeatedly: timer.Once(seconds, action) runs it one time, timer.Every(seconds, action) runs it until stopped, timer.Repeat(seconds, times, action) runs it a fixed number of times, and NextTick(action) runs it on the next server frame. Carbon loads the same plugins and supports the same calls.
The timer API
| Call | Behavior | Returns |
|---|---|---|
timer.Once(5f, () => ...) | Runs once after 5 seconds | Timer |
timer.Every(60f, () => ...) | Runs every 60 seconds until destroyed | Timer |
timer.Repeat(10f, 3, () => ...) | Runs every 10 seconds, 3 times in total | Timer |
NextTick(() => ...) | Runs on the next frame | nothing |
Callbacks run on the main server thread, so they can use game objects directly. That also means a slow callback stalls the whole server, so keep them short (see rust plugin performance).
private void OnPlayerConnected(BasePlayer player)
{
if (player == null) return;
string id = player.UserIDString;
timer.Once(10f, () =>
{
var p = BasePlayer.FindByID(player.userID.Get());
if (p == null || !p.IsConnected) return;
p.ChatMessage(Msg("Welcome", id, p.displayName));
});
}Looking the player up again after the delay, or at least checking IsConnected, protects you from acting on someone who has already left. See OnPlayerConnected.
Choosing the right call
Pick the call that matches the lifetime of the work, because that decides how much cleanup you owe.
timer.Oncefits one delayed action: a welcome message, removing a temporary effect, re-enabling something after a cooldown. It finishes by itself, so a stored reference is only needed if you may cancel it early.timer.Everyfits background jobs that run for as long as the plugin is loaded: announcements, cleanup sweeps, periodic checks. You own this timer, so store it and destroy it inUnload.timer.Repeatfits a bounded sequence, such as a countdown that prints "3, 2, 1" or a message that repeats a few times. It stops on its own after the last run.NextTickfits "after the current hook is done". It has no delay parameter and returns nothing, so it cannot be cancelled.
A countdown is a good timer.Repeat example:
private void StartWipeCountdown(BasePlayer player)
{
if (player == null) return;
string id = player.UserIDString;
int remaining = 3;
timer.Repeat(1f, 3, () =>
{
var p = BasePlayer.FindByID(player.userID.Get());
if (p == null || !p.IsConnected) return;
p.ChatMessage(Msg("Countdown", id, remaining));
remaining--;
});
}Here the shared remaining variable is captured by the callback and changes between runs. That is useful for counters, but it is also the main source of surprises with lambdas, covered below.
Storing Timer references
Fire-and-forget timers are fine for short delays. Store the Timer whenever you need to cancel, restart or replace it.
private Timer announceTimer;
private void OnServerInitialized(bool initial)
{
announceTimer = timer.Every(300f, () =>
{
Server.Broadcast(Msg("Announcement"));
});
}
private void Unload()
{
announceTimer?.Destroy();
}Destroy() stops the timer and is safe to call on a timer that has already finished. Use ?. in case it was never assigned. Create repeating timers in OnServerInitialized rather than Init when they touch game state, because on a fresh server start the world may not be ready during Init. If you change a timer's interval from the config, destroy the old timer and create a new one instead of keeping both.
Capturing variables safely
A timer callback is a lambda, and it keeps whatever it captured alive until it runs. Three habits keep this predictable:
- Copy simple values such as
player.UserIDStringinto a local variable before creating the timer, and use the copy inside the callback. - Treat captured game objects (
BasePlayer, entities, items) as possibly null or destroyed when the callback finally runs. Re-check them, or look them up again by id. - Do not capture a collection you are enumerating elsewhere. If the callback changes a list that another loop is iterating, you get the exception described on collection was modified.
Long delays make all of this more important. A 10 second delay rarely sees a changed world; a 30 minute one almost always does.
NextTick
NextTick defers work until after the current call stack finishes. It is useful inside hooks where the game state is still changing.
private void OnEntitySpawned(BaseNetworkable entity)
{
var box = entity as StorageContainer;
if (box == null) return;
NextTick(() =>
{
if (box == null || box.IsDestroyed) return;
// inventory is fully set up by now
box.inventory?.MarkDirty();
});
}Always re-check that the entity still exists inside the callback: it can be destroyed in the meantime. See OnEntitySpawned. A short timer.Once with a small delay also works, but NextTick is cleaner when you only need to wait for the current hook to return.
Spreading heavy work over several frames
If you must process many entities or players, do not do it all in one callback. Handle a small batch, then schedule the next batch.
private void ProcessInBatches(List<BaseEntity> list, int index)
{
int end = Math.Min(index + 50, list.Count);
for (int i = index; i < end; i++)
{
var entity = list[i];
if (entity == null || entity.IsDestroyed) continue;
// work on entity
}
if (end < list.Count)
NextTick(() => ProcessInBatches(list, end));
}Each frame then does a bounded amount of work, which keeps you under the hook-time warning threshold. See took ms performance warning.
Cleanup in Unload
Oxide destroys a plugin's timers when it unloads, but do not rely on that for everything around them. A reload must leave nothing behind, so in Unload:
- Destroy stored timers, including those in dictionaries.
- Remove any UI you created for players in
BasePlayer.activePlayerList. - Save data files.
private void Unload()
{
announceTimer?.Destroy();
foreach (var t in playerTimers.Values) t?.Destroy();
playerTimers.Clear();
foreach (var player in BasePlayer.activePlayerList)
CuiHelper.DestroyUi(player, "MyPanel");
}Per-player timers
When each player has their own countdown, key a dictionary by UserIDString. Destroy the old timer before replacing it, otherwise two timers run for one player.
private readonly Dictionary<string, Timer> playerTimers = new Dictionary<string, Timer>();
private void StartCountdown(BasePlayer player, float seconds)
{
if (player == null) return;
string id = player.UserIDString;
StopCountdown(id);
playerTimers[id] = timer.Once(seconds, () =>
{
playerTimers.Remove(id);
var p = BasePlayer.FindByID(player.userID.Get());
if (p != null && p.IsConnected)
p.ChatMessage(Msg("CountdownDone", id));
});
}
private void StopCountdown(string id)
{
Timer existing;
if (playerTimers.TryGetValue(id, out existing))
{
existing?.Destroy();
playerTimers.Remove(id);
}
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
StopCountdown(player.UserIDString);
}Removing the entry from OnPlayerDisconnected keeps the dictionary from growing forever. The recipes for kits with cooldowns and combat block use this pattern.
Timers versus hooks for polling
Prefer a hook whenever the game already tells you when something happens. A timer that loops over all players every second to detect an event costs CPU even when nothing happens, and it can miss short events between ticks.
- Reacting to a death, a spawn or a damage event: use OnPlayerDeath, OnEntitySpawned or OnEntityTakeDamage.
- State that has no event, such as "has this player been idle for 5 minutes": a timer with a coarse interval is appropriate.
- Per-frame hooks like OnTick or OnPlayerInput run very often; avoid heavy work in them and do not log from them.
If you must poll, use the longest interval that still feels right (usually 5 to 60 seconds), and do not allocate new lists on every tick. See oxide hooks explained for how hooks are dispatched.
Exceptions inside timers
If a timer callback throws, Oxide logs a line of this form and destroys that timer:
Failed to run a 60 second timer in 'MyPlugin v1.0.0' (NullReferenceException: ...)
The important consequence is that a repeating timer silently stops after its first failure. The usual causes are a player or entity that became null or destroyed between scheduling and execution. Fix it by null-checking everything you captured, and consider wrapping risky parts of a long loop so one bad element does not end the schedule. The full explanation is on Failed to run timer. For general debugging steps see debugging rust plugins.
When a plugin's periodic task seems to have "just stopped", search the console and the log for this message first. Do not add Puts calls inside a timer that runs every second; one line per run floods the log. Log only on failure or when the state changes.
Real-time scheduling with DateTime
timer.Every counts seconds from when it was created, not wall-clock time. For "every day at 06:00 UTC" or "kit available again at this moment", store a timestamp and compare it with the current time.
private Timer clockTimer;
private DateTime nextRun;
private void OnServerInitialized(bool initial)
{
nextRun = NextOccurrence(6, 0);
clockTimer = timer.Every(30f, () =>
{
if (DateTime.UtcNow < nextRun) return;
nextRun = NextOccurrence(6, 0);
Server.Broadcast(Msg("DailyEvent"));
});
}
private DateTime NextOccurrence(int hour, int minute)
{
var now = DateTime.UtcNow;
var next = new DateTime(now.Year, now.Month, now.Day, hour, minute, 0, DateTimeKind.Utc);
if (next <= now) next = next.AddDays(1);
return next;
}A coarse check timer plus a stored target time has two advantages: restarts and reloads do not shift the schedule, and the precision loss is at most the check interval. Persist the target (or a last-claimed timestamp) with the data system described in rust plugin data files. Facepunch.Math.Epoch.Current gives Unix seconds if you prefer integers in a data file.
Use UTC for stored times. Local time shifts with daylight saving and with the host machine's settings, which makes saved timestamps ambiguous after a move or a clock change.
Ready-made examples: scheduled commands, scheduled restarts and auto broadcast.
Common mistakes
- Creating a
timer.Everyin a hook that fires repeatedly (such as a connect or spawn hook) without destroying the previous one, which stacks timers. - Capturing a
BasePlayerfor a long delay and using it without a null orIsConnectedcheck. - Using
UnityEngine.Time.realtimeSinceStartupfor schedules that must survive a restart; it resets on every boot. - Forgetting that callbacks run on the main thread, so a long loop inside one causes lag and a hook-time warning (took ms performance warning).
- Skipping cleanup in
Unload, leaving UI or dictionary entries behind after a reload. - Expecting a timer to fire at an exact moment. It fires on the first server update after the time has passed.
To have the structure above written for you, describe the behavior in the plugin generator.
FAQ
Do timers survive a plugin reload or server restart?
No. Timers live in memory and belong to the plugin instance. When the plugin is unloaded or reloaded, its timers are destroyed, and a server restart starts with none. If a schedule must survive, store the target time (for example a UTC timestamp) in a data file and recreate the timer in OnServerInitialized.
Can a timer callback run for a player who has disconnected?
Yes. The callback runs regardless of who it was created for. The BasePlayer you captured may be disconnected or destroyed by then. Check player != null && player.IsConnected inside the callback, or destroy the player's timer in OnPlayerDisconnected.
What is the difference between timer.Every and timer.Repeat?
timer.Every(seconds, callback) repeats until you destroy it or the plugin unloads. timer.Repeat(seconds, times, callback) runs the callback a fixed number of times and then stops by itself. Both return a Timer you can store and destroy early.
Is it safe to touch the game from NextTick?
Yes. NextTick runs your action on the main thread on the next server frame, so it can use the same game APIs as a hook. It is the usual way to run code after the current hook has finished, for example after an item has been moved or an entity has finished spawning.
How accurate are Oxide timers?
Timers are checked by the server's update loop, so they fire on or shortly after the requested time, never exactly on it, and lag on a struggling server will delay them. Do not use them for precise measurement; compare real timestamps when accuracy matters.
More guides
- CovalencePlugin vs RustPlugin: Differences and Which to Use4 min read
- How to Debug a Rust Oxide Plugin: Compile Errors, Exceptions and Slow Hooks10 min read
- How to Give Items to Players in a Rust Plugin (Oxide/Carbon)5 min read
- How to Install Oxide on a Rust Dedicated Server (Windows and Linux)7 min read
Skip the C#: describe your plugin
Open generator