Howdy. You’ve Reached Borger.
The Problem
In a fast-paced multiplayer video game, latency is the root of all evil. The fundamental problem that you as a developer face is the highly variable amount of time between one player tapping a key and 10 other players seeing it.
Take, for example, a simple racing game in which 3 players drive their cars toward a self destruct button. The first player to cross the finish line explodes violently and wins the game as charred gears rain from the sky.
💥🏁 🚗💨
💥🏁 🚙💨
💥🏁 🏎️💨
However, if you were to sit the 3 players right next to each other and observe all 3 of their screens, you’d see something very odd:
Player 1
💥🏁 🚗💨
💥🏁 🚙💨
💥🏁 🏎️💨
Player 2
💥🏁 🚗💨
💥🏁 🚙💨
💥🏁 🏎️💨
Player 3
💥🏁 🚗💨
💥🏁 🚙💨
💥🏁 🏎️💨
You’re witnessing the effects of network latency (also known as ping in gaming communities): the amount of time it takes for information to disseminate to all players across the game session.
- When player 1 presses the left arrow key, they see their car move immediately, and the game feels responsive to them.
- When player 2 presses the left arrow key, player 1 won’t see any change for a fraction of a second.
Player 1 is looking at a delayed, outdated version of player 2’s car. The delay is caused by how slow it is to send information, such as key presses, across the internet. Even players under the same roof have a small but noticeable amount of latency across their devices. Players on opposite sides of the globe are up against the speed of light and face a very high ping; perhaps 200-300 milliseconds and constantly fluctuating.
While the visual discrepancy is annoying enough on its own, it also wreaks havoc while writing game logic. What happens when all 3 players think they got to the finish line first? How do you make sure that only 1 player sees that they won? If 2 cars crash into each other, where do you render the dents? Your brain quickly melts and smells bad from trying to answer each individual question in the form of code.
The Solution
Borger uses a technique called rollback netcode to automatically resolve these types of conflicts. It allows writing multiplayer game logic with code that looks no different from single-player logic, solving one of the hardest problems in gamedev in a way that you don’t even need to think about it.
Installation
Hardware Requirements
Although games built with Borger are very small and efficient, running 2 rustc instances and rust-analyzer at the same time during development is absolutely brutal. At bare minimum, you will need at least 12GB of RAM and 10GB of available disk storage.
Software Requirements
- Bash: Required to run Borger’s CLI tool. Installing it depends on your operating system:
- Linux: Any self-respecting distribution should already have it. Borger itself was written on a 2014 Dell Precision M2800 equipped with Xubuntu.
- macOS:
- First you must purchase expensive, unrepairable, unupgradable hardware
- Check your Bash version in the terminal:
bash --version. If it outputs version 3.2, congrats, your Bash is 20 years out of date. Otherwise, your Bash should be good to go already. - Before you can update it, you’ll need Homebrew installed.
brew install bash- Check the version again to be on the safe side. If it’s still stuck at 3.2, restart the terminal.
- Windows: Requires Windows Subsystem for Linux (WSL), a mini Linux virtual machine. It is highly probable that Bill Gates performed unscrupulous activities with Jeffrey Epstein.
- Git: Used internally by the CLI tool to generate projects
More recommendations
- Visual Studio Code - Technically any IDE will do the job (or even a plain text editor if you hate being productive), but Borger is preconfigured to work with VSCode.
- rust-analyzer - It’s nearly impossible to write Rust without this
- Even Better TOML - Syntax highlighting for TOML files
- CodeLLDB - For debugging the game server. Because game logic is shared between server and client, this is often easier than trying to debug Rust in-browser.
- If you do find yourself needing to debug Rust with your preferred browser’s DevTools:
- Chromium-based (Chrome, Brave, Edge, Opera, etc.)
- Firefox - Unpleasant but surprisingly doable
- Safari (lol)
Installing Borger
Open a terminal and run the command:
curl -fsSL https://eat.borger.dev | bash
This installs the latest borger CLI tool, as well as Rustup, Bun, Cargo Watch, and wasm-pack if they can’t be found. If asked upon completion, close and restart the terminal.
Development Cycle
Creating a New Project
Replace my_game with the name of the project folder to be created.
borger init my_game
cd my_game
borger dev
Loading an Existing Project
git clone https://whatever/my_game.git
cd my_game
borger setup
borger dev
Using borger dev
The dev command runs several parallel jobs:
- Automatically recompile each time you modify code
- Host a local game server
- Host a local game client web (HTTPS) server - specifically Vite, which supports hot reloading HTML, CSS, and sometimes even graphics without having to refresh the page or restart the game.
It can be tricky at first to decipher when dev is finished and it’s safe to load the game. The golden rule is:
- The most recent output of
[SERVER-RUST]isit's alive.The text is highlighted neon green and is hard to miss. - AND
- The most recent output of
[CLIENT-RUST]is[Finished running. Exit status: 0] - The server usually finishes a few seconds before the client
Here’s an example of a game that’s ready to go:
Sometimes during compilation, you’ll see the harmless error:
Cannot find module '@borger/rs' or its corresponding type declarations.
This can be safely ignored. For unknown reasons, the wasm-pack tool deletes the old WASM build before beginning, so for a few seconds during compilation, the module doesn’t exist. As seen in the screenshot (Found 0 errors after it tries again), it corrects itself upon completion.
A few more helpful pointers:
-
Visit https://localhost:5173 in your browser to finally see the “game”! (A blank page by default)
-
If you ever forget the URL, the Vite server reminds you:
[CLIENT-VITE] ➜ Local: https://localhost:5173/ -
Push F12 or Ctrl+Shift+I to open the DevTools console in order to verify the engine loaded successfully

