OBSERVABILITY · RATE-LIMITED ALERTS

How to Send Reliable Alerts with Game.notify()

Use state transitions, per-room Memory, repeat intervals, message truncation, priorities, and a 20-notification queue without confusing groupInterval minutes with game ticks or claiming external delivery success.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — 1000-character message, 20 scheduled notifications per tick, groupInterval minutes, Simulation unavailable · Delivery boundary: The API call condition can be verified; external email or profile delivery is not proven by a return code

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — 1000-character message, 20 scheduled notifications per tick, groupInterval minutes, Simulation unavailable
Delivery boundary
The API call condition can be verified; external email or profile delivery is not proven by a return code
Rate boundary
Per-alert state and a central queue are separate from notification grouping
JavaScript syntax
Passed
Offline alert review
Passed — entry, active, repeat, recovery, re-entry, truncation, priority and expiration states
Screeps Console test
Pending
Live notification queue and external delivery test
Pending
Last verified
July 25, 2026

Quick answer

Send a notification when a condition enters risk, optionally repeat it after a tick-based interval, and re-arm it after recovery. Store state per alert key, normalize every message to 1000 characters or fewer, and submit at most 20 queued notifications per tick. groupInterval is measured in minutes, not ticks.

Official notification limits

RuleOfficial boundaryApplication requirement
Message lengthMaximum 1000 charactersTruncate before submission
Scheduled per tickMaximum 20Use one priority queue
groupIntervalMinutesDo not reuse a tick count
Default grouping0Scheduled immediately
Simulation RoomUnavailableLive delivery requires another environment

The API reference does not document a delivery receipt that proves an email or profile notification arrived.

Use three independent rate controls

LayerUnitPurpose
State transitionBusiness stateNotify immediately when normal becomes risky
Repeat intervalGame ticksAllow a long-running risk to remind again
groupIntervalReal minutesLet the notification system group messages

Grouping does not prevent your code from calling the API every tick. The state machine must control when a call is allowed.

Evaluate alert state with a pure function

function evaluateAlert(input) {
  const previous = input.previousState || {
    active: false,
    lastSubmittedTick: null
  };
  const isRisk = input.isRisk === true;

  if (!isRisk) {
    return {
      shouldQueue: false,
      reason: previous.active ? 'recovered' : 'normal',
      nextState: {
        active: false,
        lastSubmittedTick: previous.lastSubmittedTick
      }
    };
  }

  const enteredRisk = previous.active !== true;
  const lastTick = Number.isInteger(
    previous.lastSubmittedTick
  )
    ? previous.lastSubmittedTick
    : null;
  const repeatDue =
    lastTick !== null
    && Number.isInteger(input.repeatAfterTicks)
    && input.repeatAfterTicks >= 0
    && input.currentTick - lastTick
      >= input.repeatAfterTicks;
  const shouldQueue = enteredRisk || repeatDue;

  return {
    shouldQueue,
    reason: enteredRisk
      ? 'entered-risk'
      : repeatDue
        ? 'repeat-due'
        : 'risk-active',
    nextState: {
      active: true,
      lastSubmittedTick: shouldQueue
        ? input.currentTick
        : lastTick
    }
  };
}

This pure function supports offline tests without calling Game.notify().

Enforce the 1000-character limit

function normalizeNotificationMessage(message) {
  const text = String(message);

  if (text.length <= 1000) {
    return text;
  }

  return text.slice(0, 997) + '...';
}

Do not append arbitrary serialized objects or full stack traces without a size budget.

Keep state per alert key

function getAlertState(key) {
  Memory.alertStates ??= {};
  return Memory.alertStates[key] || null;
}

function setAlertState(key, state) {
  Memory.alertStates ??= {};
  Memory.alertStates[key] = state;
}

A key such as controller-risk:W1N1 isolates rooms. Similar keys can isolate Spawns, Terminals, remote operations, or CPU conditions.

Complete Controller risk alert

State impact: this code writes alert state and queues an alert in Memory. It does not submit the external notification until the queue processor runs.

const CONTROLLER_ALERT_THRESHOLD = 5000;
const CONTROLLER_REPEAT_TICKS = 5000;

function evaluateAlert(input) {
  const previous = input.previousState || {
    active: false,
    lastSubmittedTick: null
  };

  if (input.isRisk !== true) {
    return {
      shouldQueue: false,
      reason: previous.active ? 'recovered' : 'normal',
      nextState: {
        active: false,
        lastSubmittedTick: previous.lastSubmittedTick
      }
    };
  }

  const enteredRisk = previous.active !== true;
  const lastTick = Number.isInteger(
    previous.lastSubmittedTick
  )
    ? previous.lastSubmittedTick
    : null;
  const repeatDue =
    lastTick !== null
    && input.currentTick - lastTick
      >= input.repeatAfterTicks;
  const shouldQueue = enteredRisk || repeatDue;

  return {
    shouldQueue,
    reason: enteredRisk
      ? 'entered-risk'
      : repeatDue
        ? 'repeat-due'
        : 'risk-active',
    nextState: {
      active: true,
      lastSubmittedTick: shouldQueue
        ? input.currentTick
        : lastTick
    }
  };
}

