CloudLine
SDK

Raw HTTP

For Go, Rust, Java, C#, PHP, Ruby — and any other language. The heartbeat protocol by hand.

When there's no official SDK for your language, you write the heartbeat loop yourself. It's one HTTP POST every N seconds. The dashboard's Heartbeat tab generates a ready-to-paste snippet for JavaScript, Python, curl, Go, Rust, C# / .NET, Java, and Ruby — copy from there if you can. This page documents the wire protocol so you can write it in anything else.

Endpoint

POST https://cloudline.kescohhtwitch.workers.dev/api/bots/{botId}/heartbeat

Headers:

Authorization: Bearer <your clb_live_… secret>
Content-Type:  application/json

Body: JSON. Every field is optional — send what you can measure, leave the rest as null or omit it entirely.

Body fields

FieldTypeMeaning
latency_msnumberDiscord gateway ping in ms.
memory_mbnumberProcess RAM in MB (resident set size).
cpu_pctnumberProcess CPU % (one core = 100).
uptime_secnumberProcess uptime in seconds.
guildsnumberServer (guild) count.
event_loop_lag_msnumberEvent-loop / async-runtime lag in ms. SDK-only on most runtimes; skip for raw fetch.
slash_p50_ms / slash_p95_ms / slash_countnumberSlash command timing percentiles + count since last beat.
component_p50_ms / component_p95_ms / component_countnumberSame, for buttons + select menus.
autocomplete_p50_ms / autocomplete_p95_ms / autocomplete_countnumberSame, for slash-command autocomplete.
shards_total / shards_connectednumberFor sharded bots. Leave null on single-process bots.
gateway_okbooleantrue when your gateway connection is healthy. Drives zombie detection.
gateway_stale_secnumberHow many seconds the gateway has been unhealthy.
shard_detailarrayPer-shard health: [{ id: 0, ok: true, ping: 47 }, ...]. Sharded bots only.
discord_rate_limit_hitsnumberHow many 429 responses Discord returned since last beat.
custom_metricsobjectYour own gauges + counters. See Custom metrics.
seqnumberMonotonic counter that increments each beat. Lets the server detect restarts (seq drops to 1).
sent_atnumberSend timestamp in milliseconds since epoch. The server uses this with its own receive time to compute delivery delay, clock-skew-tolerant.

All numeric fields are server-side range-clamped — out-of-range values become null rather than failing the request.

Minimal payload

The smallest meaningful body is just seq + sent_at:

{
  "seq": 1,
  "sent_at": 1735862400000
}

That registers the bot as online but leaves every metric blank. Add fields as you can measure them.

Typical payload (single-process bot)

{
  "latency_ms": 42,
  "memory_mb":  180,
  "uptime_sec": 3600,
  "guilds":     127,
  "seq":        1,
  "sent_at":    1735862400000
}

Responses

  • 200 OK — beat accepted. Body is empty.
  • 401 Unauthorized — bad or missing secret. Do not retry, fix the credential.
  • 429 Too Many Requests — you're heartbeating too fast or hit a rate limit on the public ingestion endpoint. Back off and try again.
  • 5xx — server-side problem. Retry with backoff (we recommend 250 ms → 500 ms → 1 s, max 2 retries).

Heartbeat loop pattern (pseudocode)

on bot start:
  seq = 0
  start_time = now

every N seconds:
  seq += 1
  payload = {
    "latency_ms": measure_gateway_ping(),
    "memory_mb":  measure_rss_mb(),
    "uptime_sec": seconds_since(start_time),
    "guilds":     count_guilds(),
    "seq":        seq,
    "sent_at":    now_in_milliseconds(),
  }
  for attempt in 1..3:
    try:
      response = POST endpoint with Bearer secret, body=json(payload)
      if response.status == 200: break
      if response.status == 401: log("bad secret"); break
      if response.status in (429, 5xx) and attempt < 3:
        sleep(250ms * 2^(attempt-1)); continue
      log("HTTP " + response.status); break
    catch network_error:
      if attempt < 3: sleep(250ms * 2^(attempt-1)); continue
      log("network error: " + error)

Things to get right

A few details that hurt if you skip them:

  1. Use a per-request timeout. Without one, a hung TCP connection blocks your bot for minutes. We recommend 10 seconds.
  2. Don't run two heartbeats in parallel. If one beat hasn't finished by the time the next interval fires, skip the new one instead of stacking. Otherwise seq can arrive out of order at the server.
  3. seq starts at 0 each process start. That's the feature — when the server sees seq drop back to 1, it knows the bot restarted.
  4. Re-stamp sent_at on each retry. Don't reuse the original. The server uses received_at − sent_at to compute delivery delay, and you want that to reflect the actual wire moment.
  5. Don't retry on 401. It will never succeed; you'll burn rate-limit quota for nothing.

Discord ping mapping

LibraryWhere to get latency in ms
discord.jsclient.ws.ping (returns -1 before the first ack — send null)
discord.pybot.latency * 1000 (returns NaN before the first ack — send null)
DiscordGo (Go)session.HeartbeatLatency().Milliseconds()
serenity (Rust)shard.latency()
Discord.Net (C#)client.Latency
JDA (Java)jda.getGatewayPing()
discordrb (Ruby)bot.heartbeat_latency * 1000

Error reporting

Errors go to a separate endpoint:

POST /api/bots/{botId}/errors
Authorization: Bearer <secret>
Content-Type:  application/json

{
  "message": "TypeError: cannot read property 'foo' of undefined",
  "stack":   "<full stack trace, optional>",
  "level":   "error",
  "context": { "userId": "123" }
}

The dashboard's Heartbeat tab also generates a ready-to-paste error reporter for the same 8 languages, complete with per-minute caps, message dedupe, and retry logic. If you write your own, mirror those guards or a crash loop will flood the endpoint.