Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
SpreadTheLoad
Server-side performance optimization for multiplayer: moves object ownership off a struggling client so other players stop inheriting its lag and desync. Valheim never rebalances who simulates what; this does. No client install needed.
| Date uploaded | 5 days ago |
| Version | 0.2.1 |
| Download link | DeathMonger-SpreadTheLoad-0.2.1.zip |
| Downloads | 237 |
| Dependency string | DeathMonger-SpreadTheLoad-0.2.1 |
This mod requires the following mods to function
denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2350README
SpreadTheLoad
Server-side performance optimization for Valheim multiplayer. It moves object ownership off a struggling client so that everybody else stops inheriting that machine's lag.
Install on the dedicated server only. No client needs it, including players running no mods at all. There is no prefab, no RPC and no version check, so nobody is locked out.
The problem
Valheim simulates each creature, ship and workbench on exactly one machine: whichever client owns it. Ownership goes to whoever was in range first, and vanilla never rebalances it — whoever loads a zone keeps it until they walk away.
That is fine until the owner is the slowest machine in the group. Then every arrow you fire at a creature it owns, and every swing at a tree it loaded, travels to that machine and back before anything happens. Its frame time becomes your input delay, and the lag you feel has nothing to do with your own hardware or your ping to the server.
Measured on a three-player server: two clients averaging 18 ms a frame, one at 63 ms, and a round trip through the slow one of 144 ms against an 18 ms ping to the server.
What it does
Three things, all server-side:
- Objects follow whoever is using them — a tree or ore vein moves to the player chopping it
- Ships follow whoever is steering — vanilla picks an arbitrary passenger instead
- Named or detected struggling machines are steered away from shared work
For the third: name the players whose machines should not be handed shared work, and the server stops giving them objects that somebody else is also standing near — and hands back the ones they are already holding, which vanilla will not do on its own.
They still own anything only they are near, so nothing is ever left unsimulated and no creature freezes. When they are the only player in a zone, nothing changes at all.
What it does not do
It will not improve the frame rate of the machine it steers away from. Ownership costs that machine CPU, and a struggling client is usually short of something else — on the server this was built for, the slow client was using less CPU than the healthy ones while rendering a third as many frames. Measurement there found ownership made no difference to its frame time either way.
This mod protects the other players. If you want the slow machine itself to run better, that is a graphics settings and hardware question, and DiagnoseServerLag will tell you which.
Configuration
BepInEx/config/DeathMonger.SpreadTheLoad.cfg, generated on first run.
| Setting | Default | Meaning |
|---|---|---|
Enabled |
true |
Turn off for stock behaviour without removing the mod. |
Yield Players |
(empty) | Who to steer work away from. Empty means the mod does nothing. |
Remember Ids |
true |
Learn and remember network ids, so names keep working. |
Log Activity |
false |
Occasional summary line; never one line per object. |
Ownership Follows Attacker |
true |
Give a tree, rock or ore vein to whoever is hitting it. |
Attacker Dwell Seconds |
5 |
How long it stays put afterwards. |
Auto Detect Struggling Players |
false |
Find struggling machines without naming anyone. |
Stalls Per Minute |
6 |
How many stalls a minute before flagging someone. |
Assign Ship To Captain |
true |
Give a ship to whoever is steering it. |
Yielding Player Retains Boat Helm Ownership |
true |
Whether a yielding player keeps the helm when they steer. |
Naming players
Yield Players takes Steam ids or character names, comma separated.
A character name is not an identity — the same person on a second character stops matching, and
somebody else picking that name starts matching. So a name is used once, to find the player;
their network id is then written to SpreadTheLoad-known-ids.txt beside the config, and from then
on they are matched by id whatever they call their character. Delete a line from that file to
forget someone.
Steam ids are the reliable thing to enter. The server logs them as
Got connection SteamID 7656119... whenever somebody joins.
Detecting struggling players automatically
Yield Players requires you to know who is struggling. Auto Detect Struggling Players works it
out instead — off by default, because deciding on its own to move work away from someone is a
judgement you should opt into.
It does not use ping or connection quality, and that matters. The client this mod was written for had a 24 ms ping — better than one of the healthy players — a clean connection, and 16 frames a second. Every network-level measurement the server can take said it was fine.
What the server can see instead is timing. A client's ZDO updates come from its own update loop, so they arrive at whatever pace that machine manages. The rate is useless — the sender is gated at about 20 Hz, so 30 fps and 200 fps look identical — but the gaps are not. A 479 ms frame, the worst measured on that machine, is a 479 ms hole in the stream, and no healthy client produces one.
So a stall is a gap over 0.3 s, and a player is flagged above Stalls Per Minute averaged over two
minutes. Clearing the flag takes five clean minutes — deliberately harder than earning it, so a
borderline machine does not flap ownership back and forth.
Honest limits:
- A gap says that machine stopped sending, not why. A frozen client and a hiccuping connection look the same. That's acceptable, since routing other players' work through either is a bad idea.
- The last healthy player on the server is never flagged. If everyone is struggling there is nobody to hand work to, and flagging everyone would only churn ownership.
- Flags live in memory only. They are never written to the known-ids file and are dropped when the player disconnects — that file is for identities you chose, not guesses the mod made.
Chopping, mining and anything else you hit
Vanilla never gives a resource to whoever is hitting it. TreeBase, TreeLog, Destructible and
MineRock5 all open their damage handler with if (!m_nview.IsOwner()) return;, and nothing
anywhere calls ClaimOwnership — so the machine that loaded a tree keeps it, and every swing anyone
else makes travels to that machine and back. For that tree, and the next one, indefinitely.
A chopping session is hundreds of interactions against a handful of objects, which makes this the
commonest way a group ends up feeling one person's frame time. Ownership Follows Attacker moves
the object to whoever is working it, so the first swing lands remotely and the rest are local.
The server sees the swings because it already relays them — it forwards every client-to-client RPC — so nothing is installed on any client.
Three details worth knowing:
- The transfer waits 0.4 s. The current owner's damage handler begins by checking it still owns the object, so changing ownership the instant the swing arrives would make it drop that hit.
- A dwell time (
Attacker Dwell Seconds) stops two players working the same tree from bouncing it between them. - A yielding player is never given the object. The yield pass would take it back within two seconds and the next swing would move it again, which is worse than leaving it alone.
Resources only. A tree's entire state is its health in the ZDO, so handing it over costs one owner revision and loses nothing. A creature carries live AI state — its target, its path, its alert timers — that is not all replicated, so moving one mid-fight can make it re-acquire or re-path. That is a real hitch in the least welcome moment, so creatures are left alone until it can be measured rather than reasoned about.
Ships
A ship is simulated by its owner, and steering is sent to that owner in 0.2 s batches, so a captain who does not own the hull waits roughly a quarter second for every turn. Vanilla only reassigns a ship when its owner is not aboard, and then hands it to an arbitrary passenger rather than to the captain - so the wrong person often owns it.
With Assign Ship To Captain, the ship follows the helm.
Note this can give a yielding player an object the rest of the mod would take away, whenever they
are the one steering. That is deliberate, and switchable. Yielding Player Retains Boat Helm Ownership decides it:
- On (default) - they keep the helm. Their steering is responsive; the hull lurches for everyone aboard whenever their machine stalls past Unity's catch-up limit.
- Off - the ship goes to a capable player. Smoother for passengers, but the captain steers through a delay, and a captain fighting a mushy helm is the one who puts the boat into a rock.
Which is better is an open question - reasoned about, not measured. Try both.
An unattended ship has no captain, so it falls back to the ordinary yield rules and moves off a struggling machine like anything else.
Open chests
A container someone has open stays with them, for the same reason a helm does. Vanilla's container code assumes whoever has the window open owns it: only the owner writes the chest back, and the panel is repainted from the network copy whenever the data revision moves. Move the chest to another machine mid-session and the stack they just dragged out reappears in the chest while the one they took sits in their inventory — nothing is duplicated, and closing the chest clears it, but it is not a thing anyone should have to see.
There is no setting for this. A chest is released as soon as it is closed.
Compatibility
Known conflict: ValheimPerformanceOptimizations replaces ZDOMan.ReleaseNearbyZDOS with its own
implementation, which is the vanilla method the yielding half of this mod works through. With it
installed, Yield Players has no effect. Helm ownership is unaffected, because the ship pass runs
on its own rather than through that method.
Rather than fail quietly, it checks twice and says so in the server log — once by asking Harmony who
else has patched that method, and once by noticing that its own decision was never consulted while
players were connected. If you see a NOT WORKING line, believe it.
ServersideQoL_MultiplayerTweaks also reassigns ownership, but by proximity — it gives objects to the closest player. That directly contradicts this mod whenever the closest player is the one you are steering away from. Run one or the other.
Credits
The conflict-detection approach is borrowed from balrond_core_optimizer, which checks Harmony
ownership of its targets and stands down rather than trusting patch order.
CHANGELOG
Changelog
0.2.2 - 2026-10-05
-
A client being sent the world is no longer mistaken for a struggling one. The detector counts gaps in the updates a client sends, and a client loading a region stops sending for stretches at a time. To the server that looks exactly like a machine failing to keep up, and it was counted: a capture on 2026-10-04 had the admin's own login produce 68 stalls in six minutes - 11.3 a minute against a threshold of 6 - which flagged the fastest machine on the server as the one to steer work away from, during the one stretch when it was not even playing yet. Single gaps over five seconds were already ignored for this reason; what that missed is that loading is mostly made of the shorter gaps, dozens of them.
A grace period after joining was the obvious fix and the wrong one: that same login streamed for five minutes, so a window long enough to cover it would be long enough to miss a genuinely bad machine, and would still not cover a portal into unexplored ground. So the server measures the thing itself - how fast each peer's ZDO set is still being filled in - and while that is climbing quickly, the client's silences are treated as the cost of receiving rather than as evidence about its hardware. It lasts exactly as long as the streaming does, whether that is twenty seconds through a portal or five minutes at login.
Loading is not a verdict either way: it neither flags a client nor counts towards the clean run that clears a flag. Where the per-peer counts cannot be read, this reports nobody streaming, so the detector behaves as it did before rather than failing.
0.2.1
-
An object goes to the player standing on it when its owner is nowhere near. Greg's rule, and it fixes the case people actually feel: shoving a boar toward its pen. Every push travels to whoever owns the boar and the corrected position comes back, so the animal lurches - and the owner may be a hundred metres away with no interest in it. The same goes for opening somebody's chest, or anything you are standing over.
Take at 2 m, give up only at 5 m. That gap is what stops an object flitting between two people: once the near player owns it they are at zero distance, so the rule cannot fire again until somebody else is nearer than 2 m while they are further than 5. It settles by construction rather than by a cooldown.
Nothing to do with anyone's machine being slow, unlike everything else here - the object is simply in the wrong hands. Distances come from each player's character ZDO, which syncs every 67 ms since the pacing change, rather than the zone-grained peer reference position.
The active-area predicate is only told an object's position and its owner, never which player is being offered it, so a small patch on
ReleaseNearbyZDOSrecords whose pass is running. Without it the rule could tell that the owner was far away but not that this player was close, and vanilla would have handed the object to whichever peer happened to be iterating. -
The guards now apply to every rule, not just the yield one. The open-container and combat checks sat inside the yield branch, so the rule above would have bypassed both - handing away a chest somebody had open, or a creature mid-fight.
-
Ownership is frozen near fighting. The attacker rule already refused to move anything living, because a creature's target, path and alert timers are not all replicated and the new owner restarts its AI from whatever the ZDO holds - which in play is a boss that stops attacking. The yield rule had no such guard, which was an inconsistency rather than a decision: it works through a predicate that is handed a position and an owner and never learns which object it is being asked about, so it cannot tell a troll from a fence post.
Greg reported exactly that symptom after a Gerhaffa fight, and it does not need the yielded player to have arrived first - ownership moves whenever an owner leaves an object's active area, so simply walking through was enough to acquire the boss and have it taken away again.
Since the object cannot be identified, the guard is spatial: every damage RPC the server relays marks a place and a time, and ownership stops moving within 40 m of recent blows until 15 quiet seconds have passed. Coarse - it protects the fence posts in the fight too - but it errs the safe way, and a fight is the one moment when rebalancing has nothing to offer.
0.2.0
-
Every player is sent their world update each cycle, instead of one player per frame. Vanilla serves exactly one peer per server frame, so each player hears from the server every (players + 1) frames. Measured on bahnsheim at its 30 Hz cap, and the model is exact at both ends: one player
(1+1) x 33.3 = 67 ms, measured 67; five players(5+1) x 33.3 = 200 ms, measured 199, 200, 201 and 204.Five updates a second, with four other people in the world. Everything a player does not own - where everyone else is, what their creatures are doing, the ship they are standing on - arrives at that rate and is interpolated in between. It is the one cost that grows with the size of the group, and it is invisible to every other measurement: in that same capture the server held a perfect 33.3 ms tick on 17% of one core and stalled 3 times in 1800 seconds.
The 50 ms gate in the vanilla loop never signifies, because
m_sendTimerkeeps accumulating during the serving frames and is always long past 0.05 by the end of a cycle. But it reads like an intended 20 updates a second, which the per-frame loop quietly turns into 20/N. This restores that intent:Updates Per Second, default 20, no longer divided by the player count.Safe because the real flow control is untouched.
SendZDOsrefuses outright when the socket's send queue is backed up and caps each package at what is left of 10 KB, so the socket's capacity still decides and the gain is self-limiting rather than a flood. On bahnsheim that queue sits at a median 3.7 KB against a refusal threshold near 8 KB, so expect real improvement rather than a clean six times.
0.1.4
-
A player being hit is no longer treated like a tree. The attacker rule skipped creatures by looking for
BaseAI, and a player does not have one - so when something hit a player, the damage RPC named that player's own ZDO and their character was handed to whoever swung. The receiving client then found it owned aPlayerthat was not its local one and did what vanilla does inPlayer.FixedUpdate: logged "Destroying old local player" and destroyed it. The player's screen went black and they had to rejoin. The test is nowCharacter, which every creature has too, so nothing living is ever moved. -
A chest stays with the player who has it open. Vanilla's container code assumes whoever has the window open owns the ZDO -
OnContainerChangedsaves onlyif (IsOwner()), andLoad()repaints the panel whenever the data revision moves. Steering a yielded player's chest away mid-session broke both: the stack they dragged out reappeared in the chest a moment later while the one they took sat in their inventory. Nothing was ever duplicated and closing the chest cleared it, but it looked alarming. Open containers are now held with their user, the way a ship is held with its captain. The open request names the chest - it is routed through the server, and only travels at all when somebody else owns it - and the container's ownInUseflag says when to let go.
0.1.3
-
A tree, rock or ore vein now goes to whoever is hitting it. Vanilla never does this - TreeBase, TreeLog, Destructible and MineRock5 all open their damage handler with
if (!m_nview.IsOwner()) return;and nothing anywhere calls ClaimOwnership - so the machine that loaded a tree keeps it and every swing anyone else makes travels there and back, for that tree and the next one. A chopping session is hundreds of interactions against a handful of objects, which makes this the commonest way a group feels somebody else's frame time.Resources only. A creature carries live AI state that is not all replicated, so moving one mid-fight can make it re-acquire its target or re-path - a real hitch in the least welcome moment, left alone until it can be measured rather than reasoned about.
Three details that matter: the transfer waits 0.4s so the swing that triggered it lands first (the old owner's handler checks ownership, and changing it immediately drops that hit); a dwell time stops two players bouncing one tree between them; and it never hands an object to a player work is being steered away from, which would only churn.
-
Ship prefabs are found in the right registry. The scan read
ZNetScene.m_prefabs, the list serialised with the scene, and found the five vanilla hulls and none of TheGreatestShips'. Mods register intom_namedPrefabs, which is whatGetPrefabreads. Now 13 hulls including everyDM_*ship.
0.1.2
- The yield list is switched off with the mod.
IsYieldingdid not checkEnabled, so with the mod turned off but names still configured it answered yes about a rule that was not in force. The ship pass consults it, so this was a real bug rather than only a display one. - A small public API,
SpreadTheLoadApi, so DiagnoseServerLag can grey the rows of players work is being steered away from. This mod stays server-only and gains no client half; DSL's server half reads it and sends the answer down with its own report.
0.1.1
Both fixes come from the first run on a real server.
- The "NOT WORKING" alarm was a false positive. It fired on a healthy server with no
conflicting mod installed. IsInPeerActiveArea is only reached from the second half of
(!zdo.HasOwner() || !IsInPeerActiveArea(...)), so an ownerless object short-circuits past it and an object owned by the peer being processed takes the other branch entirely. It is consulted only when one player's active area contains an object somebody else owns - which never happens while everyone is off in their own corner. Silence now only corroborates the Harmony inspection instead of accusing on its own. - Modded ships were missed. The prefab scan ran once at startup and found the five vanilla hulls and none of TheGreatestShips', because mods register their prefabs after ZNetScene exists. It now re-scans whenever the prefab list grows, which self-corrects however late a mod registers.
0.1.0
- First build. Server-side only; nothing to install on clients.
- Steers object ownership away from named players. Valheim simulates each object on the one machine that owns it, gives ownership to whoever was in range first, and never rebalances - so one struggling client's frame time becomes everybody else's input delay. This hands those objects to another player who is already in range, and hands back the ones already held. Anything only the named player is near stays theirs, so nothing is ever left unsimulated.
- Players are identified by network id, not by character name. A name is used once to find
somebody; their id is remembered in
SpreadTheLoad-known-ids.txtand used from then on, so the setting survives them playing a different character. - Optionally finds struggling machines by itself, off by default. Not by ping or connection quality - the machine this was written for had a 24 ms ping and ran at 16 fps - but by timing the gaps between the updates each client sends. Rate saturates around 20 Hz and tells you nothing; a 479 ms hole in the stream tells you plenty. Clearing a flag is deliberately harder than earning one, and the last healthy player is never flagged.
- Ships follow the helm. Vanilla only reassigns a ship when its owner is not aboard, and then picks an arbitrary passenger instead of the captain, while steering is batched to the owner every 0.2s and the physics runs only there - so the person steering often waits a quarter second for every turn. Ship prefabs are found by component, so modded hulls are covered without a list. Whether a yielding player keeps the helm is a setting, defaulting to yes.
- Config changes take effect without a restart. BepInEx keeps the value parsed at startup, so without a file watcher an edited setting would do nothing until the process restarted - which on a dedicated server means disconnecting everybody to change who is being steered away from. The file is watched and reloaded, and every setting is read fresh each pass, so a change lands within a second.
- Says so when it is not working. ValheimPerformanceOptimizations replaces the vanilla method this mod works through, which would otherwise leave it silently inert. It checks both by asking Harmony who else patched that method and by noticing its own decision was never consulted while players were connected.