Use this guide when
Use this page when data must persist outside ordinary Memory and your code can follow a request-now, read-later lifecycle. The important boundary is not serialization syntax; it is activation timing. Calling RawMemory.setActiveSegments() does not make those Segments readable in the same tick.
A safe manager therefore separates three concerns: which IDs should be active next tick, what data is actually available this tick, and whether a replacement string is safe to write.
The boundaries that shape the design
| Boundary | Engineering consequence |
|---|---|
Segment IDs are 0–99 | Reject negative, fractional, and greater-than-99 IDs before scheduling. |
| Up to 10 Segments may be active | Rank requests and report deferred IDs instead of silently dropping them. |
| Activation applies on the next tick | Do not treat a same-tick request as proof that RawMemory.segments[id] is available. |
Later setActiveSegments() calls replace earlier plans | Use one coordinator and one final activation call. |
| Docs describe a 100 KB Segment limit | The current official driver enforces string.length <= 100 * 1024; do not substitute a UTF-8 byte counter and call it the server rule. |
The last row is deliberately precise. The API documentation uses “100 KB,” while the current official screeps/driver implementation checks JavaScript string .length. A custom or future driver can differ, so code that targets a private server should verify that server's implementation instead of assuming this implementation detail is universal.
Request on one tick, read on a later tick
tick N
feature modules request Segment 7
one manager calls RawMemory.setActiveSegments([7])
tick N + 1
RawMemory.segments[7] may be available
read and validate the current string
write a replacement only from a state you understand
schedule the next active set
undefined and '' are not equivalent. undefined means the Segment is not available to the current script. An empty string means the active Segment currently contains no data. Collapsing both into “empty” can turn an activation bug into destructive overwrite.
Collect requests and finalize activation exactly once
function getSegmentCoordinator() {
global.segmentCoordinator ??= {
requested: new Map(),
finalizedAt: null,
plan: null
};
return global.segmentCoordinator;
}
function requestSegment(id, priority = 0) {
const coordinator = getSegmentCoordinator();
if (coordinator.finalizedAt === Game.time) {
return {
accepted: false,
status: 'activation-already-finalized'
};
}
if (
!Number.isInteger(id)
|| id < 0
|| id > 99
|| !Number.isFinite(priority)
) {
return {
accepted: false,
status: 'segment-request-invalid'
};
}
const previous = coordinator.requested.get(id);
coordinator.requested.set(
id,
previous === undefined
? priority
: Math.max(previous, priority)
);
return { accepted: true, status: 'segment-requested' };
}
function finalizeSegmentRequests() {
const coordinator = getSegmentCoordinator();
if (coordinator.finalizedAt === Game.time) {
return {
status: 'activation-already-finalized',
...coordinator.plan
};
}
const ranked = [...coordinator.requested.entries()]
.sort((a, b) => b[1] - a[1] || a[0] - b[0]);
const activeNextTick = ranked
.slice(0, 10)
.map(([id]) => id);
const deferred = ranked
.slice(10)
.map(([id]) => id);
RawMemory.setActiveSegments(activeNextTick);
coordinator.finalizedAt = Game.time;
coordinator.plan = { activeNextTick, deferred };
coordinator.requested.clear();
return {
status: 'activation-scheduled',
activeNextTick,
deferred
};
}
This guard makes repeated same-tick finalization idempotent and exposes requests that arrive too late. It cannot protect code that bypasses the coordinator and calls RawMemory.setActiveSegments() directly, so keep the raw API behind one module boundary.
Distinguish unavailable, empty, corrupt, and ready
function readSegment(id, expectedVersion) {
const raw = RawMemory.segments[id];
if (raw === undefined) {
return { status: 'segment-unavailable', data: null };
}
if (raw === '') {
return { status: 'empty', data: {} };
}
try {
const parsed = JSON.parse(raw);
if (
parsed?.version !== expectedVersion
|| !parsed.data
|| typeof parsed.data !== 'object'
|| Array.isArray(parsed.data)
) {
return { status: 'schema-mismatch', data: null };
}
return { status: 'ready', data: parsed.data };
} catch (error) {
return {
status: 'invalid-json',
data: null,
error: error instanceof Error
? error.message
: String(error)
};
}
}
A schema mismatch is not automatically corrupt data. Preserve the original string until a migration, rollback, or recovery policy succeeds.
Preflight writes against the current official driver
function writeSegment(id, version, data) {
if (RawMemory.segments[id] === undefined) {
return {
ok: false,
status: 'segment-unavailable'
};
}
const raw = JSON.stringify({
version,
writtenAt: Game.time,
data
});
// Current official screeps/driver boundary.
// This is JavaScript string length, not UTF-8 encoded bytes.
const length = raw.length;
if (length > 100 * 1024) {
return {
ok: false,
status: 'segment-too-large-for-current-driver',
length
};
}
RawMemory.segments[id] = raw;
return {
ok: true,
status: 'segment-written',
length
};
}
Do not use new TextEncoder().encode(raw).length and describe that result as the current Screeps server's enforcement rule. UTF-8 byte length and JavaScript string length diverge for some non-ASCII text. A byte-based application budget can still be useful as your own portability policy, but label it as project policy rather than official driver behavior.
The current driver also validates saved Segment values as strings and rejects saving more than 10 Segment keys in one tick. The safest public-API workflow remains simple: write only Segments that are actually available to this tick and keep the active set coordinated.
A minimal read, merge, write workflow
function updateIntelSegment() {
const segmentId = 7;
const version = 1;
const request = requestSegment(segmentId, 100);
if (!request.accepted) {
return { status: request.status };
}
const current = readSegment(segmentId, version);
if (
current.status !== 'ready'
&& current.status !== 'empty'
) {
return { status: current.status };
}
return writeSegment(segmentId, version, {
...current.data,
lastSeenTick: Game.time
});
}
function runSegmentTick() {
const update = updateIntelSegment();
const activation = finalizeSegmentRequests();
return { update, activation };
}
On a first request, updateIntelSegment() commonly reports segment-unavailable while the finalizer schedules the ID for the next tick. That is an expected lifecycle state, not a reason to initialize or overwrite the Segment.
Failure modes worth logging separately
| Status | What it actually tells you |
|---|---|
segment-request-invalid | The ID or priority failed local validation. |
activation-already-finalized | A module requested data after the one activation plan for this tick was already committed. |
segment-unavailable | The Segment is not exposed to this tick; do not infer that its persistent contents are empty. |
schema-mismatch | The payload is readable JSON but not the version your consumer understands. |
segment-too-large-for-current-driver | The serialized JavaScript string exceeds the current official driver's length guard. |
How to verify across ticks
Record the requested IDs and Game.time, then inspect availability on the next tick. Test unavailable, empty, valid, invalid-JSON, schema-mismatch, deferred, repeated-finalize, ASCII boundary, and non-ASCII boundary cases separately. This revision contains no fabricated Segment contents, Console transcript, or live persistence trace.
Choose another guide when
Use the Memory guide for small durable application state. Use the global-cache guide when data is disposable and rebuildable after a global reset.