On a bad connection, the client can show an NPC somewhere different from its server position, especially when path changes arrive late. It predicts movement locally and compensates for latency, then smooths out corrections when updates arrive. For very large corrections, I just let it snap.
The server still decides hits, so this is just a visual issue.
No Humanoids, no server-side models, no physics replication. 250 Roblox monsters as pure data and paths, for under 4 KB/s and ~0.4 ms of server time per frame.
Everything you see is client footage. The client builds and animates every monster from shared data.
How it stays this cheap:
AI LOD. Monster brains near a player think at 60 Hz, within 200 studs at 20 Hz, and beyond that at 10 Hz. At 250 monsters that's ~3,300 brain updates/s instead of ~15,400.
Paths. The server sends each monster's path once, and the client works out where the monster is from the clock. NPC traffic stays under 4 KB/s. Streaming every moving monster's position at 20 Hz would be ~35 KB/s, so that's roughly 10–50× less depending on the moment.
Path smoothing. When a monster gets a new path while it's already moving, the client blends into it. Fast back-to-back paths are virtually undetectable, with no snapping or stutter at the handoff.
Server authority. The server owns every monster's position, AI and hits. The client copy is purely visual. A client could delete every NPC locally and still get hit, because the server never asked it where they were.
Modular brains. Each monster type has its own brain and detection settings, all data-driven. On the client, each monster's renderer has its own animations, sounds and effects.
Result at 250: ~0.4 ms of server AI per frame (p99 under 0.7 ms), about 1.5 µs per monster. The cap is 255 because NPC network IDs are a single byte.
There is a trade-off however. Paths win when they last a while. A monster re-pathing every ~0.1 s to chase a player resends a whole path each time, which can cost more than streaming its position, depending on path length. So the system can switch to position streaming too, the best of both :)
Really the hardest part of this was making the client-sided path smoothing and reconciliation, along with actions such as jumping / climbing being able to work.
This is a Studio solo test, so server and client share one machine. Live ping adds delay on top, which matters most for those fast-re-pathing chasers but is also taken into account on the path smoothing.
Footage is sped up 1.7x, and some details within the rooms might be missing due to streaming enabled when recording.
It's been live in production since launch. Wrote it myself around January 2026, my 5th NPC system.
The only possible improvement I can think of is not using AnimationControllers, and having a sort of animation LOD, to animate far-away monsters at a lower rate.
There's a lot more in it than this post covers. Thinking about open sourcing it, would you use it?
#RobloxDev
The server bottleneck I’ve hit with more active NPCs is brain work (pathfinding and how much each NPC does per update in its current state). This demo mostly avoids that because they’re just roaming. When lots of them are actively chasing targets, the next optimizations are sharing pathfinding work and reducing repeated AI calculations.
So you're right, the 1 byte choice was only because I didn't need more for my use case.
On the client, the biggest cost is the rendering itself, but depending on the NPCs, the server hits its limits faster.
Also fun fact: the map generator from my other post, also used here, already has an underlying 2D grid. That provides a starting point for shared or hierarchical pathfinding, but I never ended up connecting it as I needed to actually finish the game.
@MelleryTercefw@FancyXicy I finished it in January, AI models weren't that powerful specially in Roblox yet. It's doable now though, you'd just need to make sure the structure you end up with is actually nice to work with.
@WUndertaleW This might actually be a great starting point for optimizing the animations. I'll have to benchmark it against the other idea I mentioned as a possible improvement.
Did this before vibe coding was a thing, AI just helped me draw it here 😅
It works as designed, but a map that changes every run might not be it.
Does procedural generation work on Roblox, or do players need a map they can learn?
Visit my website for a more detailed breakdown on the map generator, under Projects -> Wonderland
(7/7)