NEOLearn

Time, sleep and async

The simulation clock, Date in scripts, await sleep, waiting on several things at once, and the order timers resolve in.

Updated October 17, 2026

On this page

Scripts have one clock: the game's simulation clock. It moves forward with the game's fixed timestep, never with the wall clock, which is why a script behaves the same on every run and every device.

The simulation clock#

WhereWhat you get
this.time (Python: self.time)Simulation seconds, as a float.
dt in a tick handlerMilliseconds since the previous tick.
Date.now(), new Date() (JavaScript)Simulation time as a date, counted from 2026-01-01T00:00:00Z.
sleep(seconds)A promise (Python: an awaitable) that resolves after that much simulation time.

The clock starts at 0 when the game starts. A script loaded into a game that's already running starts at the game's current time, not at 0. JavaScript dates are always UTC: new Date().getTimezoneOffset() is 0 on every machine. Python scripts have no date type; use self.time.

Because time is simulated, there is no way to measure real time from a script, and nothing in a script waits on the real world.

sleep#

await sleep(seconds) pauses an async handler and resumes it on a later tick:

JavaScript
import { World, on, sleep } from "neo";

export default class Countdown extends World {
  state = { said: [] };

  @on("round_start")
  async start() {
    for (const n of [3, 2, 1]) {
      this.state.said = [...this.state.said, n];
      await sleep(1.0);
    }
    this.emit("round_go", { at: this.time });
  }
}

How sleep resolves:

  • The wait is rounded to whole milliseconds.
  • It resolves at the first tick whose time is at or past the target. With 16 ms ticks, sleep(0.1) resumes at 112 ms, not at exactly 100 ms.
  • Timers resolve at the start of a tick, before that tick's tick handlers.
  • sleep(0) resumes on the next tick, never within the same call.

An async handler runs normally until its first await, within the tick that started it. Promise callbacks (.then, await on an already-resolved value) finish before the call ends.

Waiting on several things#

JavaScript uses all from neo (the same as Promise.all); Python uses gather. Results come back in argument order, whichever finished first:

JavaScript
import { World, on, sleep, all } from "neo";

export default class Race extends World {
  state = { got: null };

  @on("go")
  async go() {
    this.state.got = await all([
      sleep(0.05).then(() => "late"),
      Promise.resolve("now"),
      sleep(0.016).then(() => "soon"),
    ]);
    // ["late", "now", "soon"]
  }
}

gather takes the awaitables as separate arguments and returns a tuple; convert it with list(...) before storing it, or store it as it is (a tuple is stored as a list).

Timer order#

When several timers are due on the same tick, they resolve in order of their target time, and timers with the same target resolve in the order they were created. Handlers run in a fixed order too (see Events and handlers). Nothing depends on how fast a machine is.

Pausing and reloading#

  • Paused scripts don't run: no events, no ticks. When you resume a script, its clock catches up and any timers that came due while it was paused resolve on that tick. Events sent while it was paused are not replayed.
  • Hot reload keeps the script's clock and state but drops pending timers. A handler waiting in await sleep(...) in the old version never resumes. See Script state.

Errors in async handlers#

If an async handler throws after an await, or a promise it awaits rejects, that is a fault for the handler, the same as an error in a normal handler. The rest of the world keeps running.

See also#