Skip to content
RustPluginGenerator

How to make a Rust AFK kick plugin (idle timer with warning)

Tracks each connected player's movement and look direction with a repeating timer and kicks anyone who stays idle past a configured limit after a chat warning. Exempt permission and a minimum player-count gate are built in.

Idle players eat server slots, keep bases lit and parking spots occupied, and make the player list look busier than it is. Vanilla Rust has no AFK kick, so admins either tolerate it or run a moderation plugin. This one keeps a per-player snapshot of position and eye rotation, refreshed on a repeating timer instead of a per-tick hook, and compares it against the previous snapshot. If nothing changed for the configured idle time, the player gets a warning; one minute later they are kicked with a reason. Players with the exempt permission are skipped, and the whole check is disabled while the server is below the configured player-count threshold so small populations are not punished. The plugin stores its settings in oxide/config and registers one chat command that reports the caller's current idle time.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Afk Kick", "RustPluginGenerator", "1.0.0")]
    [Description("Kicks players who stay idle for too long, with a warning one minute before.")]
    public class AfkKick : RustPlugin
    {
        private const string PermUse = "afkkick.use";
        private const string PermExempt = "afkkick.exempt";

        private class PluginConfig
        {
            [JsonProperty("Idle Minutes")]
            public float IdleMinutes { get; set; } = 30f;

            [JsonProperty("Warning Grace Minutes")]
            public float WarningGraceMinutes { get; set; } = 1f;

            [JsonProperty("Minimum Players")]
            public int MinimumPlayers { get; set; } = 20;

            [JsonProperty("Check Interval Seconds")]
            public float CheckIntervalSeconds { get; set; } = 5f;

            [JsonProperty("Move Tolerance")]
            public float MoveTolerance { get; set; } = 0.5f;

            [JsonProperty("Angle Tolerance")]
            public float AngleTolerance { get; set; } = 2f;

            [JsonProperty("Kick Reason")]
            public string KickReason { get; set; } = "Kicked for being AFK";
        }

        private class PlayerState
        {
            public Vector3 Position;
            public Quaternion Rotation;
            public float LastActive;
            public bool Warned;
        }

        private PluginConfig config;
        private readonly Dictionary<ulong, PlayerState> states = new Dictionary<ulong, PlayerState>();
        private Timer checkTimer;

        private void Init()
        {
            permission.RegisterPermission(PermUse, this);
            permission.RegisterPermission(PermExempt, this);
        }

        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>
            {
                ["Warning"] = "You have been idle. Move within {0} minute(s) or you will be kicked.",
                ["Kicked"] = "Kicked for being AFK",
                ["Status"] = "You have been idle for {0} second(s). The kick threshold is {1} minute(s).",
                ["NoPermission"] = "You do not have permission to use that command."
            }, this);
        }

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

        private void OnServerInitialized()
        {
            checkTimer?.Destroy();
            checkTimer = timer.Every(Mathf.Max(1f, config.CheckIntervalSeconds), RunCheck);
        }

        private void Unload()
        {
            checkTimer?.Destroy();
            checkTimer = null;
            states.Clear();
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null) return;
            states[player.userID] = new PlayerState
            {
                Position = player.transform.position,
                Rotation = player.eyes != null ? player.eyes.rotation : Quaternion.identity,
                LastActive = Time.realtimeSinceStartup,
                Warned = false
            };
        }

        private bool CanKick(BasePlayer player)
        {
            if (player == null || !player.IsConnected || player.IsAdmin) return false;
            if (permission.UserHasPermission(player.UserIDString, PermExempt)) return false;
            return true;
        }

        private void RunCheck()
        {
            if (BasePlayer.activePlayerList.Count <= config.MinimumPlayers) return;

            float now = Time.realtimeSinceStartup;
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (!CanKick(player)) continue;
                if (player.eyes == null) continue;

                PlayerState state;
                if (!states.TryGetValue(player.userID, out state))
                {
                    states[player.userID] = new PlayerState
                    {
                        Position = player.transform.position,
                        Rotation = player.eyes.rotation,
                        LastActive = now,
                        Warned = false
                    };
                    continue;
                }

                Vector3 pos = player.transform.position;
                Quaternion rot = player.eyes.rotation;

                bool moved = Vector3.Distance(pos, state.Position) > config.MoveTolerance;
                bool turned = Quaternion.Angle(rot, state.Rotation) > config.AngleTolerance;

                if (moved || turned)
                {
                    state.Position = pos;
                    state.Rotation = rot;
                    state.LastActive = now;
                    state.Warned = false;
                    continue;
                }

                float idleSeconds = now - state.LastActive;
                float warnAt = config.IdleMinutes * 60f;
                float kickAt = (config.IdleMinutes + config.WarningGraceMinutes) * 60f;

                if (idleSeconds >= kickAt)
                {
                    states.Remove(player.userID);
                    player.Kick(Msg("Kicked", player.UserIDString));
                }
                else if (idleSeconds >= warnAt && !state.Warned)
                {
                    state.Warned = true;
                    player.ChatMessage(Msg("Warning", player.UserIDString, config.WarningGraceMinutes));
                }
            }
        }

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

            PlayerState state;
            float idle = 0f;
            if (states.TryGetValue(player.userID, out state))
            {
                idle = Time.realtimeSinceStartup - state.LastActive;
            }

            player.ChatMessage(Msg("Status", player.UserIDString, Mathf.FloorToInt(idle), config.IdleMinutes));
        }
    }
}

