Cut Fish-Net Bandwidth Costs: Network LOD and Serialization for High-Entity Unity IO Games
Dense Unity IO games burn money on the wire. Hundreds of entities move every tick, browser clients sit on constrained WebGL paths, and server egress becomes the line item that scales with success. Fish-Net already sits in a strong place for that problem space—bandwidth and resource optimization are core reasons teams pick it—but defaults still send more than a fat world needs. The fix is not a single checkbox. It is an egress-first stack: distance-tiered synchronization, deliberate serialization packing, and observer conditions that stop unnecessary data before it leaves the host.
Network Level of Detail (a Pro feature) is the spine of that stack. Configured on ObserverManager and opted in per NetworkObject, it scales how often objects reconcile and update by how far they sit from each player. Nearby entities keep full-speed fidelity; mid-range and far brackets throttle toward a Maximum Send Interval, with exponential scaling between brackets, while replicates keep flowing so ownership and critical state stay honest. Pair that with Prediction Manager local state creation and clients can start reconciling objects they sprint toward before the next server LOD pass, which avoids the pop and walk-through desync that naive throttling creates.
LOD alone is not enough if every update is fat. Fish-Net auto-serializes many C# and Unity types plus public fields on your types, but high-entity IO wins come from [ExcludeSerialization], custom Write/Read packing, and NetworkTransform packing levels that trade precision for bytes. Layer multi-condition Area of Interest observers—and scene conditions—so clients never subscribe to objects outside their world slice. That combination is how you keep nearby play crisp, distant crowds cheap, and browser sessions playable without lighting server bills on fire. The sections below walk through configuration, packing discipline, observer design, and how to measure the result under real CCU rather than empty-scene defaults.
When Dense IO Worlds Turn Bandwidth Into a Cloud Bill
Outbound traffic in a high-entity IO title is cloud egress and concurrent-user headroom long before it is a smoothness metric on a graph. Every replicate, reconcile, and transform update leaves your host as paid bytes. At real CCU those bytes decide whether sessions stay affordable—or whether you thin the world, raise prices, or cap rooms early.
Low-entity multiplayer can absorb chatty sync. A dozen avatars, sparse state, and bursts of motion leave margin. IO and fat patterns erase that margin. Hundreds or thousands of small networked objects—pellets, projectiles, pickups, swarm AI—move constantly and publish state often. Nearby play still has to feel immediate, so base send rates stay aggressive. Object count × tick rate × payload size is a steeper curve than traditional lobby-scale games ever draw.
Bandwidth and resource optimization therefore act as direct server-cost levers for large player counts. The main advantages cited for Fish-Networking are framed that way: bandwidth and resource optimization that can save on server costs, support for large player counts from dozens to hundreds, and built-in prediction with lag compensation. When idle objects and distant crowds still consume CPU and egress, the stack that sends less while retaining performance under load is what keeps browser and dense IO sessions viable.
Older official benchmarks—bandwidth metrics that should be re-measured on modern builds—still show why teams treat packing and interest management as first-order design, not late polish. Fish-Net used 67–78% less bandwidth than Mirror in those comparable tests, and at 200 CCU it retained 92.6% server performance while Mirror retained only 16.8%. Treat the figures as historical motivation. Your own traffic shape under real CCU is what you ship against; the gap they describe is why Network LOD, serialization discipline, and multi-condition observers come before you scale.
Where High-Entity Fish-Net Traffic Actually Comes From
Those three levers only pay off once you know which packets dominate the outbound stream. In a dense IO session the bulk of Fish-Net egress is not mystery framing or transport overhead—it is a small set of systems writing state every tick for every object a client is allowed to see. Map those producers first, and the rest of the optimization path becomes obvious.
The three streams that fill the pipe
NetworkTransform traffic is usually the loudest. Every relevant, moving object that still has an observer writes position, rotation, and often scale on a cadence you control. Multiply that by hundreds of food pellets, projectiles, or AI agents and you get a continuous transform flood—especially when every client is told about every object in the scene. Packing levels and send intervals matter later; the first question is whether that transform should leave the server at all for a given peer.
SyncVar and SyncType updates are the next layer. Health, score, team, skin IDs, inventory flags—anything marked for automatic replication—emits when values change (and, depending on settings, on join or at intervals). In IO designs these fields churn constantly on owned and nearby entities. Unfiltered, they ride alongside transforms and inflate every interest set you leave too wide.
Prediction reconcile traffic hits owned or otherwise predicted objects. The server sends authoritative state so the client can correct its local simulation. Reconciles are necessary for responsive feel, but they are also high-frequency and sensitive to distance: far-away objects do not need full reconcile fidelity on every tick. Left untiered, reconcile bandwidth scales with how many predicted objects you keep “hot” for each connection.
Not sending is the first win
Before you shrink a payload, ask whether the client should receive that object’s state at all. Observer conditions and area-of-interest rules exist to drop whole categories of traffic—not only to limit what cheaters can see. Fish-Net lets you stack multiple conditions at once (distance, scene, custom host logic, and more), so an object can fail out of a client’s interest set for several independent reasons. When it does, NetworkTransforms, SyncTypes, and reconciles for that object simply never enqueue for that connection. That is the single largest bandwidth cut available in a high-entity world: fewer recipients per object, every tick.
Hot path cost is part of the bill
Bandwidth is only half the story under load. Fish-Net is built around a tick-based core with zero allocations on the hot path, which keeps GC spikes from compounding when thousands of objects are evaluated each frame. That stands in contrast to stacks that allocate regularly under comparable sync pressure. Pair that with built-in or custom object pooling that resets network values—including SyncTypes—when instances return to the pool, and you avoid both leaked state and allocation churn as entities spawn and despawn at IO rates. Stable server frame time is what lets aggressive observer culling and later LOD tiers actually hold under real CCU.
Hold a simple mental model before you touch transport choice or hosting topology: reduce who receives data (multi-condition observers), how often they receive it (intervals and distance-tiered LOD on the next step of this path), and how small each payload is (serialization exclusions, custom serializers, NetworkTransform packing). Everything that follows in this article is a refinement of those three axes.
Network LOD: Distance Brackets That Throttle Sync Without Killing Fidelity
Three levers. Multi-condition observers already decide who is in the interest set. Network LOD is how Fish-Net Pro answers how often those interested clients still hear from each object—especially when the arena is packed with pellets, bots, and other players that do not all deserve full-rate reconciliation traffic.
Network Level of Detail scales synchronization frequency by distance from the observing player. Nearby objects keep high-fidelity precision; distant ones are throttled so the server stops shipping full-rate state and reconcile traffic to every client that merely has them in view. The documented outcome is straightforward: by throttling updates for distant entities while maintaining high-fidelity precision for nearby interactions, you significantly reduce server bandwidth and improve client-side CPU performance. In a high-entity IO title that is the difference between every idle pellet and edge-of-map NPC costing a full stream, and only the fight in front of the player doing so.
Enabling LOD on ObserverManager and per NetworkObject
Configuration lives on the ObserverManager. Enable Use Level of Detail—this turns the system on and surfaces the options; it requires Fish-Net Pro. From there you define the Level of Detail Distances brackets (the distance thresholds that separate near, mid, and far tiers) and a Maximum Send Interval that caps how slowly the farthest tier may update. LOD is not automatic on every spawned object: each NetworkObject that should participate must opt in. That per-object gate matters in IO layouts where some actors (the local player’s own body, a boss, a contested objective) should never leave full-rate sync even when another client is far away.
Near, mid, and far—and what still always sends
Behavior across brackets is explicit. Nearby objects sync at the standard full-speed interval; mid-range objects drop to a reduced mid-tier frequency; far objects sync only at the maximum allowed delay. Scaling between brackets is exponential, so the falloff is steeper as distance grows rather than a flat step that feels wrong at the boundary. Critically, replicates always send. Ownership input paths stay responsive; you are not starving the prediction pipeline of the owner’s own actions just because another client is watching from across the map. LOD prioritizes cutting reconciles and other distance-sensitive outbound state—the expensive server→client traffic—while leaving the replicate path intact.
Map those brackets to a typical IO arena rather than abstract units. Near should cover the tight combat radius where collision, eating, and skillshots actually decide rounds—often a small circle around the player where full-rate transforms and reconciles earn their bytes. Mid covers the surrounding ring of visible threats and contested resources a player is tracking but not yet touching. Far is everything else: map-edge pellets, roaming NPCs, and players on the opposite side of a large open field. For that far tier, set Maximum Send Interval in multi-second territory (for example every several seconds) so a client still eventually converges on distant state without paying full-rate egress for thousands of entities that are not in the fight.
The before/after mental model is simple. Before LOD, every object still in a client’s observer set streams at the same aggressive interval—fine for a dozen characters, ruinous when the interest set still holds hundreds or thousands of movers. After LOD, the same interest set is tiered: full-rate only where proximity justifies it, exponentially slower mid-band, and Maximum Send Interval for the long tail. Server outbound shrinks with distance; client CPU stops reconciling and applying state it barely needs. Combined with the multi-condition observers already dropping whole objects from interest, you now control both membership and rate—the two largest levers before you touch how small each remaining payload is.
Pack Every Remaining Byte: Serialization and NetworkTransform Levels
That last lever—how small each remaining payload is—starts with what Fish-Net already does for free. Fish-Net automatically creates serializers for many basic C# and Unity types, as well as classes and structs built from serializable public fields and properties. For an IO food pellet, score pickup, or compact player-state blob, you can often ship a clean type without hand-rolling writers, as long as every public member is something the generator understands.
Strip what the wire never needed
The common trap is letting cosmetic or client-only data ride along on a networked type. Tint colors, VFX seeds, debug labels, and UI-only display names inflate every update when they stay in the serialized surface. Mark them with [ExcludeSerialization] so the generated path skips them entirely. On a dense pellet or XP-orb type that still sits inside a mid-range LOD bracket, dropping two or three floats or strings per object compounds across thousands of entities that observers have not fully culled.
When default packing still wastes bits—or your compact food, score, and state structs live in a shared assembly the generator does not see—add custom Write and Read extension methods. For types that cross assembly boundaries, enable UseGlobalCustomSerializer so Fish-Net picks those extensions up everywhere. Aim the custom path at fixed-point scores, quantized attributes, or bit-packed flags rather than full floats; most IO entity state never needs full single-precision fidelity on every field.
NetworkTransform packing: precision where it matters
Movement is the other constant stream. NetworkTransform packing levels trade precision for bandwidth: Unpacked keeps maximum precision at the highest cost; Packed squeezes the transform hardest for the lowest bandwidth. Less packing equals more precision at the cost of more network bandwidth used. Between those poles, choose intermediate packing when gameplay can tolerate it. Distant map-edge wanderers and slow ambient creatures almost always can; tight arena combat and fast dash abilities often cannot. Match packing to the same distance intuition you already use for LOD brackets—nearby precision, far economy—without forcing every object onto one global setting.
Keep the interpolation buffer size as a separate smoothness-versus-feel knob. A larger buffer can mask jitter on the client, but it will not fix an oversized payload. If you are padding the buffer to hide fat NetworkTransform or SyncType traffic, fix the serialization and packing first. Profile the lean path under real CCU rather than tuning feel knobs to paper over bytes that never needed to leave the server.
Smooth Approaches: Pairing LOD with Local State Creation
Even with payloads packed tight, aggressive distance brackets create a different failure mode: a player boosts or drifts toward something still sitting on a multi-second far interval. Until the next LOD recalculation, that object is still thin on the wire. The result is pop-in, a late snap into place, or the classic walk-through where your blob clips a body that has not updated in time. In dense IO maps that is not a rare edge case—it is the moment players notice the optimization.
Fish-Net Pro’s answer is not to abandon LOD or raise every far interval. Network LOD is designed to cooperate with Prediction Manager Local State Creation, which is on by default. When an object crosses into a nearer interest range on the client, the client can start reconciling it locally before the server’s next LOD refresh lands. As the docs put it: even if the server has not yet recalculated LOD for an object you just sprinted toward, the client will automatically begin reconciling nearby objects locally. That bridge keeps the bandwidth win—distant entities still throttle hard—while the approach feels continuous instead of stepped.
Keep the scope tight here. This is not a full client-side prediction setup guide. You are only confirming the LOD-adjacent switch that preserves your observer and packing gains without ruining feel. Local State Creation is the piece that stops far-bracket thrift from reading as desync when someone closes distance fast. Leave deeper prediction graphs, reconcile tuning, and input replay for a dedicated prediction pass; for bandwidth work, treat this as the safety latch on your distance tiers.
IO moments that stress the pair
Playtest the combinations that spike approach speed or entity importance. Boost into a contested pellet cluster and watch whether food and rival outlines resolve before contact. Rush a large body after a mass spike and check that collision and swallow windows match what the player sees, not a stale far-interval silhouette. Circle the edge of a fight where many NetworkObjects are crossing mid and near brackets at once. Verify that Local State Creation is enabled, that LOD is actually opted in on those prefabs, and that you are not masking remaining fat payloads with interpolation buffer padding. If approaches still hitch, the interval ladder or packing level is wrong—not the prediction feature. When the pair is healthy, clients only pull full-rate fidelity as proximity justifies it, and the sprint-in never exposes the throttle.
Benchmark Real Egress, Then Lock the Dense-World Stack
That health check only sticks if you can prove it under load. The pair of Network LOD and local state creation is doing its job when nearby combat stays sharp and distant map traffic barely moves the needle—but the only way to know is to measure outbound bytes and server headroom on a build that looks like your live IO world, not a demo scene.
Published figures are useful anchors for expectation-setting, not guarantees. In the same-era Fish-Net vs Mirror benchmarks, at 100 CCU the Fish-Net server sent roughly 7.3 mB/s (adjusted 0.55 mB) against Mirror’s 31.8 mB/s (adjusted 30.2 mB), with Fish-Net clients holding higher FPS. With 4000 idle networked objects, Fish-Net retained 100% server performance while Mirror fell to 36%, and Fish-Net frame times stayed flat around 1.5 ms across the full 0–4000 range. An independent 2023 transform-bandwidth pass (default settings) put Fish-Net server out at 62 / 103 / 145 kBps across Move Y, Move All Axis, and Wander scenarios—well under Mirror and NGO in that test suite, though some heavily compressed stacks landed lower still. Treat every one of those numbers as a starting reference: your entity mix, tick rate, observer graph, packing choices, and especially a browser/WebGL transport path will move the result. Project-specific profiling is mandatory before you set CCU or egress budgets.
How to profile the stack you actually ship
Enable Fish-Net’s BandwidthDisplay on a representative server build and run scenarios that match production density: full pellet fields, contested clusters, boost-into-pack approaches, and idle map edges. Capture server send/receive totals and per-client averages at the CCU and entity counts you care about. Toggle Network LOD off versus on (same distance brackets and Maximum Send Interval you plan to ship), then sweep NetworkTransform packing from Unpacked through Packed on the high-count prefabs. Pair the in-editor readout with OS and network tools—interface counters, process bandwidth, and packet captures on the host—so you can separate Fish-Net payload from transport overhead and any WebGL/Bayou path cost. What you want is a clear before/after on whole-object drops (observers), interval throttling (LOD), and per-message size (serialization and packing). If far-bracket traffic still dominates, tighten distances or intervals; if near-bracket payloads look fat, revisit ExcludeSerialization and custom writers before you touch tick rate.
A shippable dense-world config stack
When the numbers look right, lock the stack so the next content pass cannot quietly undo the savings. Use this as the regression gate before you raise CCU targets or open a new map tier:
- Network LOD enabled on ObserverManager with explicit Level of Detail Distances and Maximum Send Interval; opted in per NetworkObject on every high-count prefab (pellets, NPCs, debris)—left off only where full-rate is non-negotiable.
- Packing and serialization defaults matched to role: Packed (or the lowest tolerable level) plus aggressive
[ExcludeSerialization]on pellet/food/score structs; higher precision reserved for player bodies and anything prediction reconciles every tick; custom Write/Read extensions registered where cross-assembly types appear. - Observer conditions as the outer traffic gate—distance/AOI plus scene (and any team or state conditions you need) so whole objects never enter a client’s interest set until justified.
- Pooling enabled for the same high-churn prefabs, with SyncTypes reset on rent so you do not pay spawn/despawn and dirty-state spikes under IO eat/respawn loops.
- Local State Creation left on (Prediction Manager default) so LOD approaches stay smooth; verified in playtests for boost-into-cluster and contested-pellet cases.
- Regression checklist before CCU bumps: BandwidthDisplay + OS counters at target density with LOD on/off and packing sweeps; confirm replicates still flow, far intervals hold, no hitch on sprint-in; re-check WebGL/browser builds separately if that is a ship target.
Fish-Net’s advantage in high-entity IO is not a single switch—it is the compound effect of who receives data, how often, and how small each payload is. LOD brackets, packed serializers, multi-condition observers, and pooling are the levers; BandwidthDisplay under real CCU is the proof. Lock that stack, re-run the checklist when content or maps change, and egress stays a controlled cost instead of the bill that scales with every new pellet on the field.