Skip to content
RustPluginGenerator

OnTick

OnTick is called once per server frame from ServerMgr.DoTick, before RCON updates and network processing. It runs on the main thread every tick regardless of players being connected.

Signature
void OnTick()

Return value

The hook is patched into a void method and its return value is ignored. Returning null or any value has no effect on the server tick; the engine code after the hook runs either way.

How it works

Rust's server loop calls ServerMgr.DoTick on every frame. The hook fires at the very start of that method, before RCon.Update, the Nexus/Companion updates and the per-connection network work. That means it runs many times per second (typically hundreds of times on a healthy server) and it runs whether or not any player is connected.

Because there are no parameters, OnTick is a pure pulse: you cannot tell from the hook itself what changed, so it is only useful as a scheduler. Real work should be gated — count ticks or compare UnityEngine.Time.realtimeSinceStartup against a stored timestamp so your logic runs at a controlled interval instead of every frame. Even then, keep the body short: anything expensive here executes at frame rate and directly eats server FPS.

For most timed tasks timer.Every is the better tool. Reach for OnTick only when you need frame-level granularity, sub-timer precision, or logic that must run even while timers are being primed. Narrowing by parameter type is not possible; the hook has no arguments.

Example plugin: Broadcast a rotating server message on an interval

TickBroadcaster.cs
using UnityEngine;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;

namespace Oxide.Plugins
{
    [Info("Tick Broadcaster", "RustPluginGenerator", "1.0.0")]
    [Description("Broadcasts rotating server announcements on a configurable interval using the OnTick hook.")]
    public class TickBroadcaster : RustPlugin
    {
        private const string PermToggle = "tickbroadcaster.toggle";

        private PluginConfig config;
        private float lastBroadcast;
        private int messageIndex;

        private class PluginConfig
        {
            [JsonProperty("Enabled")]
            public bool Enabled = true;

            [JsonProperty("Interval seconds")]
            public float IntervalSeconds = 300f;

            [JsonProperty("Messages")]
            public List<string> Messages = new List<string>
            {
                "Welcome to the server! Type /help for commands.",
                "Vote for us to keep the server online.",
                "Report bugs and rule breakers to staff."
            };
        }

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["NoPermission"] = "You do not have permission to use this command.",
                ["Toggled"] = "Broadcaster is now {0}.",
                ["NoMessages"] = "No messages are configured.",
                ["Enabled"] = "enabled",
                ["Disabled"] = "disabled"
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args)
            => string.Format(lang.GetMessage(key, this, userId), args);

        private void Init()
        {
            permission.RegisterPermission(PermToggle, this);
            lastBroadcast = UnityEngine.Time.realtimeSinceStartup;
        }

        private void OnTick()
        {
            if (!config.Enabled || config.Messages == null || config.Messages.Count == 0)
                return;

            float now = UnityEngine.Time.realtimeSinceStartup;
            if (now - lastBroadcast < config.IntervalSeconds)
                return;

            lastBroadcast = now;
            string message = config.Messages[messageIndex % config.Messages.Count];
            messageIndex++;
            Server.Broadcast(message);
        }

        [ChatCommand("broadcast")]
        private void CmdBroadcast(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermToggle))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            if (config.Messages == null || config.Messages.Count == 0)
            {
                player.ChatMessage(Msg("NoMessages", player.UserIDString));
                return;
            }

            config.Enabled = !config.Enabled;
            lastBroadcast = UnityEngine.Time.realtimeSinceStartup;
            SaveConfig();
            player.ChatMessage(Msg("Toggled", player.UserIDString, config.Enabled ? Msg("Enabled", player.UserIDString) : Msg("Disabled", player.UserIDString)));
        }
    }
}

The plugin stores a list of announcement texts in its config and shows one every configured number of seconds. OnTick does almost nothing: it compares UnityEngine.Time.realtimeSinceStartup against the last time a message went out and returns immediately when the interval has not elapsed. When the interval passes, it picks the next line from the configured list (cycling with a modulo), calls Server.Broadcast, and rewrites the saved timestamp. All timing state lives in fields, and the interval guard keeps the per-frame cost to a single float comparison. Reload the plugin to pick up a new message list; no timer handle needs cleanup because nothing is scheduled.

Common use cases

  • Custom frame-gated scheduler

    Run logic on your own cadence without registering Oxide timers, for example a lightweight check every 0.5 seconds that compares Time.realtimeSinceStartup against a stored value.

  • Server performance sampling

    Accumulate frame deltas or count ticks over a short window and write the result to oxide/data for later analysis, keeping the collection cheap enough to run at frame rate.

  • Warmup and idle handling

    Detect when the server has had no connected players for a while and adjust entity or NPC spawn settings, again committing work only when the state actually changes.

Pitfalls

  • Runs hundreds of times per second

    The hook fires once per server frame, not once per second. Doing logging, lookups over BaseNetworkable.serverEntities or chat output inside the body will tank server performance almost immediately.

  • Use timer.Every when you just need an interval

    Oxide timers are the supported tool for periodic work and are easier to destroy on Unload. OnTick is only justified when you need frame precision or timing that must not depend on the timer system.

  • No context is provided

    There are no parameters, so the hook cannot tell you which entity, player or event caused anything. Any classification has to come from state you track yourself in other hooks.

Build a plugin with OnTick

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Custom frame-gated scheduler using the OnTick hook, with a config file and a permission
Generate this plugin

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.

Build a plugin with OnTick

Generate