diff --git a/src/NATS.Server/Configuration/ConfigReloader.cs b/src/NATS.Server/Configuration/ConfigReloader.cs
new file mode 100644
index 0000000..3813323
--- /dev/null
+++ b/src/NATS.Server/Configuration/ConfigReloader.cs
@@ -0,0 +1,341 @@
+// Port of Go server/reload.go — config diffing, validation, and CLI override merging
+// for hot reload support. Reference: golang/nats-server/server/reload.go.
+
+namespace NATS.Server.Configuration;
+
+///
+/// Provides static methods for comparing two instances,
+/// validating that detected changes are reloadable, and merging CLI overrides
+/// so that command-line flags always take precedence over config file values.
+///
+public static class ConfigReloader
+{
+ // Non-reloadable options (match Go server — Host, Port, ServerName require restart)
+ private static readonly HashSet NonReloadable = ["Host", "Port", "ServerName"];
+
+ // Logging-related options
+ private static readonly HashSet LoggingOptions =
+ ["Debug", "Trace", "TraceVerbose", "Logtime", "LogtimeUTC", "LogFile",
+ "LogSizeLimit", "LogMaxFiles", "Syslog", "RemoteSyslog"];
+
+ // Auth-related options
+ private static readonly HashSet AuthOptions =
+ ["Username", "Password", "Authorization", "Users", "NKeys",
+ "NoAuthUser", "AuthTimeout"];
+
+ // TLS-related options
+ private static readonly HashSet TlsOptions =
+ ["TlsCert", "TlsKey", "TlsCaCert", "TlsVerify", "TlsMap",
+ "TlsTimeout", "TlsHandshakeFirst", "TlsHandshakeFirstFallback",
+ "AllowNonTls", "TlsRateLimit", "TlsPinnedCerts"];
+
+ ///
+ /// Compares two instances property by property and returns
+ /// a list of for every property that differs. Each change
+ /// is tagged with the appropriate category flags.
+ ///
+ public static List Diff(NatsOptions oldOpts, NatsOptions newOpts)
+ {
+ var changes = new List();
+
+ // Non-reloadable
+ CompareAndAdd(changes, "Host", oldOpts.Host, newOpts.Host);
+ CompareAndAdd(changes, "Port", oldOpts.Port, newOpts.Port);
+ CompareAndAdd(changes, "ServerName", oldOpts.ServerName, newOpts.ServerName);
+
+ // Logging
+ CompareAndAdd(changes, "Debug", oldOpts.Debug, newOpts.Debug);
+ CompareAndAdd(changes, "Trace", oldOpts.Trace, newOpts.Trace);
+ CompareAndAdd(changes, "TraceVerbose", oldOpts.TraceVerbose, newOpts.TraceVerbose);
+ CompareAndAdd(changes, "Logtime", oldOpts.Logtime, newOpts.Logtime);
+ CompareAndAdd(changes, "LogtimeUTC", oldOpts.LogtimeUTC, newOpts.LogtimeUTC);
+ CompareAndAdd(changes, "LogFile", oldOpts.LogFile, newOpts.LogFile);
+ CompareAndAdd(changes, "LogSizeLimit", oldOpts.LogSizeLimit, newOpts.LogSizeLimit);
+ CompareAndAdd(changes, "LogMaxFiles", oldOpts.LogMaxFiles, newOpts.LogMaxFiles);
+ CompareAndAdd(changes, "Syslog", oldOpts.Syslog, newOpts.Syslog);
+ CompareAndAdd(changes, "RemoteSyslog", oldOpts.RemoteSyslog, newOpts.RemoteSyslog);
+
+ // Auth
+ CompareAndAdd(changes, "Username", oldOpts.Username, newOpts.Username);
+ CompareAndAdd(changes, "Password", oldOpts.Password, newOpts.Password);
+ CompareAndAdd(changes, "Authorization", oldOpts.Authorization, newOpts.Authorization);
+ CompareCollectionAndAdd(changes, "Users", oldOpts.Users, newOpts.Users);
+ CompareCollectionAndAdd(changes, "NKeys", oldOpts.NKeys, newOpts.NKeys);
+ CompareAndAdd(changes, "NoAuthUser", oldOpts.NoAuthUser, newOpts.NoAuthUser);
+ CompareAndAdd(changes, "AuthTimeout", oldOpts.AuthTimeout, newOpts.AuthTimeout);
+
+ // TLS
+ CompareAndAdd(changes, "TlsCert", oldOpts.TlsCert, newOpts.TlsCert);
+ CompareAndAdd(changes, "TlsKey", oldOpts.TlsKey, newOpts.TlsKey);
+ CompareAndAdd(changes, "TlsCaCert", oldOpts.TlsCaCert, newOpts.TlsCaCert);
+ CompareAndAdd(changes, "TlsVerify", oldOpts.TlsVerify, newOpts.TlsVerify);
+ CompareAndAdd(changes, "TlsMap", oldOpts.TlsMap, newOpts.TlsMap);
+ CompareAndAdd(changes, "TlsTimeout", oldOpts.TlsTimeout, newOpts.TlsTimeout);
+ CompareAndAdd(changes, "TlsHandshakeFirst", oldOpts.TlsHandshakeFirst, newOpts.TlsHandshakeFirst);
+ CompareAndAdd(changes, "TlsHandshakeFirstFallback", oldOpts.TlsHandshakeFirstFallback, newOpts.TlsHandshakeFirstFallback);
+ CompareAndAdd(changes, "AllowNonTls", oldOpts.AllowNonTls, newOpts.AllowNonTls);
+ CompareAndAdd(changes, "TlsRateLimit", oldOpts.TlsRateLimit, newOpts.TlsRateLimit);
+ CompareCollectionAndAdd(changes, "TlsPinnedCerts", oldOpts.TlsPinnedCerts, newOpts.TlsPinnedCerts);
+
+ // Limits
+ CompareAndAdd(changes, "MaxConnections", oldOpts.MaxConnections, newOpts.MaxConnections);
+ CompareAndAdd(changes, "MaxPayload", oldOpts.MaxPayload, newOpts.MaxPayload);
+ CompareAndAdd(changes, "MaxPending", oldOpts.MaxPending, newOpts.MaxPending);
+ CompareAndAdd(changes, "WriteDeadline", oldOpts.WriteDeadline, newOpts.WriteDeadline);
+ CompareAndAdd(changes, "PingInterval", oldOpts.PingInterval, newOpts.PingInterval);
+ CompareAndAdd(changes, "MaxPingsOut", oldOpts.MaxPingsOut, newOpts.MaxPingsOut);
+ CompareAndAdd(changes, "MaxControlLine", oldOpts.MaxControlLine, newOpts.MaxControlLine);
+ CompareAndAdd(changes, "MaxSubs", oldOpts.MaxSubs, newOpts.MaxSubs);
+ CompareAndAdd(changes, "MaxSubTokens", oldOpts.MaxSubTokens, newOpts.MaxSubTokens);
+ CompareAndAdd(changes, "MaxTracedMsgLen", oldOpts.MaxTracedMsgLen, newOpts.MaxTracedMsgLen);
+ CompareAndAdd(changes, "MaxClosedClients", oldOpts.MaxClosedClients, newOpts.MaxClosedClients);
+
+ // Misc
+ CompareCollectionAndAdd(changes, "Tags", oldOpts.Tags, newOpts.Tags);
+ CompareAndAdd(changes, "LameDuckDuration", oldOpts.LameDuckDuration, newOpts.LameDuckDuration);
+ CompareAndAdd(changes, "LameDuckGracePeriod", oldOpts.LameDuckGracePeriod, newOpts.LameDuckGracePeriod);
+ CompareAndAdd(changes, "ClientAdvertise", oldOpts.ClientAdvertise, newOpts.ClientAdvertise);
+ CompareAndAdd(changes, "DisableSublistCache", oldOpts.DisableSublistCache, newOpts.DisableSublistCache);
+ CompareAndAdd(changes, "ConnectErrorReports", oldOpts.ConnectErrorReports, newOpts.ConnectErrorReports);
+ CompareAndAdd(changes, "ReconnectErrorReports", oldOpts.ReconnectErrorReports, newOpts.ReconnectErrorReports);
+ CompareAndAdd(changes, "NoHeaderSupport", oldOpts.NoHeaderSupport, newOpts.NoHeaderSupport);
+ CompareAndAdd(changes, "NoSystemAccount", oldOpts.NoSystemAccount, newOpts.NoSystemAccount);
+ CompareAndAdd(changes, "SystemAccount", oldOpts.SystemAccount, newOpts.SystemAccount);
+
+ return changes;
+ }
+
+ ///
+ /// Validates a list of config changes and returns error messages for any
+ /// non-reloadable changes (properties that require a server restart).
+ ///
+ public static List Validate(List changes)
+ {
+ var errors = new List();
+ foreach (var change in changes)
+ {
+ if (change.IsNonReloadable)
+ {
+ errors.Add($"Config reload: '{change.Name}' cannot be changed at runtime (requires restart)");
+ }
+ }
+
+ return errors;
+ }
+
+ ///
+ /// Merges CLI overrides into a freshly-parsed config so that command-line flags
+ /// always take precedence. Only properties whose names appear in
+ /// are copied from to .
+ ///
+ public static void MergeCliOverrides(NatsOptions fromConfig, NatsOptions cliValues, HashSet cliFlags)
+ {
+ foreach (var flag in cliFlags)
+ {
+ switch (flag)
+ {
+ // Non-reloadable
+ case "Host":
+ fromConfig.Host = cliValues.Host;
+ break;
+ case "Port":
+ fromConfig.Port = cliValues.Port;
+ break;
+ case "ServerName":
+ fromConfig.ServerName = cliValues.ServerName;
+ break;
+
+ // Logging
+ case "Debug":
+ fromConfig.Debug = cliValues.Debug;
+ break;
+ case "Trace":
+ fromConfig.Trace = cliValues.Trace;
+ break;
+ case "TraceVerbose":
+ fromConfig.TraceVerbose = cliValues.TraceVerbose;
+ break;
+ case "Logtime":
+ fromConfig.Logtime = cliValues.Logtime;
+ break;
+ case "LogtimeUTC":
+ fromConfig.LogtimeUTC = cliValues.LogtimeUTC;
+ break;
+ case "LogFile":
+ fromConfig.LogFile = cliValues.LogFile;
+ break;
+ case "LogSizeLimit":
+ fromConfig.LogSizeLimit = cliValues.LogSizeLimit;
+ break;
+ case "LogMaxFiles":
+ fromConfig.LogMaxFiles = cliValues.LogMaxFiles;
+ break;
+ case "Syslog":
+ fromConfig.Syslog = cliValues.Syslog;
+ break;
+ case "RemoteSyslog":
+ fromConfig.RemoteSyslog = cliValues.RemoteSyslog;
+ break;
+
+ // Auth
+ case "Username":
+ fromConfig.Username = cliValues.Username;
+ break;
+ case "Password":
+ fromConfig.Password = cliValues.Password;
+ break;
+ case "Authorization":
+ fromConfig.Authorization = cliValues.Authorization;
+ break;
+ case "Users":
+ fromConfig.Users = cliValues.Users;
+ break;
+ case "NKeys":
+ fromConfig.NKeys = cliValues.NKeys;
+ break;
+ case "NoAuthUser":
+ fromConfig.NoAuthUser = cliValues.NoAuthUser;
+ break;
+ case "AuthTimeout":
+ fromConfig.AuthTimeout = cliValues.AuthTimeout;
+ break;
+
+ // TLS
+ case "TlsCert":
+ fromConfig.TlsCert = cliValues.TlsCert;
+ break;
+ case "TlsKey":
+ fromConfig.TlsKey = cliValues.TlsKey;
+ break;
+ case "TlsCaCert":
+ fromConfig.TlsCaCert = cliValues.TlsCaCert;
+ break;
+ case "TlsVerify":
+ fromConfig.TlsVerify = cliValues.TlsVerify;
+ break;
+ case "TlsMap":
+ fromConfig.TlsMap = cliValues.TlsMap;
+ break;
+ case "TlsTimeout":
+ fromConfig.TlsTimeout = cliValues.TlsTimeout;
+ break;
+ case "TlsHandshakeFirst":
+ fromConfig.TlsHandshakeFirst = cliValues.TlsHandshakeFirst;
+ break;
+ case "TlsHandshakeFirstFallback":
+ fromConfig.TlsHandshakeFirstFallback = cliValues.TlsHandshakeFirstFallback;
+ break;
+ case "AllowNonTls":
+ fromConfig.AllowNonTls = cliValues.AllowNonTls;
+ break;
+ case "TlsRateLimit":
+ fromConfig.TlsRateLimit = cliValues.TlsRateLimit;
+ break;
+ case "TlsPinnedCerts":
+ fromConfig.TlsPinnedCerts = cliValues.TlsPinnedCerts;
+ break;
+
+ // Limits
+ case "MaxConnections":
+ fromConfig.MaxConnections = cliValues.MaxConnections;
+ break;
+ case "MaxPayload":
+ fromConfig.MaxPayload = cliValues.MaxPayload;
+ break;
+ case "MaxPending":
+ fromConfig.MaxPending = cliValues.MaxPending;
+ break;
+ case "WriteDeadline":
+ fromConfig.WriteDeadline = cliValues.WriteDeadline;
+ break;
+ case "PingInterval":
+ fromConfig.PingInterval = cliValues.PingInterval;
+ break;
+ case "MaxPingsOut":
+ fromConfig.MaxPingsOut = cliValues.MaxPingsOut;
+ break;
+ case "MaxControlLine":
+ fromConfig.MaxControlLine = cliValues.MaxControlLine;
+ break;
+ case "MaxSubs":
+ fromConfig.MaxSubs = cliValues.MaxSubs;
+ break;
+ case "MaxSubTokens":
+ fromConfig.MaxSubTokens = cliValues.MaxSubTokens;
+ break;
+ case "MaxTracedMsgLen":
+ fromConfig.MaxTracedMsgLen = cliValues.MaxTracedMsgLen;
+ break;
+ case "MaxClosedClients":
+ fromConfig.MaxClosedClients = cliValues.MaxClosedClients;
+ break;
+
+ // Misc
+ case "Tags":
+ fromConfig.Tags = cliValues.Tags;
+ break;
+ case "LameDuckDuration":
+ fromConfig.LameDuckDuration = cliValues.LameDuckDuration;
+ break;
+ case "LameDuckGracePeriod":
+ fromConfig.LameDuckGracePeriod = cliValues.LameDuckGracePeriod;
+ break;
+ case "ClientAdvertise":
+ fromConfig.ClientAdvertise = cliValues.ClientAdvertise;
+ break;
+ case "DisableSublistCache":
+ fromConfig.DisableSublistCache = cliValues.DisableSublistCache;
+ break;
+ case "ConnectErrorReports":
+ fromConfig.ConnectErrorReports = cliValues.ConnectErrorReports;
+ break;
+ case "ReconnectErrorReports":
+ fromConfig.ReconnectErrorReports = cliValues.ReconnectErrorReports;
+ break;
+ case "NoHeaderSupport":
+ fromConfig.NoHeaderSupport = cliValues.NoHeaderSupport;
+ break;
+ case "NoSystemAccount":
+ fromConfig.NoSystemAccount = cliValues.NoSystemAccount;
+ break;
+ case "SystemAccount":
+ fromConfig.SystemAccount = cliValues.SystemAccount;
+ break;
+ }
+ }
+ }
+
+ // ─── Comparison helpers ─────────────────────────────────────────
+
+ private static void CompareAndAdd(List changes, string name, T oldVal, T newVal)
+ {
+ if (!Equals(oldVal, newVal))
+ {
+ changes.Add(new ConfigChange(
+ name,
+ isLoggingChange: LoggingOptions.Contains(name),
+ isAuthChange: AuthOptions.Contains(name),
+ isTlsChange: TlsOptions.Contains(name),
+ isNonReloadable: NonReloadable.Contains(name)));
+ }
+ }
+
+ private static void CompareCollectionAndAdd(List changes, string name, T? oldVal, T? newVal)
+ where T : class
+ {
+ // For collections we compare by reference and null state.
+ // A change from null to non-null (or vice versa), or a different reference, counts as changed.
+ if (ReferenceEquals(oldVal, newVal))
+ return;
+
+ if (oldVal is null || newVal is null || !ReferenceEquals(oldVal, newVal))
+ {
+ changes.Add(new ConfigChange(
+ name,
+ isLoggingChange: LoggingOptions.Contains(name),
+ isAuthChange: AuthOptions.Contains(name),
+ isTlsChange: TlsOptions.Contains(name),
+ isNonReloadable: NonReloadable.Contains(name)));
+ }
+ }
+}
diff --git a/src/NATS.Server/Configuration/IConfigChange.cs b/src/NATS.Server/Configuration/IConfigChange.cs
new file mode 100644
index 0000000..538de12
--- /dev/null
+++ b/src/NATS.Server/Configuration/IConfigChange.cs
@@ -0,0 +1,54 @@
+// Port of Go server/reload.go option interface — represents a single detected
+// configuration change with category flags for reload handling.
+// Reference: golang/nats-server/server/reload.go lines 42-74.
+
+namespace NATS.Server.Configuration;
+
+///
+/// Represents a single detected configuration change during a hot reload.
+/// Category flags indicate what kind of reload action is needed.
+///
+public interface IConfigChange
+{
+ ///
+ /// The property name that changed (matches NatsOptions property name).
+ ///
+ string Name { get; }
+
+ ///
+ /// Whether this change requires reloading the logger.
+ ///
+ bool IsLoggingChange { get; }
+
+ ///
+ /// Whether this change requires reloading authorization.
+ ///
+ bool IsAuthChange { get; }
+
+ ///
+ /// Whether this change requires reloading TLS configuration.
+ ///
+ bool IsTlsChange { get; }
+
+ ///
+ /// Whether this option cannot be changed at runtime (requires restart).
+ ///
+ bool IsNonReloadable { get; }
+}
+
+///
+/// Default implementation of using a primary constructor.
+///
+public sealed class ConfigChange(
+ string name,
+ bool isLoggingChange = false,
+ bool isAuthChange = false,
+ bool isTlsChange = false,
+ bool isNonReloadable = false) : IConfigChange
+{
+ public string Name => name;
+ public bool IsLoggingChange => isLoggingChange;
+ public bool IsAuthChange => isAuthChange;
+ public bool IsTlsChange => isTlsChange;
+ public bool IsNonReloadable => isNonReloadable;
+}
diff --git a/tests/NATS.Server.Tests/ConfigReloadTests.cs b/tests/NATS.Server.Tests/ConfigReloadTests.cs
new file mode 100644
index 0000000..a1d7fcd
--- /dev/null
+++ b/tests/NATS.Server.Tests/ConfigReloadTests.cs
@@ -0,0 +1,89 @@
+using NATS.Server;
+using NATS.Server.Auth;
+using NATS.Server.Configuration;
+
+namespace NATS.Server.Tests;
+
+public class ConfigReloadTests
+{
+ [Fact]
+ public void Diff_NoChanges_ReturnsEmpty()
+ {
+ var old = new NatsOptions { Port = 4222, Debug = true };
+ var @new = new NatsOptions { Port = 4222, Debug = true };
+ var changes = ConfigReloader.Diff(old, @new);
+ changes.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Diff_ReloadableChange_ReturnsChange()
+ {
+ var old = new NatsOptions { Debug = false };
+ var @new = new NatsOptions { Debug = true };
+ var changes = ConfigReloader.Diff(old, @new);
+ changes.Count.ShouldBe(1);
+ changes[0].Name.ShouldBe("Debug");
+ changes[0].IsLoggingChange.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Diff_NonReloadableChange_ReturnsNonReloadableChange()
+ {
+ var old = new NatsOptions { Port = 4222 };
+ var @new = new NatsOptions { Port = 5222 };
+ var changes = ConfigReloader.Diff(old, @new);
+ changes.Count.ShouldBe(1);
+ changes[0].IsNonReloadable.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Diff_MultipleChanges_ReturnsAll()
+ {
+ var old = new NatsOptions { Debug = false, MaxPayload = 1024 };
+ var @new = new NatsOptions { Debug = true, MaxPayload = 2048 };
+ var changes = ConfigReloader.Diff(old, @new);
+ changes.Count.ShouldBe(2);
+ }
+
+ [Fact]
+ public void Diff_AuthChange_MarkedCorrectly()
+ {
+ var old = new NatsOptions { Username = "alice" };
+ var @new = new NatsOptions { Username = "bob" };
+ var changes = ConfigReloader.Diff(old, @new);
+ changes[0].IsAuthChange.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Diff_TlsChange_MarkedCorrectly()
+ {
+ var old = new NatsOptions { TlsCert = "/old/cert.pem" };
+ var @new = new NatsOptions { TlsCert = "/new/cert.pem" };
+ var changes = ConfigReloader.Diff(old, @new);
+ changes[0].IsTlsChange.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Validate_NonReloadableChanges_ReturnsErrors()
+ {
+ var changes = new List
+ {
+ new ConfigChange("Port", isNonReloadable: true),
+ };
+ var errors = ConfigReloader.Validate(changes);
+ errors.Count.ShouldBe(1);
+ errors[0].ShouldContain("Port");
+ }
+
+ [Fact]
+ public void MergeWithCli_CliOverridesConfig()
+ {
+ var fromConfig = new NatsOptions { Port = 5222, Debug = true };
+ var cliFlags = new HashSet { "Port" };
+ var cliValues = new NatsOptions { Port = 4222 };
+
+ ConfigReloader.MergeCliOverrides(fromConfig, cliValues, cliFlags);
+ fromConfig.Port.ShouldBe(4222); // CLI wins
+ fromConfig.Debug.ShouldBeTrue(); // config value kept (not in CLI)
+ }
+}