Skip to content

Sandbox and performance

Every Rally game runs code written by its creator, on Rally’s servers and in players’ browsers. To keep players, Rally and other games safe, scripts run in a sandbox: a closed space where they can use the game API and nothing else. This page explains what that means for your scripts, the limits they run under, and how to make a game that stays fast.

Inside the sandbox, scripts have:

  • the whole game API from @rally/sdk;
  • standard JavaScript: Math, JSON, Date, Map, Set, promises, classes, typed arrays and so on;
  • print and warn for output.

They don’t have:

  • The network. Scripts can’t make web requests or open connections. HttpService only creates GUIDs and reads and writes JSON. Data that lasts goes through data stores.
  • Files, or anything else on the machine they run on.
  • Browser timers such as setTimeout and setInterval. Use task.wait, task.delay and RunService events, which run on game time. See Tasks and timing.
  • Other games. Each game server runs one game, sealed off from every other game.

Each running game server runs your Scripts in a sandbox of its own. On top of the rules above, WebAssembly, SharedArrayBuffer and Atomics aren’t available there.

LocalScripts run inside Rally’s game player, in a locked-down space in the player’s browser. They can’t reach the page around the game, the player’s Rally account, cookies or browser storage.

There, JavaScript’s built-in objects are frozen: a script can’t add methods to Array.prototype or change other built-ins, and trying throws an error. Write helper functions instead.

Play in Studio runs your whole game, Scripts included, in this same locked-down player in your browser.

What Limit When it’s exceeded
Memory 256 MB per game server. The game stops.
One step of the game 1 second. A step is one tick of the game, or one batch of messages from players. The game stops.
Busy time 90% of a processor core, averaged over 5 seconds. The game stops.
Loading the world when the server starts 5 seconds. The server doesn’t start.
Running every script’s top-level code at startup 5 seconds, for all scripts together. The server doesn’t start.
Output 200 lines a second. Extra lines are dropped, and Output says how many: 12 log lines were dropped (over 200/s).

The game server and its players also have limits on players per server, remote calls, message sizes and data store requests. Every number is in Limits.

A game that runs too long or runs out of memory is stopped for good, so it can’t slow down anyone else:

  • Every player on that server is removed and sees You were removed from the game with the reason: The game server stopped: a script ran too long., The game server stopped: it ran out of memory., or The game server stopped because of an error.
  • In a test on a real game server, Output also shows what was running when it stopped, so you can find the loop.
  • Anything the game hadn’t saved is gone. Players who press Play again get a new server.

A game that breaks a limit while starting never starts. Test on a real game server fails with The game server stopped while starting, and players can’t join a published version with the same problem. Look for top-level code that loops forever or does a lot of work at once.

Ordinary script errors aren’t fatal. An error in an event handler, a task callback or a remote handler shows up in Output, and everything else keeps running, including the other handlers of the same event. An error in a script’s top-level code stops the rest of that script, but other scripts still run. See Debugging.

If a LocalScript freezes the player’s browser, for example with a loop that never ends, Rally shows This game isn’t responding after 5 seconds, and the player can leave the game or keep waiting. In Studio, select Stop.

A game server runs your game in 30 steps a second, about 33 milliseconds each. In every step it runs the tasks that are due, then RunService.stepped, then physics, then RunService.heartbeat. Everything your Scripts do in a step shares that time.

If steps take longer than that, the game falls behind and runs slower. If a single step takes a whole second, the game is stopped. So:

  • Keep heartbeat handlers light. They run 30 times a second.
  • Do slow work less often. Most game logic doesn’t need every step. Think about AI, checks and timers 10 times a second or less.
  • Split big jobs across steps. Building a large map or searching a big area can be done a piece at a time, with await task.wait() between pieces.
  • Don’t search the tree every step. getDescendants() and findFirstChild(name, true) walk the tree. Look things up once, at the start, and keep the results.

This script updates its robots 10 times a second instead of 30, and passes the real time since the last update so movement stays the same speed:

