Skip to content
RustPluginGenerator

OnServerShutdown

OnServerShutdown is called by Oxide once while the server is shutting down, immediately before Oxide shuts its own plugins down and saves player data.

Signature
void OnServerShutdown()

Return value

The hook's return value is ignored. Returning anything from OnServerShutdown has no effect on the server shutdown sequence.

How it works

Oxide invokes OnServerShutdown from its Rust core handler IOnServerShutdown, which runs when the server process is stopping. It fires before Oxide's internal shutdown sequence, before player data is persisted by Covalence, and before plugin unload logic runs. It does not fire on save or restart interrupts, only on the actual shutdown path.

The hook receives no arguments, so there is nothing to inspect at call time. Use it for final cleanup that must happen while plugin state is still intact: flushing your own persisted data, recording a timestamp, clearing timers early, or writing an audit entry. Because plugins have not been unloaded yet, methods like SaveData(), Interface.Oxide.DataFileSystem.WriteObject, and player lookups through BasePlayer.activePlayerList still work.

There is no way to narrow this hook by parameter type since it takes none. Keep the body short and free of slow work; the server is in a terminal state and blocking here delays the process exit. Do not rely on it for saves that must be consistent with in-game state, since players may already be disconnecting.

Example plugin: Persist tracked playtime when the server stops

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

namespace Oxide.Plugins
{
    [Info("ShutdownPlaytimeSaver", "RustPluginGenerator", "1.0.0")]
    [Description("Tracks playtime per player and flushes totals when the server shuts down.")]
    public class ShutdownPlaytimeSaver : RustPlugin
    {
        private const string DataFileName = "Playtime";

        private PluginConfig config;
        private PlaytimeData data;
        private Timer flushTimer;
        private readonly Dictionary<ulong, float> joinTimes = new Dictionary<ulong, float>();

        private class PluginConfig
        {
            [JsonProperty("Auto save interval (seconds)")]
            public float AutoSaveInterval { get; set; } = 300f;

            [JsonProperty("Use hours instead of seconds")]
            public bool UseHours { get; set; } = true;
        }

        private class PlaytimeData
        {
            [JsonProperty("Totals")]
            public Dictionary<ulong, double> Totals { get; set; } = new Dictionary<ulong, double>();

            [JsonProperty("Last shutdown (unix)")]
            public long LastShutdown { get; set; }
        }

        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);

        private void Init()
        {
            LoadData();
        }

        private void OnServerInitialized()
        {
            if (config.AutoSaveInterval > 0f)
            {
                flushTimer = timer.Every(config.AutoSaveInterval, SaveData);
            }
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            if (player == null) return;
            joinTimes[player.userID.Get()] = Time.realtimeSinceStartup;
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null) return;
            Accumulate(player.userID.Get());
        }

        private void OnServerShutdown()
        {
            foreach (var entry in joinTimes)
            {
                Accumulate(entry.Key);
            }
            joinTimes.Clear();
            data.LastShutdown = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            SaveData();
        }

        private void Accumulate(ulong userId)
        {
            float start;
            if (!joinTimes.TryGetValue(userId, out start)) return;

            double seconds = Time.realtimeSinceStartup - start;
            if (seconds <= 0d) return;

            double existing;
            data.Totals.TryGetValue(userId, out existing);
            data.Totals[userId] = existing + seconds;
            joinTimes[userId] = Time.realtimeSinceStartup;
        }

        private void LoadData()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<PlaytimeData>(DataFileName) ?? new PlaytimeData();
        }

        private void SaveData()
        {
            if (data == null) return;
            Interface.Oxide.DataFileSystem.WriteObject(DataFileName, data);
        }

        private void Unload()
        {
            if (flushTimer != null)
            {
                flushTimer.Destroy();
                flushTimer = null;
            }
            SaveData();
        }
    }
}

The plugin accumulates per-player connection time in memory (JoinTimes, keyed by Steam ID) using OnPlayerConnected and OnPlayerDisconnected, then writes a running total into a data file. OnServerShutdown flushes every currently tracked player's session into oxide/data/Playtime.json so time recorded since the last save is not lost when the process exits. SaveData is also called by a periodic timer and in Unload so the file stays current during normal operation. OnServerInitialized loads existing totals back into the dictionary so playtime carries across restarts.

Common use cases

  • Flush custom data before exit

    Write any in-memory player statistics, event state, or admin logs to oxide/data so the next boot starts with accurate values.

  • Record shutdown time for reports

    Store a Unix timestamp with DateTime.UtcNow or Epoch.Current so external tooling or a Discord relay can show when the server last stopped.

  • Clean up UI and timers early

    Destroy active CUI panels and timer references here so nothing is left attached to a dying process, complementing the usual Unload teardown.

Pitfalls

  • Not the same as OnServerSave

    OnServerSave fires periodically and is the right place for incremental persistence. Use OnServerShutdown only for the final flush, and keep both paths idempotent.

  • May not fire on hard kills

    A crash, SIGKILL, or abrupt process termination skips this path entirely. Do not treat the shutdown hook as a guarantee that your data was saved.

  • Keep it fast

    Blocking work here delays process exit and can prevent Oxide from finishing its own shutdown. Avoid loops over large entity sets or network calls.

Plugin recipes that use OnServerShutdown

Build a plugin with OnServerShutdown

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

Prompt: Flush custom data before exit using the OnServerShutdown 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 OnServerShutdown

Generate