OnClientDisconnected
OnClientDisconnected is called after a client connection has been torn down and just before the server removes that connection from its tracking. It fires at the network layer, so it runs even when no player entity is available.
void OnClientDisconnected(Network.Connection connection, string reason)Parameters
| Parameter | Type | Description |
|---|---|---|
connection | Network.Connection | The Network.Connection of the client that dropped. It is already marked inactive (connection.active is false) and holds the player's Steam ID via connection.userid, but the player entity may already be gone. |
reason | string | The disconnect reason string reported by the network layer, such as the client closing the connection or an error. It is free text and not guaranteed to match a fixed set of values. |
Return value
The return value is ignored at this injection point. Returning null, false or nothing at all has no effect on the game, and the connection will still be removed from the server list.
How it works
The hook fires inside Network.Server.OnDisconnected, after the connection has been marked inactive and after the framework's own connection callback ran, but before the server drops the connection from its list. Because it is placed at the transport level, it triggers on any disconnect: normal logout, timeouts, kicks and Steam/auth failures alike.
The connection argument gives you userid (the Steam ID) and active; the player object may already be invalid, so resolve names through IDs rather than BasePlayer.FindByID. The reason string is whatever the network layer reports and should be treated as informational only.
Use this hook when you specifically want the raw transport-level signal, for example to log every drop including ones that never reached a spawned player. For gameplay-level cleanup (inventory restoration, vanish state, team bookkeeping) prefer OnPlayerDisconnected, which carries the real BasePlayer. Declaring the parameter as Network.Connection is required here; there is no more specific subtype to narrow with. Keep the handler light because every disconnect passes through it.
Example plugin: Log every client disconnect with reason
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Disconnect Audit", "RustPluginGenerator", "1.0.0")]
[Description("Records recent client disconnects with their reported reason and exposes them to admins")]
public class DisconnectAudit : RustPlugin
{
private const string PermView = "disconnectaudit.view";
private PluginConfig config;
private readonly List<Entry> recent = new List<Entry>();
private class PluginConfig
{
[JsonProperty("Maximum entries kept in memory")]
public int MaxEntries = 50;
[JsonProperty("Ignore entries without a Steam ID")]
public bool IgnoreAnonymous = true;
}
private class Entry
{
public string Time;
public string UserId;
public string Reason;
}
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>
{
["NoEntries"] = "No disconnects recorded yet.",
["Entry"] = "{0} | {1} | {2}",
["Cleared"] = "Disconnect log cleared.",
["Denied"] = "You do not have permission to use this command."
}, this);
}
private string Msg(string key, string userId = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, userId), args);
}
private void Init()
{
permission.RegisterPermission(PermView, this);
}
private void OnClientDisconnected(Network.Connection connection, string reason)
{
if (connection == null) return;
string userId = connection.userid.ToString();
if (config.IgnoreAnonymous && (string.IsNullOrEmpty(userId) || userId == "0")) return;
recent.Add(new Entry
{
Time = DateTime.UtcNow.ToString("HH:mm:ss"),
UserId = userId,
Reason = string.IsNullOrEmpty(reason) ? "unknown" : reason
});
int overflow = recent.Count - Mathf.Max(1, config.MaxEntries);
if (overflow > 0) recent.RemoveRange(0, overflow);
}
[ConsoleCommand("disconnectaudit.list")]
private void CmdList(ConsoleSystem.Arg arg)
{
if (arg == null) return;
if (!arg.IsAdmin && (arg.Player() == null || !permission.UserHasPermission(arg.Player().UserIDString, PermView)))
{
arg.ReplyWith(Msg("Denied"));
return;
}
if (recent.Count == 0)
{
arg.ReplyWith(Msg("NoEntries"));
return;
}
for (int i = recent.Count - 1; i >= 0; i--)
{
Entry e = recent[i];
arg.ReplyWith(Msg("Entry", null, e.Time, e.UserId, e.Reason));
}
}
[ConsoleCommand("disconnectaudit.clear")]
private void CmdClear(ConsoleSystem.Arg arg)
{
if (arg == null) return;
if (!arg.IsAdmin && (arg.Player() == null || !permission.UserHasPermission(arg.Player().UserIDString, PermView)))
{
arg.ReplyWith(Msg("Denied"));
return;
}
recent.Clear();
arg.ReplyWith(Msg("Cleared"));
}
private void Unload()
{
recent.Clear();
}
}
}The plugin records each transport-level disconnect into an in-memory ring buffer of recent entries and prints them on demand. OnClientDisconnected converts the connection's userid into a string, filters out entries not yet authenticated when the config says so, and stores the timestamp, ID, and reason. A console command disconnectaudit.list shows the last N entries to admins, and disconnectaudit.clear wipes the buffer. Nothing is written to disk, so the log is transient and cheap per disconnect, which matters because the hook fires for every drop including timeouts and Steam failures.
Common use cases
Monitor disconnects and reasons
Keep a rolling list of recent drops with their reported reasons so admins can spot patterns like mass timeouts or repeated auth failures.
Alert on suspicious disconnects
Compare a player's last known state against their connection ID and send a Discord-style broadcast (via another plugin) when a player drops unexpectedly during combat.
Release external session data
If another plugin tracks temporary resources keyed by Steam ID, this hook lets you free them even when the player entity was never fully spawned.
Pitfalls
Player object may not exist
The hook fires at the network layer, so
BasePlayer.FindByIDcan return null. Work from the connection's userid and guard any lookup.Overlapping with OnPlayerDisconnected
Both hooks fire on a normal logout. Doing the same cleanup in both will run twice; pick the one that has the data you actually need.
Reason text is not standardized
The reason string comes straight from the network layer and varies by cause. Do not branch on exact matches; log it or treat it as a hint only.
Build a plugin with OnClientDisconnected
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 OnClientDisconnected
Generate