RUNTIME · GLOBAL CACHE AND RESET RECOVERY

How to Build a Safe Global Cache in Screeps

Use the global object as a disposable runtime cache, rebuild after global resets, version and expire entries, cache IDs and derived data instead of live game objects, and return cloned values to prevent accidental mutation.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — global scope can persist across ticks but may reset; Memory remains the persistent application store · Persistence boundary: Global cache is disposable acceleration, never the only source of truth

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — global scope can persist across ticks but may reset; Memory remains the persistent application store
Persistence boundary
Global cache is disposable acceleration, never the only source of truth
Object boundary
Live Room, Creep, Structure and Source objects are not cached across ticks; IDs are resolved again
JavaScript syntax
Passed
Offline cache review
Passed — reset, hit, miss, version mismatch, TTL, clone isolation and bounded eviction states
Screeps Console test
Pending
Live global reset, CPU and multi-tick invalidation test
Pending
Last verified
July 25, 2026

Quick answer

The Screeps global scope can preserve JavaScript values across multiple ticks while the same runtime context remains alive, but it can reset without notice. Use global only as a disposable performance cache. Every entry must be rebuildable from persistent state or current game objects, and callers must remain correct when the cache is empty.

What global lifetime actually guarantees

Top-level module code and properties assigned to global belong to the current global execution context. They can survive from one tick to the next, which makes them useful for parsed constants, lookup tables, route metadata, or static CostMatrix layers. The runtime may create a new global context, removing all of them.

global.runtimeBoot ??= {
  bootTick: Game.time,
  bootCount: 0
};

global.runtimeBoot.bootCount += 1;

console.log({
  bootTick: global.runtimeBoot.bootTick,
  currentTick: Game.time,
  loopExecutionsInThisGlobal:
    global.runtimeBoot.bootCount
});

This detects the current global lifetime. It does not identify why the previous context ended and should not be used as durable history.

Cache is not the source of truth

DataDurable sourcePossible global cache
Role configurationCode or MemoryValidated lookup map
Room route policyMemory or codeCompiled room-cost table
Source assignmentsMemory IDsDerived assignment index
Static layout matrixCurrent room state plus versionCostMatrix instance
Current Creep objectGame.creepsNever reuse across ticks

Correct code handles a cache miss by rebuilding, using a slower path, or returning an explicit unavailable status. It does not assume the cache must exist because it existed on the previous tick.

Do not cache live game objects

function cacheTargetId(key, target) {
  global.targetIdCache ??= new Map();

  if (!target?.id) {
    global.targetIdCache.delete(key);
    return false;
  }

  global.targetIdCache.set(key, target.id);
  return true;
}

function resolveCachedTarget(key) {
  const id = global.targetIdCache?.get(key);

  if (typeof id !== 'string') {
    return null;
  }

  return Game.getObjectById(id);
}

Room, Creep, Structure, Source, Mineral, Flag-position wrappers, and other live objects represent the current tick. Keep an ID or JSON-compatible description when you need a stable reference, then resolve the live object again.

Use versioned entries and TTL

function isCacheEntryFresh(entry, options) {
  if (!entry || entry.version !== options.version) {
    return false;
  }

  if (!Number.isInteger(options.maxAge)) {
    return true;
  }

  return Game.time - entry.createdAt
    <= options.maxAge;
}

A version handles schema or policy changes. A TTL handles time-based freshness. Neither automatically detects structure changes, ownership changes, destroyed objects, or modified Memory configuration; those need explicit invalidation inputs.

Return cloned values

function cloneCacheValue(value) {
  if (typeof structuredClone === 'function') {
    return structuredClone(value);
  }

  return value === undefined
    ? undefined
    : JSON.parse(JSON.stringify(value));
}

Clone isolation is appropriate for JSON-compatible data. It is not a way to persist live game objects, functions, CostMatrix instances, Maps, Sets, or class prototypes. Those need type-specific handling.

Complete versioned global cache

State impact: this helper writes only to global. A reset removes the cache. The builder must be safe to run again.

function getRuntimeCache() {
  global.runtimeCache ??= new Map();
  return global.runtimeCache;
}

function cloneJsonValue(value) {
  return value === undefined
    ? undefined
    : JSON.parse(JSON.stringify(value));
}

