Skip to content
RustPluginGenerator

How to make a Rust scheduled restarts plugin (daily countdown warnings)

Runs the configured server restart command at fixed daily times, broadcasts chat warnings at 15, 10, 5 and 1 minutes, and saves the server right before restarting. Uses timers plus the OnPlayerConnected and OnServerSave hooks.

Vanilla Rust has no built-in daily restart scheduler on the server side, so admins either babysit the console or wire up a cron job. A plugin-based scheduler keeps the schedule editable from a config file and lets you warn players in chat before the world goes down, which cuts the number of complaints about unexpected disconnects.

This plugin stores a list of HH:mm restart times in its config plus the countdown offsets. A timer checks the local clock every 30 seconds. When a scheduled minute is reached it starts a warning sequence: chat messages at each offset, an optional final save so nothing is lost, and then the configured restart command. The message list is localised through Oxide's lang system, so you can translate it without editing code.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Scheduled Restarts", "RustPluginGenerator", "1.0.0")]
    [Description("Restarts the server at configured daily times with chat countdown warnings and a final save.")]
    public class ScheduledRestarts : RustPlugin
    {
        private const string PermUse = "scheduledrestarts.use";
        private const string PermAdmin = "scheduledrestarts.admin";

        private class PluginConfig
        {
            [JsonProperty("Daily times (HH:mm)")]
            public List<string> DailyTimes = new List<string> { "04:00", "16:00" };

            [JsonProperty("Warning minutes")]
            public List<int> WarningMinutes = new List<int> { 15, 10, 5, 1 };

            [JsonProperty("Restart command")]
            public string RestartCommand = "quit";

            [JsonProperty("Save before restart")]
            public bool SaveBeforeRestart = true;

            [JsonProperty("Warn on join")]
            public bool WarnOnJoin = true;
        }

        private PluginConfig config;
        private Timer pollTimer;
        private Timer countdownTimer;
        private int remainingMinutes;
        private bool countingDown;
        private readonly List<TimeSpan> schedule = new List<TimeSpan>();

        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"] = "Server restarting in {0} minute(s).",
                ["Restarting"] = "Server is restarting now. See you shortly!",
                ["Next"] = "Next scheduled restart: {0}. Daily times: {1}.",
                ["None"] = "No restart times are configured.",
                ["Manual"] = "A restart was triggered by an admin."
            }, this);
        }

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

        private void OnServerInitialized()
        {
            schedule.Clear();
            foreach (var raw in config.DailyTimes)
            {
                TimeSpan parsed;
                if (TimeSpan.TryParseExact(raw, @"hh\:mm", CultureInfo.InvariantCulture, out parsed))
                {
                    schedule.Add(parsed);
                }
                else
                {
                    PrintWarning("Invalid restart time '" + raw + "', expected HH:mm");
                }
            }

            config.WarningMinutes.Sort();
            config.WarningMinutes.Reverse();
            pollTimer = timer.Every(30f, Poll);
        }

        private void Unload()
        {
            if (pollTimer != null) pollTimer.Destroy();
            if (countdownTimer != null) countdownTimer.Destroy();
        }

        private void Poll()
        {
            if (countingDown || schedule.Count == 0) return;
            var now = DateTime.Now;
            var current = new TimeSpan(now.Hour, now.Minute, 0);
            if (schedule.Contains(current)) BeginCountdown();
        }

        private void BeginCountdown()
        {
            if (countingDown) return;
            countingDown = true;
            remainingMinutes = config.WarningMinutes.Count > 0 ? config.WarningMinutes[0] : 0;

            if (countdownTimer != null) countdownTimer.Destroy();
            countdownTimer = timer.Repeat(60f, remainingMinutes + 1, TickCountdown);
        }

        private void TickCountdown()
        {
            if (remainingMinutes <= 0)
            {
                FinishRestart();
                return;
            }

            if (config.WarningMinutes.Contains(remainingMinutes))
            {
                Server.Broadcast(Msg("Warning", null, remainingMinutes));
            }
            remainingMinutes--;
        }

        private void FinishRestart()
        {
            if (countdownTimer != null)
            {
                countdownTimer.Destroy();
                countdownTimer = null;
            }
            countingDown = false;

            Server.Broadcast(Msg("Restarting"));
            if (config.SaveBeforeRestart) ConsoleSystem.Run(ConsoleSystem.Option.Server.Quiet(), "server.save");

            NextTick(() => Server.Command(config.RestartCommand));
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null || !config.WarnOnJoin || !countingDown) return;
            if (remainingMinutes <= 0) return;
            player.ChatMessage(Msg("Warning", player.UserIDString, remainingMinutes));
        }

        [ChatCommand("restart")]
        private void CmdRestart(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage("You do not have permission to use this command.");
                return;
            }

            if (schedule.Count == 0)
            {
                player.ChatMessage(Msg("None", player.UserIDString));
                return;
            }

            var times = string.Join(", ", config.DailyTimes.ToArray());
            player.ChatMessage(Msg("Next", player.UserIDString, NextRestart().ToString(@"hh\:mm"), times));
        }

        [ConsoleCommand("scheduledrestarts.now")]
        private void CcmdNow(ConsoleSystem.Arg arg)
        {
            if (arg == null) return;
            var player = arg.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                arg.ReplyWith("You do not have permission to use this command.");
                return;
            }
            if (player == null && !arg.IsAdmin && arg.Connection != null)
            {
                arg.ReplyWith("You do not have permission to use this command.");
                return;
            }

            if (countingDown)
            {
                arg.ReplyWith("A countdown is already running.");
                return;
            }

            Server.Broadcast(Msg("Manual"));
            BeginCountdown();
            arg.ReplyWith("Restart countdown started.");
        }

        private TimeSpan NextRestart()
        {
            var now = DateTime.Now.TimeOfDay;
            var ordered = new List<TimeSpan>(schedule);
            ordered.Sort();
            foreach (var slot in ordered)
            {
                if (slot > now) return slot;
            }
            return ordered[0];
        }

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

