Snake Battle Royale: High-Frequency Game Server optimization project
IPC,game logic setup and low level fruits

The snake server
I've been working on a high-frequency game server optimization project, with a massive multiplayer snake battle royale as the test scenario. The point is to learn and experiment with low-level optimization techniques in Rust by pushing tick rates as high as they'll go.
The target is 200,000+ ticks per second (5µs per tick) with 1000+ concurrent snakes.
The game
1000+ snakes move at the same time on one shared 2D grid. They collide and eat food, and all of it has to stay real-time, with sub-millisecond tick processing.
The architecture is intentionally simple, but performance-critical:
- Single process with two dedicated threads pinned to separate CPU cores
- Custom minimal lock-free queue for inter-thread communication
- 1000×1000 cell grid with fast spatial queries
Making it fast
I know this level of optimization might seem excessive for a snake game. In the real world, bottlenecks are usually in rendering, networking, or other higher-level stuff. But here I have the privilege to ignore the big picture and focus purely on shaving every possible microsecond from the game tick loop. This is my playground for exploring performance optimization techniques that I can't justify in my day job.
Low hanging fruit, rebuilding the grid every tick
My first optimization was just obvious, stop recreating the grid every tick:
- Before: recreating a 1000×1000 grid every tick
- After: incrementally updating only the cells that changed
That one change made a massive difference:
- 1000 snakes with heavy updates: from 51.71 μs down to 8.22 μs
- ~6x improvement
But it also meant managing the grid by hand, which is a bug magnet.
Manual grid updates were a nightmare
With incremental updates, every state change had to be followed by the matching grid update:
- Snake moves: update the old position (empty) and the new position (snake)
- Snake grows: update the new tail segment
- Snake dies: clear all segments
- Apple consumed: remove it from the grid
- Apple spawned: add it to the grid
It worked, but small changes could easily break grid consistency, and maintaining the code was turning into a nightmare. I needed a better model.

Options I considered
A few ideas came up:
Event-driven architecture (rejected)
Use events to decouple state changes from grid updates.
It didn't fit, because snake movement is inherently order-dependent. One snake must move first, then the others operate on the updated state, and events don't handle that sequential dependency well.
Type-level invariants with phantom data (rejected)
Use Rust's type system to enforce grid consistency at compile time. That would mean significant refactoring and complex type machinery, so no.
Smart wrapper types with invariants (the one I went with)
Wrap the data structures in types that handle grid updates automatically. That keeps all the grid logic in one place and the code clean, without costing performance.
Building the wrappers
So I wrote GridAwareSnake and GridAwareApple. Here's the snake one:
pub struct GridAwareSnake {
snake: Snake,
}
impl GridAwareSnake {
pub fn move_forward(&mut self, grid: &mut Grid) -> bool {
let new_head = self.snake.calculate_new_head();
// Check collisions BEFORE moving
if grid.get_cell(&new_head) == Cell::Snake {
self.snake.is_alive = false;
return false;
}
// Clear old tail, move snake, update new head
if let Some(tail) = self.snake.body.back() {
grid.set_cell(*tail, Cell::Empty);
}
self.snake.move_forward();
if let Some(head) = self.snake.body.front() {
grid.set_cell(*head, Cell::Snake);
}
true
}
}
Raw pointers and memory safety
My initial implementation actually stored a raw pointer (*mut Grid) in the wrapper types:
pub struct GridAwareSnake {
snake: Snake,
grid: *mut Grid, // ❌ Dangling pointer after grid moves
}
That broke. GameState::random() created the grid locally and then moved it, so the raw pointers became dangling and I got segmentation faults.
So I switched to passing an explicit grid reference:
pub fn move_forward(&mut self, grid: &mut Grid) -> bool {
// ✅ Safe, explicit dependency
}
The wrappers made it faster
With the wrappers in, tick time went from 8.22 μs to 4.56 μs, another ~1.7x.

This surprised me. I expected the wrapper approach to be slower, because of:
- Additional function call overhead
- More complex control flow
- Extra self-collision detection logic
Why the wrappers are faster
The speedup probably comes from four things.
Better control flow
The old code moved first and checked for collisions after:
snake.move_forward();
if self.grid.get_cell(head) == Cell::Snake {
snake.is_alive = false;
continue; // Dead snake still processes rest of loop
}
The wrapper checks before moving and exits early:
if grid.get_cell(&new_head) == Cell::Snake {
self.snake.is_alive = false;
return false; // Early exit, no further processing
}
Improved apple consumption logic
The old approach checked apple consumption after movement. The wrapper checks it before, with a will_eat_apple flag.
Compiler optimizations
This one is more speculative. The wrapper structure might let the compiler optimize better through:
- More predictable branch patterns
- Better inlining opportunities
- Improved cache locality
Less work for dead snakes
Dead snakes return early and skip the rest of the processing.
Where the numbers ended up
Every step so far:
- Initial: 51.71 μs per tick
- After the grid optimization: 8.22 μs per tick (~6x faster)
- After the smart wrappers: 4.56 μs per tick (another ~1.7x)
- After removing some redundant checks: same performance, even cleaner code
That's ~11x faster in total, and the code is cleaner and easier to maintain than where I started.
What's next
We haven't even touched any of the really juicy low-level stuff yet.
But for now, I'm satisfied with the journey from 51.71 μs to 4.56 μs.