Event-driven pings

The ping patterns for on-demand work, and why concurrent runs need a run_id to stay distinguishable.

Three pings, one run_id:

bash
RUN_ID="job-$(uuidgen)"
API="https://api.drumbeats.io/v1/ping/<monitor-id>"

curl -sf "$API/start?run_id=$RUN_ID"
python process_order.py
if [ $? -eq 0 ]; then
  curl -sf "$API/success?run_id=$RUN_ID"
else
  curl -sf "$API/failure?run_id=$RUN_ID"
fi
RUN_ID="job-$(uuidgen)"
API="https://api.drumbeats.io/v1/ping/<monitor-id>"

curl -sf "$API/start?run_id=$RUN_ID"
python process_order.py
if [ $? -eq 0 ]; then
  curl -sf "$API/success?run_id=$RUN_ID"
else
  curl -sf "$API/failure?run_id=$RUN_ID"
fi

These patterns apply to Event-driven monitors: queue workers, webhook handlers, CI jobs, anything triggered rather than scheduled.

Why run_id is not optional here#

Ten workers processing the same queue send ten start pings, then ten finishes, interleaved in whatever order they complete. Without run_id, Drumbeats sees an undifferentiated stream and cannot pair any start with any finish. Per-run duration becomes meaningless and hung runs go undetected, because every start looks like it was closed by somebody else's success.

Pass the same run_id on start, on every log, and on the finish.

StrategyExampleUse when
UUIDjob-$(uuidgen)Anything concurrent. The safe default
Domain identifierorder-${order.id}You want real order or ticket numbers on the timeline
Timestamp plus randomjob-$(date +%s)-$RANDOMCheap uniqueness with no uuidgen available
Timestamp alonejob-$(date +%s)Single-process scripts only. Collides under concurrency

Wire it into your language#

Python
import requests, uuid

API = "https://api.drumbeats.io/v1/ping/<monitor-id>"

def run_job():
    run_id = f"job-{uuid.uuid4()}"
    requests.get(f"{API}/start", params={"run_id": run_id}, timeout=5)
    try:
        do_work()
        requests.get(f"{API}/success", params={"run_id": run_id}, timeout=5)
    except Exception as e:
        requests.post(
            f"{API}/failure",
            params={"run_id": run_id},
            json={"payload": str(e)},
            timeout=5,
        )
        raise
import requests, uuid

API = "https://api.drumbeats.io/v1/ping/<monitor-id>"

def run_job():
    run_id = f"job-{uuid.uuid4()}"
    requests.get(f"{API}/start", params={"run_id": run_id}, timeout=5)
    try:
        do_work()
        requests.get(f"{API}/success", params={"run_id": run_id}, timeout=5)
    except Exception as e:
        requests.post(
            f"{API}/failure",
            params={"run_id": run_id},
            json={"payload": str(e)},
            timeout=5,
        )
        raise
Node.js
const API = "https://api.drumbeats.io/v1/ping/<monitor-id>";

async function runJob(jobId: string) {
  const runId = `job-${jobId}-${Date.now()}`;
  await fetch(`${API}/start?run_id=${runId}`);
  try {
    await doWork(jobId);
    await fetch(`${API}/success?run_id=${runId}`);
  } catch (err) {
    await fetch(`${API}/failure?run_id=${runId}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ payload: err instanceof Error ? err.message : String(err) }),
    });
    throw err;
  }
}
const API = "https://api.drumbeats.io/v1/ping/<monitor-id>";

async function runJob(jobId: string) {
  const runId = `job-${jobId}-${Date.now()}`;
  await fetch(`${API}/start?run_id=${runId}`);
  try {
    await doWork(jobId);
    await fetch(`${API}/success?run_id=${runId}`);
  } catch (err) {
    await fetch(`${API}/failure?run_id=${runId}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ payload: err instanceof Error ? err.message : String(err) }),
    });
    throw err;
  }
}
Go
package main

import (
    "fmt"
    "net/http"
    "time"
)

const api = "https://api.drumbeats.io/v1/ping/<monitor-id>"

