Skip to content

Time and scheduled functions

Fireemu has a clock of its own, and every service reads it: server timestamps, document and object times, token expiry, scheduled functions and Security Rules request.time. By default it runs with real time, as production does. Set where it starts, move it, or freeze it, and every service sees the new time.

Unless you pin a start time, the clock follows real time from the moment the daemon started. A move (clock:advance, clock:advanceTo, clock:set or the Runtime page) is an offset on top: the clock runs on from where it lands.

Pinning daemon.clockStart makes a run reproducible, so the clock then stands still until it is moved. Set daemon.clock to choose either way explicitly: "realtime" runs a pinned clock on from its start, and "frozen" stops an unpinned one.

# Configuration schema version: 1.
schemaVersion = 1
[daemon]
# Start the virtual clock at this date-time.
clockStart = "2026-01-31T23:59:00Z"
# Virtual clock behavior: "realtime" or "frozen".
clock = "frozen"

A virtual functions.clock (see Application time in Functions) needs a pinned start and therefore a still clock, and so do the worlds of fireemu/testing, which pin it.

A schedule runs when the clock reaches an occurrence. With the default clock that happens as real time passes. With a frozen clock real time runs nothing, so move the clock from the Runtime page of the Emulator UI, or with the control API (clock:advance, clock:advanceTo, clock:set). Each move makes every occurrence in (previous time, new time] due. Configure the time zone, catch-up and overlap behavior in the scheduler section.

  • scheduler.catchUp decides what runs when a move passes several occurrences: all runs each one (at most scheduler.maxCatchUpRuns per job and move), latest runs the newest, and none runs none.
  • scheduler.overlap (allow, skip, queue, reject, skip-in-flight) decides what happens when a run comes due while the previous one is still running. Under strict the default is skip-in-flight, as production’s Cloud Scheduler behaves; under emulator it is allow.

The Functions page of the UI can also run a scheduled function immediately, or advance the clock to its next occurrence. A manual run does not consume the next occurrence.

  • An occurrence exactly at the new time is included in a move, and one exactly at the previous time is not.
  • With scheduler.catchUp: all, runs beyond scheduler.maxCatchUpRuns per job and clock move stay due and run as room frees. latest and none record one skipped: catch-up summary per job and clock move that drops runs.
  • A rewind never replays: occurrences the schedule already passed are not run again when the clock moves forward over them, and the schedule resumes once the clock passes the last occurrence it ran or still owes (its cursor). After a rewind below that point, runs still owed from a backlog or held for room stay owed but do not come due again until the clock passes it. A session reset or a snapshot restore starts every schedule from the clock’s current time instead, so after a rewind the occurrences between that time and a later one are due again.
  • scheduler.overlap applies to scheduler runs and manual runs alike, except skip-in-flight, which never refuses a manual run. Under skip-in-flight (the strict default) an occurrence is dropped (recorded as skipped: in flight) while a handler of the function that started before the clock move is still executing, as production’s Cloud Scheduler did, and the occurrences of one clock move do not count against each other.
  • A function counts as busy while any of its runs is queued or running, including a run that is waiting to be retried, so under skip and reject a retry still pending can suppress the next occurrence; Fireemu does not distinguish a retry attempt from an overlapping run here. With catchUp: all, one clock move over several occurrences of the same function overlaps itself, so skip keeps the oldest of them and reject counts the rest as rejections.
  • If the Functions event queue is full (4,096 active events or 64 MiB), a scheduled run that comes due stays due and runs once room frees.

A manual run (POST /v1/sessions/{session}/functions/{name}:run) enqueues one run of a scheduled function stamped with the current virtual time. It does not consume the next occurrence, which still comes due and is subject to scheduler.overlap like any run. It answers 400 INVALID_ARGUMENT for an unknown function, a function that is not scheduled, an overlap policy refusal, or a runtime that is shutting down, and 429 RESOURCE_EXHAUSTED when the event queue is full. This is Fireemu’s own control, not a claim about production’s run-now.

On a task-queue function (onTaskDispatched), the same route releases the Cloud Tasks that wait for their scheduleTime: all of them, or the one named by a {"task": "<task id>"} body (404 when no waiting task has that id). See Task queues.

The clock can move backwards (clock:set with "allowBackwards": true, or Allow moving backwards on the Runtime page). A rewind never replays occurrences that already ran.

In the emulator profile, ID tokens stay usable after a rewind, as with the official emulator, which does not check their issue time. Under strict, Authentication (INVALID_ID_TOKEN), Firestore and Storage refuse an ID token issued after the new time until the clock reaches its issue time again. In both profiles an expired token is still refused.

A rewind to before an account was created, or before its tokens were revoked, makes even a new sign-in’s token count as revoked; move the clock forward again or reset the session instead. Conversely, a revocation or password change made after a rewind does not revoke tokens issued at later times on the timeline: the emulator profile accepts them at once, and strict accepts them once the clock reaches their issue time. Move the clock forward past their issue time before revoking, or reset the session.

By default your function code sees real time. A Python codebase always does: it is not moved onto the virtual clock, and a configuration that asks for a virtual date for the Functions runner is refused for it. To make Date.now(), timers and Cloud Tasks follow the virtual clock too, set functions.clock together with a pinned daemon.clockStart:

# Configuration schema version: 1.
schemaVersion = 1
[daemon]
# Start the virtual clock at this date-time.
clockStart = "2026-01-31T23:59:00Z"
[functions]
# Directory of the Functions codebase.
source = "./functions"
[functions.clock]
# Functions date clock: "real" or "virtual".
date = "virtual"
# Functions timer clock: "real" or "virtual".
timers = "real"
# Cloud Tasks clock: "real" or "virtual".
tasks = "virtual"

Each mode accepts real or virtual. Virtual timers require virtual date.

With tasks set to virtual, a Cloud Tasks task enqueued with a scheduleTime becomes due when the virtual clock reaches it, so a test advances the clock instead of waiting. With real, the task waits for the wall clock under strict; the Functions page of the Emulator UI lists waiting tasks and can run them now.

For parallel tests that need their own clock, the fireemu/testing helper starts independent worlds, each pinned to a start time you move by hand. See Test worlds.