How m-ui hot-reloads users without restarting every connection
Adding a user should not disconnect the other two hundred. m-ui gets there with three reload tiers, chosen by what actually changed, and a couple of details about how sing-box identifies users.
Three tiers, smallest first
| What changed | What happens | Who notices |
|---|---|---|
| User: added, removed, enabled, disabled, credentials, line assignment | ReloadUsers — rebuild each inbound's user list from the database and swap it into the running inbound; re-apply per-user speed and device limits | Only the user whose credentials went away (they are disconnected on purpose) |
| Upstream: added, edited, removed | ReloadUpstreams — render the outbound set, add new outbounds, replace changed ones, remove stale ones; routing rules reference outbounds by name, so they stay as they are. Renaming an upstream changes what the rules reference, and that case is promoted to ReloadAll automatically | Connections through a replaced outbound reconnect; everything else stays |
| Certificate: renewed or overwritten in place | No reload at all — sing-box watches the certificate files and swaps them itself; only a changed certificate path triggers ReloadAll | Nobody |
| Line: protocol, port, TLS, transport, options, deployment | ReloadAll — render the whole config, stop the box, start the new one; if it fails to start, start the previous config again | All connections on that server reconnect (a few seconds) |
The panel picks the tier from the change itself, so an operator never has to decide "is this safe to apply now?". Nodes apply pushed snapshots with the same logic: a snapshot that only differs in users triggers only tier one on every node.
How sing-box identifies a user
Inside a sing-box inbound, users are a list of credentials. The name attached to each entry is a label used for logging and traffic attribution; the credential is what actually matches a connection. Two consequences shape m-ui's design:
- Two entries with the same name and different credentials are, to sing-box, two ways into the same account. m-ui uses this for temporary share links: the share gets a second credential set under the user's name, so the borrower's traffic and devices count against the owner, and revoking the share removes just that credential.
- Replacing the user table with one that lacks a credential makes that credential's future connections fail — but existing connections are already established. So after a swap m-ui explicitly closes the tracked connections of users who were disabled or whose credentials were revoked, on the master and on every node (nodes receive the list of revoked shares with the snapshot). hysteria2 / TUIC / AnyTLS add one more layer: they authenticate once when the session is established and never again for the streams opened inside it, so closing only the streams lets the client open another one immediately. m-ui therefore registers the sessions of these three inbounds and, on a swap, closes the whole session of every user who was removed or whose credentials changed (an inbound whose table did not change keeps every session); kicking a user goes through the same path.
What "swap the user table" costs
Rebuilding a user list is a database read plus a call into the inbound's user manager. It runs in milliseconds for hundreds of users, and it runs inside the panel process, so there is no config file, no signal and no race with a supervisor restarting the core.
Why upstream changes do not restart either
sing-box's outbound manager can add and remove outbounds at run time. m-ui diffs the rendered outbound set against what is running (by tag and rendered options), applies only the difference, and updates the routing rules that map lines to exits. Switching a line from direct to an upstream, or rotating an upstream's credentials, therefore never touches the inbounds.
When a restart is unavoidable
Listeners cannot change protocol or port in place, so a line edit restarts the data plane. Two guards keep that from being risky:
- The new configuration was already parsed by sing-box inside the save transaction; if it did not parse, the save failed and nothing restarted.
- If it parses but does not start — typically a port that another process took in the meantime — the previous configuration is started again and the panel shows the error. The server is never left without a data plane.
Port conflicts with the panel, other lines and other processes are also rejected at save time, which is why the restart path rarely has to fall back at all.
Related: why the core is embedded · how nodes receive the same changes · runner/ on GitHub