function enqueueNotification(alert) {
  Memory.notificationQueue ??= [];
  Memory.notificationQueue.push(alert);
}

function checkControllerRisk(room) {
  const controller = room.controller;

  if (
    !controller
    || controller.my !== true
    || !Number.isFinite(controller.ticksToDowngrade)
  ) {
    return 'controller-unavailable';
  }

  const key = 'controller-risk:' + room.name;
  const previous = getAlertState(key);
  const decision = evaluateAlert({
    isRisk:
      controller.ticksToDowngrade
      < CONTROLLER_ALERT_THRESHOLD,
    currentTick: Game.time,
    previousState: previous,
    repeatAfterTicks: CONTROLLER_REPEAT_TICKS
  });

  setAlertState(key, decision.nextState);

  if (!decision.shouldQueue) {
    return decision.reason;
  }

  enqueueNotification({
    key,
    createdAt: Game.time,
    expiresAt: Game.time + 1000,
    priority: 100,
    groupMinutes: 60,
    message: normalizeNotificationMessage(
      [
        '[controller-risk]',
        'room=' + room.name,
        'ticksToDowngrade=' +
          controller.ticksToDowngrade,
        'threshold=' + CONTROLLER_ALERT_THRESHOLD,
        'reason=' + decision.reason,
        'gameTick=' + Game.time
      ].join(' ')
    )
  });

  return 'queued';
}

The thresholds are example policy, not official recommendations.

Build a central priority queue

function normalizeNotificationQueue(queue) {
  if (!Array.isArray(queue)) {
    return [];
  }

  const byKey = new Map();

  for (const item of queue) {
    if (
      !item
      || typeof item.key !== 'string'
      || typeof item.message !== 'string'
      || !Number.isFinite(item.priority)
      || !Number.isInteger(item.createdAt)
      || !Number.isInteger(item.expiresAt)
    ) {
      continue;
    }

    if (item.expiresAt < Game.time) {
      continue;
    }

    const previous = byKey.get(item.key);
    if (
      !previous
      || item.priority > previous.priority
      || item.createdAt > previous.createdAt
    ) {
      byKey.set(item.key, item);
    }
  }

  return [...byKey.values()].sort(
    (a, b) =>
      b.priority - a.priority
      || a.createdAt - b.createdAt
      || a.key.localeCompare(b.key)
  );
}

Deduplication prevents multiple modules from submitting the same condition in one tick.

Submit at most 20 notifications

State impact: this function calls Game.notify() up to 20 times and keeps remaining valid items in Memory.

function submitNotificationQueue() {
  const queue = normalizeNotificationQueue(
    Memory.notificationQueue
  );
  const submitted = queue.slice(0, 20);
  const deferred = queue.slice(20);

  for (const item of submitted) {
    Game.notify(
      normalizeNotificationMessage(item.message),
      Number.isFinite(item.groupMinutes)
        ? Math.max(0, item.groupMinutes)
        : 0
    );
  }

  Memory.notificationQueue = deferred;

  return {
    submitted: submitted.length,
    deferred: deferred.length
  };
}

Call this once after every producer has queued alerts. Submission count does not prove external delivery.

Do not claim delivery success

The reliable local evidence is:

  • the condition entered a notification state;
  • the queue accepted a normalized item;
  • the queue processor selected it within the 20-call limit;
  • your code called Game.notify().

Email, profile, client, or account-delivery behavior requires live verification outside the offline state machine.

Recovery should re-arm the alert

When risk ends, set active to false but retain the previous submission tick for history. A later transition into risk becomes a new entered-risk event and can notify immediately.

const recovered = evaluateAlert({
  isRisk: false,
  currentTick: 2000,
  previousState: {
    active: true,
    lastSubmittedTick: 1500
  },
  repeatAfterTicks: 5000
});

Debugging checklist

  • Keep tick intervals separate from grouping minutes.
  • Normalize every message to 1000 characters or fewer.
  • Store state per stable alert key.
  • Notify on entry, due repeats, and re-entry after recovery.
  • Use one central queue.
  • Deduplicate by key.
  • Expire stale queue items.
  • Sort by explicit priority.
  • Submit no more than 20 per tick.
  • Do not claim external delivery without a live test.

Scope and next steps

This guide does not implement account-channel monitoring, webhook delivery, retry receipts, cross-shard queues, Segment-backed alert history, or live email verification. Continue with Room.getEventLog() to create alerts from previous-tick combat events.

Frequently asked questions

Can groupInterval replace Memory cooldowns?

No. It groups notifications in minutes; your business state still needs tick-based control.

Should every module call Game.notify directly?

No. A central queue enforces the account-wide per-tick limit.

Should recovery send a notification?

That is a product decision. The example records recovery only so the next risk entry can alert again.

Can the Simulation validate delivery?

No. The official API says notifications are unavailable there.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.