-
Push Ctrl+C in the terminal to close dev mode
-
The dev server uses something called self-signed certificates. If you’ve never worked with these before, you’ll see a terrifying error the first time you try to test the game:

In most cases, the browser is correct to scare you, but local web development is a notable exception. Choose
Advanced -> Proceed. Essentially what’s happened is the browser is unable to verify that this is a legitimate website, because it hasn’t been deployed anywhere yet.
Project Directory Structure
/src/state.ts - Declaration of networked state/src/presentation/index.ts - Presentation logic entry point (rendering, UI, audio)/src/simulation/lib.rs - Simulation logic entry point (game logic)/src/simulation/input.rs - Input handling callbacks/index.html - Main webpage, client entry point/assets - Art files loaded by the game/borger - Source code of the framework, linked via a Git submodule/Cargo.toml - Rust library dependencies/package.json - Java/TypeScript library dependencies/rust-toolchain.toml - Change which version of Nightly Rust to compile with/vite.config.ts - Install Vite plugins (such as React) for better hot reloading support
The rest can usually be ignored.
Deployment
Release build
borger release --build
This produces a release folder containing:
/release/server- The final native executable, compiled to run on your OS/release/client- The static webpage, which can be hosted with any HTTP server capable of serving files
Hosting
Deployment is a bit of a manual procedure at the moment due to requirements that Borger isn’t able to provide for free:
- A domain name. TLS certificate providers typically refuse to generate certificates for a raw IP address.
- At least one always-on dedicated server. Pros with a high concurrent user (CCU) count typically have multiple across different regions of the world to handle demand.
If enough demand is shown, a paid service will be introduced in order to automate this process. For now, server orchestration and matchmaking are outside the scope of the problems Borger aims to solve.
The simplest, cheapest possible procedure for a small friend group looks something like this. It’s not recommended for any significant amount of CCU, so this guide won’t go into great detail.
-
Acquire a domain name. If you don’t mind using a subdomain, check out Duck DNS.
-
Acquire a server. You could either self-host on your own hardware at home (keep in mind that home IP addresses often change), or rent from a cloud provider. Oracle Cloud has an “always free” tier for their bottom of the barrel servers.
-
Point the domain name at the server’s IP address. Set TTL to 0 to make it propagate fast. For example:

-
Transport Layer Port Application Protocol Purpose UDP 6969 WebTransport Game Server TCP 6996 WebSocket Game Server TCP 80 HTTP Game Client TCP 443 HTTPS Game Client -
TLS Certificates can be obtained for free through a service called Let’s Encrypt, via a tool called Certbot
-
Run the servers, on the server:
borger release --run --fullchain /etc/letsencrypt/live/borger.land/fullchain.pem --privkey /etc/letsencrypt/live/borger.land/privkey.pem
Server and Client
Borger implements standard client-server architecture.

