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:
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"
fiRUN_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"
fiThese 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.
| Strategy | Example | Use when |
|---|---|---|
| UUID | job-$(uuidgen) | Anything concurrent. The safe default |
| Domain identifier | order-${order.id} | You want real order or ticket numbers on the timeline |
| Timestamp plus random | job-$(date +%s)-$RANDOM | Cheap uniqueness with no uuidgen available |
| Timestamp alone | job-$(date +%s) | Single-process scripts only. Collides under concurrency |
Wire it into your language#
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,
)
raiseimport 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,
)
raiseconst 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;
}
}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
}$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 sees | Result |
|---|---|
start then matching success | Run recorded as successful, with duration |
start then matching failure | Failure counter increments. Monitor flips DOWN at failure_tolerance |
start with no finish inside max_duration_seconds | Hung run, counted as a failure |
success with no preceding start | Recorded, but no duration for that run |
Two start pings sharing one run_id | Only the first opens the run. Later ones sit on the timeline without double-counting |
| No pings at all | Nothing. 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.