Honesty metrics
This is the moat. Most hosts hide performance behind a marketing dashboard with reassuring green bars. TrueTick exposes the real server health — TPS, MSPT, player count — over the same API your tooling uses, measured directly from the running server. When there's no data, you get an honest empty state, not a fabricated number.
What you get
GET /v1/servers/{id}/metrics (scope servers:read) returns:
| Field | Meaning |
|---|---|
tps | Ticks per second from the latest poll. 20 is healthy. Sustained dips below ~18 mean the server is struggling. Only meaningful when tpsSource is TPS_SOURCE_MEASURED or TPS_SOURCE_CORE — see below |
tpsSource | Where tps came from: TPS_SOURCE_MEASURED (we counted the world's own tick counter), TPS_SOURCE_CORE (the core reported its own rate), or TPS_SOURCE_UNSPECIFIED — no TPS reading exists, and tps is a zero value, not a zero measurement |
headlineTps, headlineWindowSeconds | The smoothed rate the panel's tile shows: the tick counter's delta across a trailing 60 s window. headlineWindowSeconds is how much measured ticking that number rests on, which can be less than 60 right after a start or wake. headlineWindowSeconds: 0 means there is no headline yet |
targetTps | The tick rate the server is configured to aim for (normally 20). 0 = the core didn't report one |
mspt | Milliseconds per tick — how long the average tick takes. Note that a tick fitting in the budget does not by itself mean the loop ran at 20 Hz; that's what tps is for |
msptP95 | The tail: 95th-percentile tick time. Lag lives here, not in the mean. Read 0 as "no tail to act on in this reading", never as a measured zero — and note that it does not tell you why. Cores we never read a tail from are 0 on every poll: Forge, NeoForge and Pumpkin at any version, and Paper or Purpur below Minecraft 1.20.3 — we read their own tick-stat command, which carries no percentile line. On a core whose command does carry one, 0 is also what we store for a response that carries no percentile line, and what the core itself prints for a tail small enough to round away at one decimal (P95: 0.0ms). Those are not distinguishable on the wire, so a 0 on a TPS_SOURCE_MEASURED sample is not a statement about the core, and the next poll may carry a number. (Vanilla and Fabric below 1.20.3 report no tick stats at all, so the question does not arise for them.) |
tickStatus | The core's own word about its loop: "running", "lagging", or "" when the core has no state word. This is a quote from the server, not our interpretation |
players | Current online player count |
live | true when tick data (MSPT, status, players) is a fresh RCON measurement; false/empty when there's no live data. live: true does not guarantee a TPS reading — check tpsSource |
cpuPercent | Container CPU usage |
memUsedMb, memLimitMb | Container RSS vs the cgroup memory limit. On a game server, the default perf preset (-XX:+AlwaysPreTouch, heap sized fully at boot) makes this a fixed function of the tier — it doesn't move with load, so it's not a load signal. (A Velocity proxy server runs without that preset, so its reading does move. A Pumpkin server gets no preset either — it is a Rust core with no JVM heap to pre-touch — so nothing pins its reading to the tier.) Use heapUsedMb/heapMaxMb for a load-sensitive reading on a JVM game server (see the next row for Pumpkin) |
heapUsedMb, heapMaxMb | JVM heap in use vs the heap ceiling (-Xmx), read from a read-only JMX exporter — the number that actually tracks load on a game server. 0/absent until the server's next restart after this is enabled, and always absent on server types with no JVM to instrument — a Velocity proxy, and a Pumpkin server, which is a Rust core with no JVM heap at all. On Pumpkin neither number is available, so memUsedMb is the only memory usage reading there is |
series | A recent ring of samples (for sparklines / trend) |
const m = await client.servers.metrics("my-smp");
const rate = m.tpsSource === "TPS_SOURCE_UNSPECIFIED" ? "no TPS reading" : `TPS ${m.tps}`;
console.log(`${rate} · MSPT ${m.mspt} · ${m.players} players (live: ${m.live})`);curl -H "x-api-key: $TRUETICK_API_KEY" \
"https://api.truetick.gg/v1/servers/my-smp/metrics"How it's measured
A poller queries the running server over RCON on an interval, keeps the latest values plus a short series ring, and gates them behind a staleness check.
We measure the tick rate rather than infer it. On Paper, Purpur, Vanilla and Fabric from Minecraft
1.20.3, each poll reads the world's own tick counter (time query gametime) and the rate is the counter's
delta divided by the seconds that actually elapsed. That is what the world delivered, not what the average
tick time suggests it could have delivered — those two diverge, because time also passes between ticks.
Those samples carry tpsSource: "TPS_SOURCE_MEASURED".
Forge and NeoForge report an achieved rate themselves (forge tps / neoforge tps), as do Paper and
Purpur below 1.20.3 (/tps) — there we show the core's number and mark it TPS_SOURCE_CORE. MSPT, the
p95 tail and tickStatus come from the core's tick query output on every core that has it.
GET /v1/servers/{id}/tick-history?hours=24 returns the same measurements as one-minute buckets
(tps, msptMean, msptP95, players, lagging, and samples — how many polls each bucket rests
on). Minutes where the server was asleep or unmeasured have no row at all, so a gap in the series is
a real gap, never a zero.
Honest empty states
The single most important rule: TrueTick never invents metrics.
- A hibernated / stopped server has no live tick loop, so
liveis false and there's no TPS to report — you'll see an honest empty/live:falsestate, not a stale "20 TPS." - If the poller's data is stale (server unreachable, just woke, between polls), the API reports it as not-live rather than serving cached-but-false numbers.
- A measured rate needs two counter readings, so the first poll after a start or a wake has
live: trueandtpsSource: "TPS_SOURCE_UNSPECIFIED". Same for a world that is legitimately parked (pause-when-emptywith nobody online): the counter isn't advancing because there is nothing to tick. - Vanilla and Fabric below Minecraft 1.20.3 have no tick command at all; rather than fake it, the API returns an honest empty TPS with a type-specific note. A Velocity proxy has no world tick to report either. (Memory and player count still report.)
- Cores that print no percentiles leave
msptP95at0rather than fabricating a smooth tail.
tps: 0 is not automatically "the server stalled." Read it together with tpsSource:
TPS_SOURCE_UNSPECIFIED means no reading, and a client that treats it as a number will fire
alerts at healthy servers. A real 0.0 — the world genuinely not ticking while players are online —
arrives with TPS_SOURCE_MEASURED.
When you read live: false, that's the system being honest — there genuinely is no live tick data
right now. Treat it as "no signal," not "zero performance."
Using the metrics
Because the numbers are real, you can build real automation on them:
- Autoscaling / alerting agents — read TPS to decide whether to restart, warn, or migrate load.
- Health gates in CI — after deploying a plugin to an ephemeral server, assert TPS stayed at 20 under your test load before promoting.
- Public status — surface the same honest numbers your players would see.
TrueTick can also push Discord alerts when a server's TPS drops (configurable per server via the alert-webhook endpoint). A server whose core reports no live tick metrics gets none, so keep polling if you need to know about a drop — treat an alert as a convenience, not as your monitoring.
// A health gate. The tpsSource check is the load-bearing part: without it,
// the first poll after a wake (no reading yet, tps === 0) fails the gate on a
// server that is perfectly healthy.
const m = await client.servers.metrics("ci-test");
const measured = m.tpsSource === "TPS_SOURCE_MEASURED" || m.tpsSource === "TPS_SOURCE_CORE";
if (measured && m.tps < 19) {
throw new Error(`Performance regression: TPS ${m.tps} (mspt ${m.mspt})`);
}
if (m.tickStatus === "lagging") {
throw new Error(`The core reports it can't keep up (mspt ${m.mspt}, p95 ${m.msptP95})`);
}