function getCachedValue(key, options, builder) {
  if (
    typeof key !== 'string'
    || !Number.isInteger(options?.version)
    || options.version < 0
    || typeof builder !== 'function'
  ) {
    return {
      status: 'arguments-invalid',
      value: null,
      cacheHit: false
    };
  }

  const cache = getRuntimeCache();
  const entry = cache.get(key);
  const maxAgeValid =
    !Number.isInteger(options.maxAge)
    || (
      options.maxAge >= 0
      && Game.time - entry?.createdAt
        <= options.maxAge
    );
  const fresh = Boolean(
    entry
    && entry.version === options.version
    && maxAgeValid
  );

  if (fresh) {
    entry.lastAccessedAt = Game.time;
    entry.hits += 1;

    return {
      status: 'cache-hit',
      value: cloneJsonValue(entry.value),
      cacheHit: true
    };
  }

  let built;
  try {
    built = builder();
  } catch (error) {
    return {
      status: 'builder-failed',
      value: null,
      cacheHit: false,
      error: error instanceof Error
        ? error.message
        : String(error)
    };
  }

  const stored = cloneJsonValue(built);
  cache.set(key, {
    version: options.version,
    createdAt: Game.time,
    lastAccessedAt: Game.time,
    hits: 0,
    value: stored
  });

  return {
    status: entry
      ? 'cache-rebuilt'
      : 'cache-created',
    value: cloneJsonValue(stored),
    cacheHit: false
  };
}

The cache is correct only when the builder can reconstruct the value after a reset and the version or invalidation inputs change whenever the derived result becomes stale.

Cache IDs and resolve objects each tick

A common safe pattern is to cache a sorted list of object IDs, then resolve and filter them in the current tick.

function getSourceIds(room) {
  const result = getCachedValue(
    'source-ids:' + room.name,
    {
      version: 1,
      maxAge: 1000
    },
    () => room.find(FIND_SOURCES)
      .map(source => source.id)
      .sort()
  );

  return Array.isArray(result.value)
    ? result.value
    : [];
}

function resolveSourceIds(ids) {
  return ids
    .map(id => Game.getObjectById(id))
    .filter(source => source !== null);
}

If the room is not visible, the builder cannot perform room.find(). Callers should use an existing cached ID list only when that stale-data policy is acceptable, or return a visibility-required status.

Invalidate from explicit state versions

function getLayoutVersion(roomName) {
  const value = Memory.layoutVersion?.[roomName];
  return Number.isInteger(value) && value >= 0
    ? value
    : 0;
}

function getStaticLayoutKey(roomName) {
  return [
    'static-layout',
    roomName,
    getLayoutVersion(roomName)
  ].join(':');
}

Increment a persistent version when your code changes a layout contract or confirms a relevant state change. Do not increment every tick, or the cache will never hit. Do not omit it when structure layout affects the cached result.

Keep the cache bounded

function evictRuntimeCache(maxEntries = 200) {
  const cache = getRuntimeCache();

  if (
    !Number.isInteger(maxEntries)
    || maxEntries < 0
    || cache.size <= maxEntries
  ) {
    return 0;
  }

  const entries = [...cache.entries()].sort(
    (a, b) =>
      a[1].lastAccessedAt - b[1].lastAccessedAt
      || a[0].localeCompare(b[0])
  );
  const removeCount = cache.size - maxEntries;

  for (let index = 0; index < removeCount; index += 1) {
    cache.delete(entries[index][0]);
  }

  return removeCount;
}

The limit of 200 is example policy. Measure heap pressure, rebuild cost, key count, and access patterns before choosing a production bound.

Measure warm and reset ticks separately

function describeCacheRuntime() {
  global.cacheBootTick ??= Game.time;
  global.runtimeCache ??= new Map();

  return {
    bootTick: global.cacheBootTick,
    globalAge: Game.time - global.cacheBootTick,
    entries: global.runtimeCache.size,
    cpuUsedSoFar: Game.cpu.getUsed()
  };
}

A reset tick may rebuild several entries and consume more CPU than a warm tick. Profile both. A cache that is fast only after a very expensive uncontrolled rebuild can still cause bucket loss or hard-limit failures.

Debugging checklist

  • Verify every cached value has a rebuild path.
  • Keep durable state outside global.
  • Do not cache live game objects across ticks.
  • Use IDs or primitive derived data.
  • Version schema and policy changes.
  • Add TTL only when time-based freshness is meaningful.
  • Return cloned mutable values.
  • Bound entry count and key growth.
  • Measure cache hits, misses, rebuilds, and reset ticks separately.
  • Keep callers correct when the cache is empty.

Scope and next steps

This guide does not implement a heap profiler, cross-shard cache, automatic structure-event invalidation, shared module loader internals, WeakMap policies, or live reset benchmarks. Continue with RawMemory Segments when data must persist across global resets without occupying ordinary Memory.

Frequently asked questions

Is global faster than Memory?

It can avoid repeated parsing or reconstruction during one global lifetime, but performance must be measured in your workload.

Can a cache survive a code deployment?

Do not rely on it. Treat deployments and runtime resets as cache-loss events.

Should I cache every lookup?

No. Cache work that is expensive enough, reused enough, and safe to invalidate.

Can a global cache replace Segments?

No. Global is disposable. Segments are persistent strings with next-tick activation semantics.

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.