Skip to content

[FIX] Heartbeat ceiling 600s -> 240s : it sat above the server's 300s session window and under-billed peak CCU - #3

Merged
francoios merged 1 commit into
mainfrom
fix/heartbeat-ceiling-240
Aug 25, 2026
Merged

francoios merged 1 commit into
mainfrom
fix/heartbeat-ceiling-240

Conversation

@francoios

Copy link
Copy Markdown
Contributor

Companion to AnkleBreaker-Studio/tombstack#3, which fixed the Tombstack side of the same defect.

The bug

max_heartbeat_interval_s was 600. The server's SESSION_WINDOW_MS is 300s, and Unity and Godot both clamp to 240.

computePeakConcurrency merges consecutive beats into one live span only while the gap stays under the session window. At 600s a player online continuously for an hour produces six isolated instants instead of one span — concurrent players stop overlapping, monthly peak CCU reads low, and the studio is invoiced on the low figure.

An Unreal or dedicated-server integrator raising the interval to cut telemetry cost — explicitly permitted by the header's own comment — silently reduced their own bill and the product's revenue together.

This is bit-for-bit the defect the Unity SDK shipped in 0.19.3. Tombstack's tests/heartbeat-cadence.test.ts exists to stop it recurring and its docblock names this exact failure, but it read the C# source only, so this SDK kept 600 with a green suite on both sides.

Changed

  • src/client.cpp — the enforcing std::clamp ceiling, 600 to 240
  • include/tombstone/tombstone.h — the documented range on options.heartbeat_interval_s
  • Version bumped to 0.9.2 (CMakeLists.txt + src/tombstone_api.cpp) and a CHANGELOG entry

A caller passing more than 240 now gets 240. The default (60s) and floor (15s) are untouched. No signature or ABI change.

Not tested in this repo — stated plainly

max_heartbeat_interval_s is translation-unit local and Client exposes no accessor for the resolved interval, so asserting it would mean widening internals for the test — a larger change than the fix.

The contract an integrator actually reads is the header, and that is now pinned from Tombstack: the guard there parses the ceiling out of this repo's public header and asserts Unity, Godot and native agree on one number. It was proved non-blind by reverting the header to 600 and watching it fail.

A follow-up exposing the resolved interval would let tests/ cover the clamp directly.

Follow-up flagged, not silently carried

The version is duplicated as a literal in src/tombstone_api.cpp (sdk_version) and in project(... VERSION ...). That duplication has already shipped two wrong versions — the changelog records this string pinned at 0.1.0 once and stuck at 0.8.0 once. Both are bumped here and both now carry a comment saying so; deriving it from PROJECT_VERSION is the real fix.

Test plan

  • Tombstack suite green with the mirrored header: 443 files / 8596 tests
  • Vendored header in Tombstack re-verified content-identical to this one
  • cmake --build build && ctest --test-dir build on a machine with a toolchain (not run here — no C++ toolchain in this environment)

…n window, so it under-billed peak CCU

max_heartbeat_interval_s was 600. The server's SESSION_WINDOW_MS is 300s, and Unity and Godot both
clamp to 240.

WHY THAT IS A BILLING BUG, NOT A CADENCE PREFERENCE. computePeakConcurrency merges consecutive beats
into ONE live span only while the gap stays <= the session window. At 600s a player online continuously
for an hour produces six isolated instants instead of one span. Concurrent players' spans stop
overlapping, the monthly PEAK CCU comes out low, and the studio is invoiced on the low figure. An
Unreal or dedicated-server integrator raising the interval to cut telemetry cost - explicitly permitted
by the header's own comment - silently reduced their own bill and the product's revenue together.

This is bit-for-bit the defect the Unity SDK shipped in 0.19.3 (MAX_HEARTBEAT_INTERVAL_SECONDS = 600f).
Tombstack carries tests/heartbeat-cadence.test.ts to stop it recurring, and its docblock names this
exact failure - but it read the C# source and ONLY the C# source, so this SDK kept 600 with a green
suite on both sides. That guard now parses the ceiling out of this repo's public header as well, and
asserts Unity, Godot and native agree on one number.

Changed here: the enforcing std::clamp in src/client.cpp and the range documented on
options.heartbeat_interval_s in the public header. A caller that passes more than 240 now gets 240; the
default (60s) and floor (15s) are untouched, and no signature or ABI changes.

NOT TESTED IN THIS REPO, stated plainly rather than implied. max_heartbeat_interval_s is
translation-unit local and Client exposes no accessor for the resolved interval, so asserting it would
mean widening internals for the test - a larger change than the fix. The contract an integrator reads
IS the header, and that is now pinned from Tombstack. A follow-up that exposes the resolved interval
would let tests/ cover it directly.

FOLLOW-UP FLAGGED, NOT SILENTLY CARRIED: the version is duplicated as a literal in
src/tombstone_api.cpp (`sdk_version`) and in project(... VERSION ...). That duplication has already
shipped two wrong versions - the changelog records this string pinned at "0.1.0" once and stuck at
"0.8.0" once. Both are bumped to 0.9.2 here, and both now carry a comment saying so; deriving it from
PROJECT_VERSION is the real fix.
@francoios
francoios merged commit f34808d into main Aug 25, 2026
3 checks passed
@francoios
francoios deleted the fix/heartbeat-ceiling-240 branch August 25, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant