Unload
Unload is called by Oxide when a plugin is being removed from the plugin manager, so it runs on every reload, unload and server shutdown. Use it to tear down everything the plugin created outside its own managed state.
void Unload()Return value
The hook returns void and the return value is ignored; nothing you return changes how Oxide removes the plugin. Work is done purely through the side effects of the method body.
How it works
Oxide calls Unload from CSharpPlugin.HandleRemovedFromManager right before the plugin object is detached from the manager, only while the plugin still reports itself as loaded. That covers oxide.reload <name>, oxide.unload <name>, plugins that fail during compilation after being live, and the plugin teardown that happens on server shutdown. The hook takes no arguments and gives you no indication of why the plugin is going away, so cleanup logic must be safe to run in all of those cases.
This is the counterpart of Init and Loaded. Anything that lives outside your plugin instance and would survive a reload must be released here: CUI elements added with CuiHelper.AddUi, timers created with timer.Once / timer.Every / timer.Repeat, and data you keep in memory that is not written to oxide/data after every change. Permanent side effects - permission registrations, lang entries, config files, data files - do not need to be undone and should not be, since they are meant to survive restarts.
There is nothing to narrow here: the signature has no parameters, so Unload always fires with no arguments. Keep the body short and defensive; a throw inside Unload leaves the cleanup half-finished.
Example plugin: Clean shutdown for a timed supply-drop announcer
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Supply Drop Announcer", "RustPluginGenerator", "1.0.0")]
[Description("Announces the next timed supply drop and shows a countdown panel to all players")]
public class SupplyDropAnnouncer : RustPlugin
{
private const string PanelName = "SupplyDropAnnouncer.Panel";
private const string PermAdmin = "supplydropannouncer.admin";
private PluginConfig config;
private Timer dropTimer;
private Timer panelTimer;
private float nextDropAt;
private class PluginConfig
{
[JsonProperty("Interval between drops (seconds)")]
public float IntervalSeconds = 900f;
[JsonProperty("Required online players")]
public int RequiredPlayers = 3;
[JsonProperty("Spawn a real airdrop when the timer ends")]
public bool SpawnAirdrop = true;
}
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>
{
["NextDrop"] = "Next supply drop in {0}.",
["DropNow"] = "A supply drop is on its way.",
["NotEnough"] = "Not enough players online for a scheduled drop.",
["Panel"] = "Supply drop in {0}",
["Reloaded"] = "Supply Drop Announcer reset its timers."
}, 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(PermAdmin, this);
}
private void OnServerInitialized()
{
ScheduleDrop();
panelTimer = timer.Every(1f, UpdatePanels);
}
private void ScheduleDrop()
{
nextDropAt = UnityEngine.Time.realtimeSinceStartup + config.IntervalSeconds;
if (dropTimer != null) dropTimer.Destroy();
dropTimer = timer.Once(config.IntervalSeconds, OnDropTimerElapsed);
}
private void OnDropTimerElapsed()
{
if (BasePlayer.activePlayerList.Count >= config.RequiredPlayers)
{
if (config.SpawnAirdrop) SpawnFallingAirdrop();
Server.Broadcast(Msg("DropNow"));
}
ScheduleDrop();
}
private void SpawnFallingAirdrop()
{
var prefab = "assets/prefabs/npc/cargo plane/cargo_plane.prefab";
var entity = GameManager.server.CreateEntity(prefab, new Vector3(0f, 200f, 0f), Quaternion.identity);
entity?.Spawn();
}
private void UpdatePanels()
{
int remaining = Mathf.Max(0, Mathf.CeilToInt(nextDropAt - UnityEngine.Time.realtimeSinceStartup));
string text = Msg("Panel", null, FormatTime(remaining));
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || !player.IsConnected) continue;
CuiHelper.DestroyUi(player, PanelName);
var container = new CuiElementContainer();
container.Add(new CuiPanel
{
Image = { Color = "0.05 0.05 0.05 0.6" },
RectTransform = { AnchorMin = "0.42 0.92", AnchorMax = "0.58 0.955" }
}, "Hud", PanelName);
container.Add(new CuiLabel
{
Text = { Text = text, FontSize = 13, Align = TextAnchor.MiddleCenter, Color = "0.9 0.9 0.9 1" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
}, PanelName);
CuiHelper.AddUi(player, container);
}
}
private static string FormatTime(int seconds)
{
return string.Format("{0:00}:{1:00}", seconds / 60, seconds % 60);
}
private void Unload()
{
if (dropTimer != null) { dropTimer.Destroy(); dropTimer = null; }
if (panelTimer != null) { panelTimer.Destroy(); panelTimer = null; }
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null) continue;
CuiHelper.DestroyUi(player, PanelName);
}
}
}
}The plugin announces upcoming airdrops in chat and shows a countdown panel to every connected player. OnServerInitialized calls ScheduleDrop to start the drop timer and starts a repeating timer that refreshes the panel via UpdatePanels using CUI. Because the timers and the UI both outlive a normal plugin reload if nothing removes them, Unload destroys both the drop and panel timers and loops BasePlayer.activePlayerList calling CuiHelper.DestroyUi for the panel name. The plugin has no persistent gameplay data, but its configuration is saved through SaveConfig. Reloading the plugin repeatedly therefore never stacks extra timers or leaves an orphaned overlay on screen.
Common use cases
Remove CUI overlays
Destroy every panel, HUD or menu element the plugin added so a reload does not leave dead UI on screen or let a deleted command string linger in a button.
Stop timers
Keep Timer fields for
timer.Onceandtimer.Everyand callDestroy()on each, otherwise the callbacks keep firing against a removed plugin instance.Flush in-memory state
Write player caches or statistics to
Interface.Oxide.DataFileSystemso data collected since the last save is not lost on reload or shutdown.
Pitfalls
Timer callbacks reference the old instance
An undisposed timer keeps a reference to the unloaded plugin and will run its lambda after reload, usually causing duplicate messages or errors about a missing plugin.
Undoing permanent registrations
Unregistering permissions or clearing lang entries in
Unloadmakes them vanish on every reload. Leave those to exist for the lifetime of the server.Assuming players are connected
On shutdown or a late unload the player list may be empty or the server may be stopping. Guard loops and never rely on network calls succeeding inside
Unload.
Build a plugin with Unload
Describe what should happen and get a compiled plugin that uses this hook.
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 Unload
Generate