Use this guide when
Use this page when a value is expensive to rebuild but safe to lose: a sorted ID list, compiled configuration, static room index, or other derived data. Durable decisions still belong in Memory or Segments. A global cache is useful only when every caller remains correct after the cache disappears.
The runtime may keep global data, but it owes you no lifetime
global.runtimeInfo ??= {
startedAt: Game.time,
loopCalls: 0
};
global.runtimeInfo.loopCalls += 1;
const runtimeSnapshot = {
startedAt: global.runtimeInfo.startedAt,
currentTick: Game.time,
loopCalls: global.runtimeInfo.loopCalls
};
This can show that the current runtime survived several ticks. It cannot identify why an earlier runtime ended, and it is not durable history. Code deployment, server behavior, or another global reset can remove the object before the next tick.
Define the rebuild and invalidation contract first
| Question | Required answer |
|---|---|
| What is the source of truth? | Current game state, code, Memory, or an active Segment |
| How is the value rebuilt? | A deterministic builder or a safe slower path |
| When is it stale? | Explicit version change, age limit, or confirmed state change |
| What happens after reset? | The next caller rebuilds or returns a clear unavailable status |
“It was present last tick” is not an invalidation policy.
Cache one concrete derived value
function getRoomIndexCache() {
global.roomIndexCache ??= new Map();
return global.roomIndexCache;
}
function getVisibleRoomIndex(
room,
version,
maxAge = 100
) {
if (
!room
|| !Number.isInteger(version)
|| !Number.isInteger(maxAge)
|| maxAge < 0
) {
return {
status: 'arguments-invalid',
sourceIds: [],
structureIds: []
};
}
const cache = getRoomIndexCache();
const entry = cache.get(room.name);
const fresh = Boolean(
entry
&& entry.version === version
&& Game.time - entry.builtAt <= maxAge
);
if (fresh) {
return {
status: 'cache-hit',
sourceIds: [...entry.sourceIds],
structureIds: [...entry.structureIds]
};
}
const next = {
version,
builtAt: Game.time,
sourceIds: room.find(FIND_SOURCES)
.map(source => source.id)
.sort(),
structureIds: room.find(FIND_STRUCTURES)
.map(structure => structure.id)
.sort()
};
cache.set(room.name, next);
return {
status: entry
? 'cache-rebuilt'
: 'cache-created',
sourceIds: [...next.sourceIds],
structureIds: [...next.structureIds]
};
}
The builder requires current room visibility. The returned arrays are copies, so a caller cannot reorder or delete the cached arrays accidentally. The example version must change when your cache contract changes; the age limit is a fallback, not proof that the room layout stayed unchanged.
Cache IDs, then resolve current objects
function resolveCurrentObjects(ids) {
if (!Array.isArray(ids)) {
return [];
}
return ids
.map(id => Game.getObjectById(id))
.filter(object => object !== null);
}
Do not keep a Creep, Room, Structure, Source, or other live game object for reuse on later ticks. Cache its ID or derived primitive data and recover the current object when needed. A null result can mean the object is gone or currently outside vision; do not erase remote plans until your visibility policy can distinguish those cases.
Invalidate from an explicit state version
function getRoomLayoutVersion(roomName) {
const value = Memory.roomLayoutVersion?.[roomName];
return Number.isInteger(value) && value >= 0
? value
: 0;
}
function bumpRoomLayoutVersion(roomName) {
Memory.roomLayoutVersion ??= {};
Memory.roomLayoutVersion[roomName] =
getRoomLayoutVersion(roomName) + 1;
return Memory.roomLayoutVersion[roomName];
}
Increment the version after your code confirms a layout change that affects the derived value. Do not increment it every tick. A reset removes the cache but not the persistent version, so the next visible-room read can rebuild under the same contract.
Failure boundaries
- If the room is not visible and no safe stale result exists, return
visibility-requiredinstead of fabricating current data. - If the builder throws, keep the old entry untouched or return a clear build failure.
- If keys can grow without bound, add deterministic eviction.
- If callers need mutable data, return a copy or a type-specific clone.
- If rebuilding several entries can exceed the current CPU budget, stagger warm-up work.
How to verify across ticks
Record the runtime start tick, cache status, cache version, and builder inputs. Compare at least one cache hit with one rebuild. A separate real global-reset observation is required before claiming reset recovery. This revision does not contain measured CPU savings or a fabricated reset transcript.
Choose another guide when
Use the Memory guide when the value must survive a reset. Use the Segments guide for larger persistent strings. Use the CostMatrix guide when the cached value is specifically a static navigation layer.
Make the cache key describe the inputs that change the answer
A cache can be internally consistent and still return the wrong answer when its key omits part of the input. A room name is enough only for a value that depends on that room alone. Add explicit schema, configuration, policy, or layout revisions when those inputs change the derived result.
function makeRoomCacheKey(input) {
if (
typeof input.roomName !== 'string'
|| !Number.isInteger(input.schemaVersion)
|| !Number.isInteger(input.configRevision)
) {
return null;
}
return [
input.roomName,
'schema=' + input.schemaVersion,
'config=' + input.configRevision
].join('|');
}
The revision numbers are application data, not Screeps constants. Change them when the represented inputs change. For structure layouts, an explicit layout revision or event-driven invalidation can be more accurate than treating an arbitrary TTL as the only source of truth.
A missing cache does not prove why the runtime state disappeared
Code must handle an empty module/global cache after a fresh runtime context, code reload, explicit heap clearing, or another event that recreates JavaScript state. From global.exampleCache === undefined alone, do not invent the cause. The observable fact is a cache miss; the engineering response is to rebuild from current or durable inputs.
function getOrBuild(key, builder) {
global.exampleCache ??= new Map();
if (global.exampleCache.has(key)) {
return {
status: 'cache-hit',
value: global.exampleCache.get(key)
};
}
const value = builder();
global.exampleCache.set(key, value);
return {
status: 'cache-built',
value
};
}
If correctness depends on data that cannot be reconstructed after a miss, that state should not live only in disposable global cache.
Measure cold rebuilds and warm hits separately
Do not publish a CPU improvement because one cached call looked cheaper. Measure the same boundary repeatedly and label whether each sample rebuilt or hit the cache. Input size, room visibility, bucket state, code path, and cache state can all change the comparison.
function measureCacheCall(label, read) {
const before = Game.cpu.getUsed();
const outcome = read();
const used = Game.cpu.getUsed() - before;
return {
tick: Game.time,
label,
cacheStatus: outcome.status,
used,
bucket: Game.cpu.bucket,
limit: Game.cpu.limit
};
}
This records one section delta; it does not prove a long-run CPU benefit. Keep cold rebuilds and warm hits in separate groups, collect multiple comparable server samples, and leave the performance conclusion Pending until real-shard data exists. Use the CPU profiling guide for measurement boundaries.