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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/restart | Shows the next scheduled restart time and the configured daily times. | scheduledrestarts.use |
scheduledrestarts.now | Triggers the full warning sequence immediately; ignoreTime skips the countdown and restarts at once. | scheduledrestarts.admin |
| Permission | Grants |
|---|---|
scheduledrestarts.use | Allows a player to run /restart to see the next restart time. |
scheduledrestarts.admin | Allows running the scheduledrestarts.now console command. |
How the code works
Config layout
The
PluginConfigclass holdsDaily times(a list ofHH:mmstrings),Warning minutes(default 15, 10, 5, 1),Restart command(defaultquit) and two booleans:Save before restartandWarn on join.Warning minutesis sorted in descending order inOnServerInitialized. Times are parsed once there withTimeSpan.TryParseExact; a malformed entry is logged withPrintWarningand skipped so one typo does not disable the whole schedule.LoadConfigfalls back to defaults if the file cannot be read.Watching the clock
A
timer.Every(30f, ...)comparesDateTime.Now.HourandMinuteagainst the parsed schedule. When the current minute matches a scheduled time and no countdown is already running,BeginCountdownfires. 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 inUnload.Countdown messages
BeginCountdownstarts a one-minute repeating timer that ticks down a remaining-minutes counter. Each value that appears inWarning minutestriggersServer.Broadcastwith the localisedWarningmessage and the{0}placeholder replaced by the number. Messages go throughlang.GetMessage; broadcasts use a null user id so the server default language is used.Save then restart
When the counter reaches zero the plugin runs
server.savethroughConsoleSystem.RunifSave before restartis true, then waits one tick viaNextTickbefore sending the restart command throughServer.Command(config.RestartCommand). With the defaultquitcommand the server shuts down; on hosting panels you can swap in your own restart command.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 thescheduledrestarts.usepermission, checked withpermission.UserHasPermission, and printsNoneif no times are configured. The console commandscheduledrestarts.nowrequiresscheduledrestarts.adminwhen run by a player (or admin status from the console), broadcasts a manual message and calls the sameBeginCountdownroutine, which is handy for testing the warning flow without waiting for a real slot.Joining during a countdown
If
Warn on joinis enabled and a countdown is in progress,OnPlayerConnectedsends 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
Save ScheduledRestarts.cs
Keep the file name exactly as the class name. Oxide matches them.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.Edit the config if you want
On first load the default config is written tooxide/config/ScheduledRestarts.json(Carbon:carbon/configs/ScheduledRestarts.json). After editing, reload it:oxide.reload ScheduledRestarts c.reload ScheduledRestarts # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionDiscord webhook warning
Post a message to a Discord channel before the restart using the server's own HTTP call.
Generate this versionWipe-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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More utility plugins
- Command aliasesThis plugin maps short chat commands like /h and /k to longer ones such as /home and /kit, defined entirely in its config file, using theโฆOnServerInitializedOnPlayerCommandOnPlayerDisconnected
- Info commandsA config-driven text command plugin: every entry in the config defines a command name and a list of chat lines, and the plugin registers them allโฆ
- Map grid locationA /grid chat command that converts a player's world position into a Rust map grid cell (like G12) using the server's world size, with exactโฆOnServerInitializedOnPlayerConnected
Last updated Oct 26, 2026.
Customize scheduled restarts
Customize