rustfs/rustfs · warning · HeartbeatError

RustFS is not registered with Connect

Error message

RustFS is not registered with Connect

What it means

Every heartbeat builds its mTLS client from a stored device credential; when credential_store.load() returns None (device.crt.json absent), the node has no Connect identity, so TelemetryError::NotRegistered maps to HeartbeatError::NotRegistered. It means registration never completed for this state directory — a pending registration file alone is not enough, the device credential only exists after completion. Heartbeats cannot proceed until registration finishes.

Source

Thrown at rustfs/src/connect/heartbeat.rs:369

}

fn fsync_dir(directory: &Path) -> io::Result<()> {
    #[cfg(unix)]
    fs::File::open(directory)?.sync_all()?;
    #[cfg(not(unix))]
    let _ = directory;
    Ok(())
}

#[derive(Debug, thiserror::Error)]
pub enum HeartbeatError {
    #[error("Connect heartbeat endpoint must be an HTTPS base URL without credentials, query, or fragment")]
    Endpoint,
    #[error("Connect heartbeat root CA configuration is invalid")]
    RootCertificate,
    #[error("Connect heartbeat schedule is invalid")]
    Schedule,
    #[error("RustFS is not registered with Connect")]
    NotRegistered,
    #[error("the Connect device private key is missing")]
    IdentityMissing,
    #[error("the stored Connect certificate and device private key cannot form a TLS identity")]
    IdentityCertificate,
    #[error("the stored Connect credential name is invalid")]
    CredentialName,
    #[error("the stored Connect device certificate is not currently valid")]
    CredentialExpired,
    #[error("the Connect heartbeat node summary is outside protocol bounds")]
    NodeSummary,
    #[error("the Connect heartbeat sequence is exhausted")]
    SequenceExhausted,
    #[error("a Connect heartbeat runtime already owns this state")]
    AlreadyRunning,
    #[error("the persisted Connect heartbeat changed while delivery was in flight")]
    StateConflict,
    #[error("Connect heartbeat state I/O failed at {path}: {source}")]

View on GitHub (pinned to 201c653dcd)

Solutions

  1. Complete the Connect registration flow (token -> registration) so device.crt.json is persisted in the credential directory
  2. Verify the credential directory is the same one registration wrote to and that device.crt.json exists with mode 0600
  3. If registration was completed before, restore the state directory from backup; otherwise run registration again
Defensive patterns

Strategy: try-catch

Validate before calling

let credential_file = Path::new(&connect_state_dir).join("device.crt.json");
if !credential_file.exists() {
    // run Connect registration before starting the heartbeat loop
}

Type guard

fn is_not_registered(err: &HeartbeatError) -> bool {
    matches!(err, HeartbeatError::NotRegistered)
}

Try / catch

match sender.send(&pending).await {
    Err(HeartbeatError::NotRegistered) => {
        // expected pre-registration: defer/suspend heartbeat, surface registration prompt;
        // do NOT tight-loop retries — registration is an operator action
    }
    result => result,
}

Prevention

When it happens

Trigger: Starting the heartbeat/telemetry loop on a fresh install before running Connect registration; registration started but interrupted before device.crt.json was written; the credential directory deleted, moved, or pointing at the wrong path (load() only treats NotFound as 'no credential').

Common situations: New deployments skipping onboarding; restoring cluster data without the connect state directory; containers losing the state volume on restart; env var typos pointing at an empty directory.

Related errors


AI-assisted analysis of rustfs/rustfs@201c653dcd (2026-08-23). Data as JSON: /api/errors/605edc3980209dd1. Report an issue: GitHub.