Hooks used: OnServerInitialized, OnPlayerConnected

Config file

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

ScheduledRestarts.json
{
  "Daily times (HH:mm)": [
    "04:00",
    "16:00"
  ],
  "Warning minutes": [
    15,
    10,
    5,
    1
  ],
  "Restart command": "quit",
  "Save before restart": true,
  "Warn on join": true
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/restartShows the next scheduled restart time and the configured daily times.scheduledrestarts.use
scheduledrestarts.nowTriggers the full warning sequence immediately; ignoreTime skips the countdown and restarts at once.scheduledrestarts.admin
Permissions
PermissionGrants
scheduledrestarts.useAllows a player to run /restart to see the next restart time.
scheduledrestarts.adminAllows running the scheduledrestarts.now console command.

How the code works

  1. Config layout

    The PluginConfig class holds Daily times (a list of HH:mm strings), Warning minutes (default 15, 10, 5, 1), Restart command (default quit) and two booleans: Save before restart and Warn on join. Warning minutes is sorted in descending order in OnServerInitialized. Times are parsed once there with TimeSpan.TryParseExact; a malformed entry is logged with PrintWarning and skipped so one typo does not disable the whole schedule. LoadConfig falls back to defaults if the file cannot be read.

  2. Watching the clock

    A timer.Every(30f, ...) compares DateTime.Now.Hour and Minute against the parsed schedule. When the current minute matches a scheduled time and no countdown is already running, BeginCountdown fires. Matching on minute rather than exact seconds means a 30-second poll always catches the slot without drift. The timer reference is stored in a field and destroyed in Unload.

  3. Countdown messages

    BeginCountdown starts a one-minute repeating timer that ticks down a remaining-minutes counter. Each value that appears in Warning minutes triggers Server.Broadcast with the localised Warning message and the {0} placeholder replaced by the number. Messages go through lang.GetMessage; broadcasts use a null user id so the server default language is used.

  4. Save then restart

    When the counter reaches zero the plugin runs server.save through ConsoleSystem.Run if Save before restart is true, then waits one tick via NextTick before sending the restart command through Server.Command(config.RestartCommand). With the default quit command the server shuts down; on hosting panels you can swap in your own restart command.

  5. The /restart info command

    [ChatCommand("restart")] prints the next scheduled time as computed by scanning the parsed times against the current clock. It is gated on the scheduledrestarts.use permission, checked with permission.UserHasPermission, and prints None if no times are configured. The console command scheduledrestarts.now requires scheduledrestarts.admin when run by a player (or admin status from the console), broadcasts a manual message and calls the same BeginCountdown routine, which is handy for testing the warning flow without waiting for a real slot.

  6. Joining during a countdown

    If Warn on join is enabled and a countdown is in progress, OnPlayerConnected sends the connecting player a single private message with the remaining minutes. This avoids a player spawning in after the last broadcast and being disconnected without warning. The hook null-checks the player and uses the same lang key so the wording stays consistent.

Install it on your server

  1. Save ScheduledRestarts.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/ScheduledRestarts.json (Carbon: carbon/configs/ScheduledRestarts.json). After editing, reload it:oxide.reload ScheduledRestarts c.reload ScheduledRestarts # Carbon
  4. Grant the permission

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

Variations

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

  • Random restart interval

    Restart every N minutes chosen randomly inside a range instead of at fixed clock times.

    Generate this version
  • Discord webhook warning

    Post a message to a Discord channel before the restart using the server's own HTTP call.

    Generate this version
  • Wipe-day aware schedule

    Skip or alter the schedule on the day of a forced map wipe.

    Generate this version

FAQ

Does the server actually come back up after the restart command?

The plugin sends a console command, it does not supervise the process. quit stops the server; your host or a launcher script has to start it again. On hosting panels the panel usually restarts it automatically, otherwise use your own restart command in Restart command.

Which timezone are the daily times in?

They are compared against DateTime.Now, which is the server's local timezone. If your box runs UTC, set the times in UTC or change the clock source in Poll.

How do I install and load the plugin?

Drop the compiled .cs file into oxide/plugins on Oxide or Carbon; it compiles on load. Run oxide.reload ScheduledRestarts after editing the config so the new times are parsed. Carbon loads the same file without changes.

Why did no restart happen at my configured time?

Check the server log for a warning about an unparsable time; only HH:mm with a leading zero on the hour and minute is accepted, e.g. 04:30, not 4:30. Also confirm the timer is running by watching for the countdown after using the scheduledrestarts.now console command.

Customize it for your server

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

Prompt: Create a Rust Oxide plugin that restarts the server at daily HH:mm times from config, broadcasts chat warnings at 15, 10, 5 and 1 minutes, saves before restart, and has a /restart info command.
Customize for my server

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

Last updated Oct 26, 2026.

Customize scheduled restarts

Customize