func runJob(jobID string) error {
    runID := fmt.Sprintf("job-%s-%d", jobID, time.Now().UnixNano())
    client := &http.Client{Timeout: 5 * time.Second}

    resp, _ := client.Get(fmt.Sprintf("%s/start?run_id=%s", api, runID))
    if resp != nil {
        resp.Body.Close()
    }

    if err := doWork(jobID); err != nil {
        if r, _ := client.Get(fmt.Sprintf("%s/failure?run_id=%s", api, runID)); r != nil {
            r.Body.Close()
        }
        return err
    }

    if r, _ := client.Get(fmt.Sprintf("%s/success?run_id=%s", api, runID)); r != nil {
        r.Body.Close()
    }
    return nil
}
package main

import (
    "fmt"
    "net/http"
    "time"
)

const api = "https://api.drumbeats.io/v1/ping/<monitor-id>"

func runJob(jobID string) error {
    runID := fmt.Sprintf("job-%s-%d", jobID, time.Now().UnixNano())
    client := &http.Client{Timeout: 5 * time.Second}

    resp, _ := client.Get(fmt.Sprintf("%s/start?run_id=%s", api, runID))
    if resp != nil {
        resp.Body.Close()
    }

    if err := doWork(jobID); err != nil {
        if r, _ := client.Get(fmt.Sprintf("%s/failure?run_id=%s", api, runID)); r != nil {
            r.Body.Close()
        }
        return err
    }

    if r, _ := client.Get(fmt.Sprintf("%s/success?run_id=%s", api, runID)); r != nil {
        r.Body.Close()
    }
    return nil
}
PHP
$api = 'https://api.drumbeats.io/v1/ping/<monitor-id>';
$runId = 'job-' . uniqid('', true);

@file_get_contents("$api/start?run_id=$runId");
try {
    doWork();
    @file_get_contents("$api/success?run_id=$runId");
} catch (Throwable $e) {
    $ctx = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: application/json',
            'content' => json_encode(['payload' => $e->getMessage()]),
            'timeout' => 5,
        ],
    ]);
    @file_get_contents("$api/failure?run_id=$runId", false, $ctx);
    throw $e;
}
$api = 'https://api.drumbeats.io/v1/ping/<monitor-id>';
$runId = 'job-' . uniqid('', true);

@file_get_contents("$api/start?run_id=$runId");
try {
    doWork();
    @file_get_contents("$api/success?run_id=$runId");
} catch (Throwable $e) {
    $ctx = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: application/json',
            'content' => json_encode(['payload' => $e->getMessage()]),
            'timeout' => 5,
        ],
    ]);
    @file_get_contents("$api/failure?run_id=$runId", false, $ctx);
    throw $e;
}

Every example sets a timeout on the ping call. Without one, a stalled connection to Drumbeats holds your worker hostage.

Send the finish ping from finally#

An early return, a caught-then-rethrown exception, or a break out of a loop all skip the finish ping if it only sits on the happy path. Drumbeats then records the run as hung, and you get paged for a job that completed fine.

Put the finish in finally, defer, or a trap so it fires on every exit path.

Record progress between phases#

What happens when it breaks#

What Drumbeats seesResult
start then matching successRun recorded as successful, with duration
start then matching failureFailure counter increments. Monitor flips DOWN at failure_tolerance
start with no finish inside max_duration_secondsHung run, counted as a failure
success with no preceding startRecorded, but no duration for that run
Two start pings sharing one run_idOnly the first opens the run. Later ones sit on the timeline without double-counting
No pings at allNothing. Event-driven monitors have no schedule to be late against

How you get alerted#

The incident pages every notification group on the monitor. A later success resolves it, flips the monitor UP, and sends a recovery message with the outage duration.

One monitor per job, not per worker#

Five workers running process_order share one monitor. run_id keeps their runs apart on the timeline, and the alert logic cooldown keeps a partial outage from paging five times. Split into separate monitors only when one worker failing means something different from another worker failing.

Next#

Event-driven monitors for the monitor side. Payloads for attaching context. Production hardening for absorbing transient ping failures.