RUNTIME · CPU MEASUREMENT AND BUDGETING

How to Measure and Control CPU Usage in Screeps

Measure CPU with Game.cpu.getUsed() deltas, understand limit, tickLimit, and bucket, avoid Simulation-only conclusions, collect bounded samples, and gate optional work without hiding essential room logic.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — getUsed(), limit, tickLimit, bucket, bucket ceiling and Simulation behavior · Measurement boundary: CPU deltas are runtime observations; the Simulation returns 0 and cannot prove production cost

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — getUsed(), limit, tickLimit, bucket, bucket ceiling and Simulation behavior
Measurement boundary
CPU deltas are runtime observations; the Simulation returns 0 and cannot prove production cost
Control boundary
Bucket thresholds gate optional work only; essential colony actions remain separate
JavaScript syntax
Passed
Offline budget review
Passed — deltas, invalid samples, percentiles, bucket bands and hard budget guards
Screeps Console test
Pending
Live shard CPU and multi-tick bucket test
Pending
Last verified
July 25, 2026

Quick answer

Game.cpu.getUsed() is cumulative within the current tick. Measure a section by recording a start value, running the section, and subtracting the start from the end. Treat Game.cpu.limit as the normal per-tick allowance, Game.cpu.tickLimit as the current hard ceiling, and Game.cpu.bucket as stored CPU capacity. The Simulation reports 0 for getUsed(), so it cannot prove real CPU cost.

Understand limit, tickLimit, bucket, and getUsed

FieldMeaningDo not confuse it with
Game.cpu.getUsed()Cumulative CPU consumed so far this tickA section delta or a cross-tick average
Game.cpu.limitNormal CPU allowance associated with your accountThe maximum CPU available on every tick
Game.cpu.tickLimitCurrent hard CPU ceiling for the tickA target that should always be consumed
Game.cpu.bucketStored CPU reserve, capped at 10,000 in the official environmentFree CPU with no future cost

When the bucket is full, the official environment can allow a tickLimit as high as 500. These values describe the runtime budget. They do not identify which function is expensive.

Measure one code section

function measureCpu(label, fn) {
  if (typeof fn !== 'function') {
    return {
      label,
      ok: false,
      cpu: null,
      value: null,
      error: 'function-required'
    };
  }

  const start = Game.cpu.getUsed();

  try {
    const value = fn();
    const end = Game.cpu.getUsed();

    return {
      label,
      ok: true,
      cpu: Math.max(0, end - start),
      value,
      error: null
    };
  } catch (error) {
    const end = Game.cpu.getUsed();

    return {
      label,
      ok: false,
      cpu: Math.max(0, end - start),
      value: null,
      error: error instanceof Error
        ? error.message
        : String(error)
    };
  }
}

The wrapper measures the function body plus wrapper overhead. Use the same harness before and after a change so the comparison remains consistent.

Why the Simulation cannot validate CPU cost

The official API states that Game.cpu.getUsed() always returns 0 in the Simulation. A zero delta there does not prove that pathfinding, sorting, JSON parsing, or room scans are free.

function canMeasureCpuHere() {
  const first = Game.cpu.getUsed();
  const second = Game.cpu.getUsed();

  return {
    first,
    second,
    measurementAvailable:
      Number.isFinite(first)
      && Number.isFinite(second)
      && (first !== 0 || second !== 0)
  };
}

A real server tick can also produce a very small or zero-looking delta for a tiny section. The environment warning should be explicit rather than inferred from one call.

Collect bounded samples

State impact: this helper writes a bounded numeric history to Memory. It does not change game objects.

function recordCpuSample(name, value, limit = 100) {
  if (
    typeof name !== 'string'
    || !Number.isFinite(value)
    || !Number.isInteger(limit)
    || limit <= 0
  ) {
    return false;
  }

  Memory.cpuSamples ??= {};
  const samples = Array.isArray(
    Memory.cpuSamples[name]
  )
    ? Memory.cpuSamples[name]
    : [];

  samples.push({
    tick: Game.time,
    value
  });

  Memory.cpuSamples[name] = samples.slice(-limit);
  return true;
}

Memory writes and serialization have their own cost. For frequent profiling, sample only selected ticks, use a short history, or aggregate statistics instead of storing every raw event.

Use averages and percentiles

function summarizeNumbers(values) {
  const clean = values
    .filter(Number.isFinite)
    .sort((a, b) => a - b);

  if (clean.length === 0) {
    return null;
  }

  const percentile = ratio => {
    const index = Math.min(
      clean.length - 1,
      Math.max(
        0,
        Math.ceil(clean.length * ratio) - 1
      )
    );
    return clean[index];
  };

  const total = clean.reduce(
    (sum, value) => sum + value,
    0
  );

  return {
    count: clean.length,
    average: total / clean.length,
    median: percentile(0.5),
    p95: percentile(0.95),
    maximum: clean[clean.length - 1]
  };
}

An average hides rare expensive ticks. A p95 or maximum can reveal hostile scans, route rebuilds, cache misses, market refreshes, or global-reset reconstruction.

Use bucket bands as policy

function classifyCpuBucket(bucket) {
  if (!Number.isFinite(bucket)) {
    return 'unknown';
  }

  if (bucket < 1000) {
    return 'critical';
  }

  if (bucket < 4000) {
    return 'conserve';
  }

  if (bucket > 9000) {
    return 'surplus';
  }

  return 'normal';
}

