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
| Data | Durable source | Possible global cache |
|---|---|---|
| Role configuration | Code or Memory | Validated lookup map |
| Room route policy | Memory or code | Compiled room-cost table |
| Source assignments | Memory IDs | Derived assignment index |
| Static layout matrix | Current room state plus version | CostMatrix instance |
| Current Creep object | Game.creeps | Never 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.