- This diagram represents 3 players with different devices who are in a shared multiplayer game session together.
- The red lines represent internet connections along which data is sent back and forth.
- You can see that clients don’t talk to each other directly: all data flows through a central game server that everybody is connected to at the same time.
- All 4 devices, both server and clients, run the same simulation logic. Borger makes sure that the same code runs everywhere.
Server
The server exists to be the source of truth for all connected clients. Each game session has exactly 1 server.
- The server’s state is said to be authoritative: regardless of what each client sees, only the server’s version of the game state is considered to be correct.
- When a client joins, it downloads the current game from the server.
- It continuously broadcasts small updates known as “diffs” to client, as the state changes from tick to tick.
Client
The term client is used loosely to refer to a group of a few different things at once. Each individual client is:
- A device
- The connection between that device and the server
- An instance of the game running on the device
- The human who’s playing said game
- The client’s state is said to be a prediction. Because of network latency, the client needs to try to predict what the server will send before the client receives anything.
- The most important thing to predict is the consequences of inputs. When the player tries to move their character, this should happen immediately without waiting for the server’s response.
- Classifying and treating the client’s state as a prediction is what prevents certain forms of cheating. It’s not real, so it can’t hurt you!
- It continuously sends input events to the server: button presses, joysticks, etc. These inputs are the client’s only means of impacting the game session.
- And of course, clients run presentation logic, unlike the server.
Simulation and Presentation
All game code in a Borger project must be categorized as either simulation or presentation logic before writing anything.
- It’s very difficult to pick incorrectly thanks to the language split: simulation is written in Rust, and presentation in TypeScript.
- Each type of logic is primarily driven by its own loop that fires at a specific interval. Each iteration of each loop is called a “tick”.
Simulation
The simulation is the core game logic that runs on both the server and client: enforcement of game rules, movement and collision, entity creation and removal, and other state changes/mutation.
- The tick rate of the simulation loop is a fixed 30Hz (iterations per second), or once every 33 milliseconds (derived by dividing 1000/30Hz).
- Each tick is assigned an incremental ID. When the server first launches, it starts at ID 0, then the next tick that happens 33ms later has ID 1, etc. When a client joins, they start simulating at whichever ID the server is currently at and automatically stay in sync.
- Each tick ID may be resimulated multiple times due to rollback. Because of this, the code running in the loop must be deterministic depending on the trade-off used.
Presentation
Presentation asks the question: “How should the simulation be presented to the player?” It deals with rendering, UI, audio, and listening for input events.
- The tick rate of the presentation loop is variable and tends to hover near (but practically never identical to) the player’s display refresh rate. This is most commonly 60Hz (iterations per second), or higher if they have disposable income.
- Presentation output state state is read-only. It’s meant to be read and, well, presented, as is.
- Crucially, presentation does not care why the output is what it is. It blindly presents whatever the simulation tells it to.
- Although the presentation loop itself does not undergo rollback, it does have to be aware that the output it’s receiving may have undergone rollback. Again, this is achieved by blindly trusting the simulation’s output, even when it makes no sense: players teleporting, entities spawning and immediately despawning, etc.
- Presentation is client-only. Servers don’t participate because they are generally headless computers living in Big Bezos warehouses, so “presenting” anything to the warehouse employees would be expensive and wasteful.
Input and Output State
Input
Output
Determinism
From Wikipedia: A deterministic algorithm is an algorithm that, given a particular input, will always produce the same output.
Applying this definition to Borger:
- The
simulation_loopthat you write, as a whole, is assumed to be a deterministic algorithm by the underlying engine that calls it. This is an invariant you must uphold. - The “input” of the algorithm (not be confused with input state) is the initial state of the
GameContextobject whensimulation_loopfirst begins. - The “output” of the algorithm (again, not to be confused with output state) is the mutated state of the
GameContextobject whensimulation_loopfinishes running.
1. INPUT: GameContext starts out in some kinda way
↓
2. ALGORITHM: simulation_loop does stuff to GameContext
↓
3. OUTPUT: GameContext is different now
The same input should always produce the same output.
While this determinism requirement may sound scary and complicated at first, in practice, it simply means avoiding certain patterns that produce unpredictable results. These are the main 3 offenders you’re most likely to run into:
-
Borger’s API is the only reliable source of truth for understanding when a tick has occurred, because the system clock continues to move forward even when Borger rolls back.
- ❌
std::time - ❌
web_timecrate - ❌
chronocrate - ✅
TickInfo::id()
- ❌
-
If using the
randcrate (or similar) for random number generation, always use a seed derived from game state:-
❌
#![allow(unused)] fn main() { let mut rng = thread_rng(); let n: f32 = rng.gen_range(0.0..10.0); //ironically thread_rng() is TOO random, and returns something //different every time, even if the tick ID hasn't changed } -
✅
#![allow(unused)] fn main() { let mut rng = SmallRng::seed_from_u64(ctx.tick.id() ^ 2468); let n: f32 = rng.gen_range(0.0..10.0); //2468 can be replaced with any random constant, or even a game //state variable, in order to avoid getting the same result from //every SmallRNG instance in the same tick }
-
-
If using Rust’s built-in
HashMaporHashSet, keep in mind that the iteration order is randomized, so deterministic code can’t rely on them to iterate in any certain order. You can still use them and their iterators, but whatever value you’re attempting to derive from them must still be deterministic. For example:- ✅ You could safely calculate the sum of a
HashSet<i32>by iterating it, because integer addition is commutative. - ❌ On the other hand,
hash_set.iter().next()is bad because it essentially returns a random value. - ✅
BTreeMapandBTreeSetDO have deterministic iteration order, but have different performance characteristics.
- ✅ You could safely calculate the sum of a
Historically, floating-point arithmetic (working with fractional, non-integer numbers) has caused a lot of non-deterministic grief for multiplayer games. For example, f32::consts::PI.sin() might return a slightly different number depending on whether it runs on an ARM or Intel CPU, meaning players on the same server who have different hardware don’t see exactly the same thing. With Borger, this is no longer an issue, because the same WASM binary runs on all CPU’s. The official spec guarantees cross-platform determinism for all situations that a game would care about.
Also note that determinism comes in varying degrees of strictness, depending on the trade-off used.
Rollback
At its core, Borger is a rollback framework. This is the idea that an older simulation tick ID can rerun, when information pertaining to the past finally arrives over the slow internet connection.
Let’s say the server is chugging along as usual:
Tick ID 100
↓
Tick ID 101
↓
Tick ID 101
↓
etc.