The thresholds are example policy, not official recommendations. Choose them from your shard behavior and workload. Bucket bands decide whether optional work should run; they do not replace section-level measurements.

Complete CPU budget controller

State impact: this controller reads CPU fields and writes a compact runtime summary. It may skip optional callbacks, but it does not skip the essential callback.

function classifyCpuBucket(bucket) {
  if (!Number.isFinite(bucket)) {
    return 'unknown';
  }

  if (bucket < 1000) return 'critical';
  if (bucket < 4000) return 'conserve';
  if (bucket > 9000) return 'surplus';
  return 'normal';
}

function runCpuBudgetedTick({
  essential,
  optional,
  reserveCpu = 2
}) {
  if (typeof essential !== 'function') {
    throw new TypeError('essential callback required');
  }

  const startedAt = Game.cpu.getUsed();
  const essentialResult = essential();
  const afterEssential = Game.cpu.getUsed();
  const bucketBand = classifyCpuBucket(
    Game.cpu.bucket
  );
  const remainingToHardLimit = Math.max(
    0,
    Game.cpu.tickLimit - afterEssential
  );
  const optionalAllowed =
    typeof optional === 'function'
    && bucketBand !== 'critical'
    && remainingToHardLimit > reserveCpu;

  let optionalResult = null;
  let optionalCpu = 0;

  if (optionalAllowed) {
    const optionalStart = Game.cpu.getUsed();
    optionalResult = optional();
    optionalCpu = Math.max(
      0,
      Game.cpu.getUsed() - optionalStart
    );
  }

  const finishedAt = Game.cpu.getUsed();
  const summary = {
    tick: Game.time,
    bucket: Game.cpu.bucket,
    bucketBand,
    limit: Game.cpu.limit,
    tickLimit: Game.cpu.tickLimit,
    essentialCpu: Math.max(
      0,
      afterEssential - startedAt
    ),
    optionalAllowed,
    optionalCpu,
    totalCpu: Math.max(0, finishedAt - startedAt)
  };

  Memory.cpuRuntime = summary;

  return {
    summary,
    essentialResult,
    optionalResult
  };
}

A production controller should define which operations are essential before enabling a low-bucket mode. Defense, spawn recovery, source logistics, Controller safety, and required Memory cleanup usually need stronger guarantees than analytics or broad scans.

Separate essential and optional work

ClassExamplesLow-bucket behavior
EssentialDefense, spawn recovery, core harvesting, Controller protectionRun with bounded algorithms
Deferred maintenanceCache rebuild, route refresh, market scanRun less often or in slices
Optional diagnosticsDetailed logs, visuals, analyticsDisable first

The categories depend on the colony. A market deal can become essential during an energy emergency, while an ordinary market scan remains optional.

Stop before optional work exhausts the tick

function hasCpuHeadroom(reserveCpu = 2) {
  if (!Number.isFinite(reserveCpu) || reserveCpu < 0) {
    return false;
  }

  return Game.cpu.getUsed()
    < Game.cpu.tickLimit - reserveCpu;
}

function runUntilBudget(items, worker, reserveCpu = 2) {
  let completed = 0;

  for (const item of items) {
    if (!hasCpuHeadroom(reserveCpu)) {
      break;
    }

    worker(item);
    completed += 1;
  }

  return completed;
}

This guard reduces the chance that optional loops consume the hard ceiling. It does not make an individual worker safe; each iteration still needs bounded complexity and error isolation.

Reduce measurement noise

  • Compare the same room count and similar input size.
  • Separate global-reset ticks from warm-cache ticks.
  • Measure cache hits and misses independently.
  • Record pathfinding operations and result shape alongside CPU.
  • Limit logging during the measured section.
  • Use several samples and report p95 or maximum.
  • Measure the caller when optimization changes work distribution.

Moving work to another tick or hiding it behind a cache can lower one function's delta while leaving total colony CPU unchanged. Track both section and whole-tick measurements.

Debugging checklist

  • Confirm the environment reports meaningful getUsed() values.
  • Subtract start from end; do not treat the cumulative end value as section cost.
  • Keep sample histories bounded.
  • Record representative input size.
  • Separate global-reset and warm-cache samples.
  • Check average, p95, and maximum.
  • Use bucket only as a scheduling signal.
  • Keep essential work outside optional low-bucket gates.
  • Reserve CPU before running optional loops.
  • Verify the complete tick, not only one optimized function.

Scope and next steps

This guide does not provide shard-specific benchmark numbers, a production profiler library, heap statistics, account CPU allocation advice, pixel generation policy, or a live bucket recovery experiment. Continue with global cache design to understand warm and reset CPU behavior.

Frequently asked questions

Is getUsed() a timer in milliseconds?

Treat it as Screeps CPU usage reported by the runtime, not wall-clock duration.

Can I optimize in the Simulation?

You can test logic, but the Simulation's zero getUsed() result cannot validate real CPU cost.

Should I always spend CPU when the bucket is high?

No. A high bucket permits optional work, but the work should still have value and a bounded budget.

What should be disabled first?

Diagnostics, visuals, broad refreshes, and non-urgent analysis are safer candidates than survival logic.

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.