ServerScriptService/Robots.server.ts
import { BasePart, Folder, getChild, Players, RunService, Vector3, workspace } from '@rally/sdk';
const robots = getChild(workspace, 'Robots', Folder)
.getChildren()
.filter((r): r is BasePart => r instanceof BasePart); // looked up once, not every step
const SPEED = 8; // studs per second
let steps = 0;
let elapsed = 0;
RunService.heartbeat.connect((dt) => {
elapsed += dt;
if (++steps % 3 !== 0) return; // every third step: 10 times a second
const players = Players.getPlayers();
for (const robot of robots) {
// Head toward the nearest living character.
let target: Vector3 | null = null;
let best = Infinity;
for (const player of players) {
const character = player.character;
if (!character || character.health <= 0) continue;
const d = character.position.distanceTo(robot.position);
if (d < best) {
best = d;
target = character.position;
}
}
if (!target || best < 3) continue;
const toward = target.sub(robot.position).withY(0).unit;
robot.position = robot.position.add(toward.mul(SPEED * elapsed));
}
elapsed = 0;
});

Players’ devices smooth the robots between updates, so 10 updates a second still look fluid.

And this script lays out a 40 × 40 checkerboard one row per step, so no step gets long, and the startup time limit doesn’t come into it. A generated maze or level builds the same way:

ServerScriptService/BuildBoard.server.ts
import { Color3, Folder, Part, task, Vector3, workspace } from '@rally/sdk';
task.spawn(async () => {
const board = new Folder();
board.name = 'Board';
board.parent = workspace;
for (let row = 0; row < 40; row++) {
for (let col = 0; col < 40; col++) {
const tile = new Part();
tile.size = new Vector3(4, 1, 4);
tile.position = new Vector3(col * 4 - 80, 0, row * 4 - 80);
tile.color = (row + col) % 2 === 0 ? Color3.fromHex('#e8ecf3') : Color3.fromHex('#c9d1de');
tile.canTouch = false;
tile.parent = board;
}
await task.wait(); // let the game run a step before the next row
}
});
  • Clean up per-player state. Every Map keyed by player needs a matching delete in Players.playerRemoving.
  • Destroy what you’re done with. Projectiles, effects and old rounds’ objects. Destroying an object also disconnects its events.
  • Don’t keep growing lists of history, positions or messages without a cap.

Every change to an object players can see is sent to every player who can see it, in batches 20 times a second. When a player joins, they receive everything they can see.

  • Move less on the server. Each moving part sends its new position every update, to every player. Animate decoration in LocalScripts, and update moving NPCs at a lower rate, as above.
  • Only write real changes. Setting a property or attribute to the value it already has sends nothing. A countdown that changes once a second costs far less than one that changes every step.
  • Batch remote events. One event with a list beats a hundred events, and each player can make at most 60 remote calls a second.
  • Keep templates in ServerStorage. Objects there aren’t sent to players until you clone them into a place players can see.
  • Edit terrain sparingly. Every terrain change sends the whole height map to every player.

See Multiplayer for how replication works.

Players’ devices draw the world and run your LocalScripts. Phones and older laptops have far less to spare than your computer.

  • Fewer, bigger parts. One long floor draws faster than a hundred tiles. A published game’s world can hold up to 50,000 objects, but most good games use far fewer.
  • A steady set of lights. Each PointLight adds drawing work, and adding or removing lights while the game runs can cause a stutter. Turn lights on and off with enabled instead.
  • Shadows cost drawing time in 3D. Turn off castShadow on small or far-away parts, or shadows on Lighting if you don’t need them.
  • Light renderStepped handlers. They run before every frame the player sees.
  • Clean up effect parts as soon as they’re done.

Test on the slowest device you expect players to use, not only on your own.

  • Test with Test on a real game server, with several players, so the server’s limits and checks apply.
  • Watch Output for warnings: dropped remote calls, dropped log lines, assets that didn’t load.
  • Leave a game running for a few minutes. Slow memory growth only shows up over time.

For every limit in one place, see Limits.