openclaw - ✅(Solved) Fix [Bug]: Heartbeat runs despite heartbeat: {} config (2M+ tokens/day with zero user activity) [2 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#64293Fetched 2026-04-11 06:15:30
View on GitHub
Comments
1
Participants
2
Timeline
11
Reactions
0
Timeline (top)
cross-referenced ×3referenced ×3labeled ×2commented ×1

Heartbeats continue running every 30 minutes despite setting agents.defaults.heartbeat: {} in config. This causes constant token burn (~2M input tokens/day) with zero user activity. Config changes and full update to 2026.4.6 did not resolve the issue.

Root Cause

Heartbeats continue running every 30 minutes despite setting agents.defaults.heartbeat: {} in config. This causes constant token burn (~2M input tokens/day) with zero user activity. Config changes and full update to 2026.4.6 did not resolve the issue.

Fix Action

Fixed

PR fix notes

PR #64329: fix(heartbeat): treat empty heartbeat config ({}) as disabled

Description (problem / solution / changelog)

Summary

Fixes #64293 — Heartbeats continue running despite heartbeat: {} config, causing ~2M tokens/day burn with zero user activity.

Root Cause

When agents.defaults.heartbeat: {} is set, the empty object {} is truthy in JavaScript. resolveHeartbeatConfig returned it as-is, and resolveHeartbeatIntervalMs then fell through to the default DEFAULT_HEARTBEAT_EVERY = "30m" since {}.every is undefined.

Result: heartbeats ran every 30 minutes even though the user intended to disable them.

Fix

Added isEmptyHeartbeatConfig() helper that detects Object.keys(config).length === 0. When the effective config (after merging defaults and overrides) is an empty object, resolveHeartbeatConfig now returns undefined — which the scheduler correctly interprets as "no heartbeat" and skips scheduling.

Why empty {} = disabled

  • Setting heartbeat: {} is the most intuitive way to say "I acknowledge this config key exists but want it off"
  • The issue reporter and other users explicitly tried this as an opt-out mechanism
  • heartbeat: { every: "0" } and omitting the key entirely already disable heartbeats; {} should be consistent
  • Users who want defaults can simply omit the heartbeat key

Changes

  • src/infra/heartbeat-runner.ts: resolveHeartbeatConfig returns undefined for empty merged config; new isEmptyHeartbeatConfig helper

Testing

  • Empty {}resolveHeartbeatConfig returns undefined → heartbeat scheduler skips
  • { every: "30m" } → works as before (not empty)
  • undefined / key omitted → works as before (fallback defaults apply)

Changed files

  • src/infra/heartbeat-runner.empty-config-disabled.test.ts (added, +85/-0)
  • src/infra/heartbeat-runner.ts (modified, +15/-3)
  • src/infra/heartbeat-summary.ts (modified, +16/-3)

PR #64540: fix: treat empty heartbeat config as disabled (closes #64293)

Description (problem / solution / changelog)

Summary

  • Problem: Setting agents.defaults.heartbeat: {} in config does not disable heartbeats — they still fire every 30 minutes, burning ~2M+ input tokens/day with zero user activity.
  • Why it matters: Users expect heartbeat: {} to mean "disabled". Silent token burn at ~$6/day (Claude Sonnet 4.5 rates) with no visible indication.
  • What changed: Added empty-object detection in isHeartbeatEnabledForAgent and resolveHeartbeatIntervalMs so that heartbeat: {} is treated as disabled instead of falling through to the default 30m interval.
  • What did NOT change: Non-empty heartbeat configs (e.g. { every: "1h" }, { prompt: "..." }) are completely unaffected. Default behavior when no heartbeat key is present is also unchanged.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #64293
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: resolveHeartbeatIntervalMs() falls through to DEFAULT_HEARTBEAT_EVERY ("30m") when heartbeat?.every is undefined — which is the case for an empty {} config. Similarly, isHeartbeatEnabledForAgent() returns true for the default agent even when the defaults heartbeat is {}, because it only checks for explicit agent-level heartbeat entries and never inspects the defaults object itself.
  • Missing detection / guardrail: No check for empty-object heartbeat config anywhere in the resolution chain.
  • Contributing context: Users naturally assume heartbeat: {} means "I acknowledged this config key but want it off" (similar to how cron: {} or memory: {} would imply disabled).

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
  • Target test or file: src/infra/heartbeat-summary.test.ts
  • Scenario the test should lock in: resolveHeartbeatIntervalMs(cfg, undefined, {}) returns null; isHeartbeatEnabledForAgent(cfg) returns false when defaults heartbeat is {}
  • Why this is the smallest reliable guardrail: Pure function, easily testable in isolation
  • If no new test is added, why not: Keeping PR minimal — happy to add tests if requested by reviewer

User-visible / Behavior Changes

  • agents.defaults.heartbeat: {} now correctly disables heartbeat polling (previously silently ran every 30m)
  • Per-agent heartbeat: {} in agents list now correctly disables that agent's heartbeat

Diagram (if applicable)

Before:
heartbeat: {} -> resolveHeartbeatIntervalMs -> falls through to DEFAULT "30m" -> schedules heartbeat

After:
heartbeat: {} -> resolveHeartbeatIntervalMs -> detects empty object -> returns null -> no heartbeat scheduled

Security Impact (required)

  • New permissions/capabilities? No
  • Auth boundary changes? No
  • Secrets/token exposure risk? No
  • New external calls? No
  • Sandbox/isolation changes? No

Evidence

  • Code inspection: traced the full heartbeat resolution chain from config load → resolveHeartbeatConfigresolveHeartbeatIntervalMs → scheduling
  • pnpm check passes (lint + typecheck)
  • AI-assisted (Claude): fix authored with AI assistance, manually reviewed and verified

Human Verification (required)

  • Verified scenarios: Traced config resolution for heartbeat: {}, heartbeat: { every: "1h" }, and no heartbeat key — confirmed only empty-object case is affected
  • Edge cases checked: heartbeat: undefined, heartbeat: null, heartbeat: { every: "0" } — all unaffected by this change
  • What you did not verify: Full integration test with running gateway (no Docker setup locally for this)

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes — only changes behavior for the specific {} empty-object case
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: Someone intentionally uses heartbeat: {} expecting default 30m behavior
    • Mitigation: This is extremely unlikely — {} universally signals "empty/disabled" in config conventions. Users wanting defaults would simply omit the key.

Changed files

  • src/infra/heartbeat-summary.ts (modified, +21/-2)
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

Heartbeats continue running every 30 minutes despite setting agents.defaults.heartbeat: {} in config. This causes constant token burn (~2M input tokens/day) with zero user activity. Config changes and full update to 2026.4.6 did not resolve the issue.

Steps to reproduce

  1. Set agents.defaults.heartbeat: {} in openclaw.json
  2. Restart gateway with docker compose restart
  3. Wait 24 hours with zero user interaction
  4. Check Anthropic API usage console
  5. Observe constant token usage every ~30 minutes (~150K tokens per heartbeat)

Expected behavior

With heartbeat: {}, no automatic heartbeat polls should occur. Token usage should be zero when there is no user activity.

Actual behavior

Heartbeats fire every 30 minutes regardless of config:

  • 2M input tokens/day ($6/day at Claude Sonnet 4.5 rates)
  • ~150K tokens per heartbeat (full context load with workspace files)
  • Consistent pattern visible in Anthropic console
  • Verified with screenshots showing evenly-spaced API calls

What we tried (all failed to stop heartbeats):

  • Set heartbeat: {} in config
  • Gateway restart via docker compose
  • Full update from 2026.3.14 to 2026.4.6
  • Verified no cron jobs active
  • Confirmed no Control UI open
  • Only one session running

OpenClaw version

2026.4.6 (updated from 2026.3.14 on April 6, 2026)

Operating system

Linux (Docker container on Hetzner VPS, Ubuntu host)

Install method

Git + Docker Compose

Model

anthropic/claude-sonnet-4-5

Provider / routing chain

Direct Anthropic API (no routing chain)

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

No response

Additional information

No response

extent analysis

TL;DR

  • Verify that the agents.defaults.heartbeat configuration is being applied correctly and explore alternative configuration options to disable heartbeats.

Guidance

  • Check the OpenClaw documentation to ensure that setting agents.defaults.heartbeat: {} is the correct way to disable heartbeats, as the expected behavior is not being observed.
  • Investigate if there are any other configuration files or environment variables that could be overriding the heartbeat setting in openclaw.json.
  • Review the Docker Compose configuration to ensure that the gateway restart is properly applying the updated configuration.
  • Consider reaching out to the OpenClaw community or support team for further assistance, as the issue persists despite updating to the latest version.

Notes

  • The provided information suggests that the configuration change should disable heartbeats, but it is not working as expected.
  • There may be other factors at play, such as caching or default values, that are causing the heartbeats to continue.

Recommendation

  • Apply workaround: Try setting agents.defaults.heartbeat to a specific value, such as null or false, to see if that disables the heartbeats, as the current empty object {} may not be the correct way to disable them.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

With heartbeat: {}, no automatic heartbeat polls should occur. Token usage should be zero when there is no user activity.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: Heartbeat runs despite heartbeat: {} config (2M+ tokens/day with zero user activity) [2 pull requests, 1 comments, 2 participants]