Hooks used: OnServerInitialized, OnPlayerConnected

Config file

Created automatically on first load at oxide/config/AfkKick.json.

AfkKick.json
{
  "Idle Minutes": 30,
  "Warning Grace Minutes": 1,
  "Minimum Players": 20,
  "Check Interval Seconds": 5,
  "Move Tolerance": 0.5,
  "Angle Tolerance": 2,
  "Kick Reason": "Kicked for being AFK"
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/afkReports your current idle time and the server's kick threshold.afkkick.use
Permissions
PermissionGrants
afkkick.useAllows the /afk chat command.
afkkick.exemptPlayers with this permission are never warned or kicked for being idle.

How the code works

  1. Track idle state per player

    A nested PlayerState class holds the last known Vector3 position, the last eye rotation, the Time.realtimeSinceStartup value when a change was last seen, and a flag for whether a warning was already sent. States live in a Dictionary<ulong, PlayerState> keyed by player.userID. OnPlayerConnected seeds the entry so a player who joins and never moves still accumulates idle time in the next check.

  2. Run the comparison on a timer

    The check runs from timer.Every(checkInterval, ...) created in OnServerInitialized and destroyed in Unload. Each pass skips work entirely while BasePlayer.activePlayerList.Count is at or below the configured threshold. For everyone else it compares player.transform.position and player.eyes.rotation against the stored snapshot using a small distance and angle tolerance, so minor physics jitter does not reset the timer.

  3. Warn, then kick

    When the elapsed idle time passes Idle Minutes, the plugin sends the warning message through the language file and sets warned = true. When it passes Idle Minutes + Warning Grace Minutes, the player is removed with player.Kick(reason) using the Kick Reason config value. The warning text is filled from lang with the grace period so players know exactly how long they have.

  4. Skip exempt players and staff

    CanKick(BasePlayer) returns false for player.IsAdmin, disconnected players, and anyone holding the afkkick.exempt permission via permission.UserHasPermission. The same guard runs inside the warning branch, so an admin who granted themselves the permission mid-session stops receiving warnings immediately. Because the check reads BasePlayer.activePlayerList, sleeping players are never touched.

  5. Tolerances come from config

    The PluginConfig class exposes Idle Minutes, Warning Grace Minutes, Minimum Players, Check Interval Seconds, Move Tolerance and Angle Tolerance. Both tolerances default to small values (0.5 units and 2 degrees) so a player shifting weight or turning slowly still counts as active. LoadConfig falls back to defaults and writes a warning if the file cannot be deserialized.

  6. Commands and cleanup

    /afk is a [ChatCommand("afk")] method that checks the afkkick.use permission and replies with the caller's idle seconds formatted through lang. Unload destroys the timer and clears the state dictionary. No data is written to disk, so a plugin reload simply starts fresh timers for everyone currently online.

Install it on your server

  1. Save AfkKick.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/AfkKick.json (Carbon: carbon/configs/AfkKick.json). After editing, reload it:oxide.reload AfkKick c.reload AfkKick # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default afkkick.use c.grant group default afkkick.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Kick only during high population

    Tightens the threshold logic so kicks trigger only above a configured peak-hour player count.

    Generate this version
  • Broadcast AFK status

    Announces idle players to the server so admins can see who is holding a slot.

    Generate this version
  • Console summary command

    Adds a console command that prints idle time for every connected player.

    Generate this version

FAQ

Does this work on Carbon?

Yes. Carbon loads standard Oxide C# plugins from the same plugin folder. Drop the compiled DLL into oxide/plugins (or the Carbon plugins directory) and it registers the same hooks, permission and config path.

Why compare against a snapshot instead of hooking input?

Input hooks fire constantly and add per-frame cost. A repeating timer at the configured interval reads position and eye rotation once every few seconds, which is cheap and still catches players who are genuinely idle. Fine-grained input tracking would also count a player holding a mouse button as active.

Will it kick someone who is building or crafting?

Yes, if their position and view direction do not change for the full idle period. Raising Move Tolerance does not help here because building still requires turning the camera. Increase Idle Minutes if your players tend to stand still while managing inventory.

Are sleeping or disconnected players affected?

No. The check iterates BasePlayer.activePlayerList, which only contains connected players, and also skips any entry where IsConnected is false. Players who log off or go to sleep simply drop out of the scan.

How do I exempt an admin group?

Grant afkkick.exempt to the group, for example oxide.grant group admin afkkick.exempt. Admins are also skipped by the hard-coded IsAdmin guard, so server owners never get kicked while working in the console.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Create an Oxide Rust plugin that kicks players idle for 30 minutes, warns them 1 minute before, skips the afkkick.exempt permission and only runs when 50 or more players are online.
Customize for my server

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

Last updated Oct 26, 2026.

Customize afk kick

Customize