Event-driven monitors

How Drumbeats watches on-demand jobs — queue workers, webhook handlers, ad-hoc scripts. Requires run_id correlation. No schedule.

An Event-driven monitor watches jobs that run on demand: queue workers, webhook handlers, manual scripts, CI jobs. Drumbeats does not enforce a schedule — it only reacts to the events your job sends.

Use Event-driven when there is no fixed cadence to fall behind on, only individual runs that succeed, fail, or hang.

How the monitor decides health

Each execution sends two events: a start ping when work begins and a success or failure ping when it ends. Drumbeats groups them by run_id (a unique string you generate per run) into a single run record. The monitor's health rolls up:

  • Latest run = success → monitor stays UP.
  • Latest run = failure (or hung past max_duration_seconds without a finish) → incident opens.
  • failure_tolerance consecutive failures → monitor flips to DOWN.

There is no MISSED state — an Event-driven monitor that no one pings is silent, not alerting.

Configuration

FieldRequiredExampleDescription
max_duration_seconds300If start is not followed by success / failure inside this window, Drumbeats records the run as hung and opens an incident.
failure_tolerance1Consecutive failures before flipping the monitor to DOWN.
nameOrder processorHuman-readable label shown in alerts and the dashboard.

The lack of a schedule field is intentional. Event-driven monitors do not catch a service that has stopped pinging entirely — pair them with an Uptime monitor on the worker's healthcheck endpoint if you also need "is the consumer alive?" coverage.

Why run_id is required

Event-driven monitors handle concurrent executions. Without a run_id, ten parallel runs of the same job each send start, then ten send success — Drumbeats cannot tell which success matches which start. Pass a unique run_id on every event and the same run_id survives start → log → success.

Wire pings into your worker

The shape is the same across stacks: generate run_id, send start, run the work, send success or failure with the same run_id.

Bash
RUN_ID="job-$(date +%s)-$RANDOM"
API="https://api.drumbeats.io/v1/ping/<monitor-id>"

curl -sf "$API/start?run_id=$RUN_ID"
if process_message; then
  curl -sf "$API/success?run_id=$RUN_ID"
else
  curl -sf "$API/failure?run_id=$RUN_ID"
fi
RUN_ID="job-$(date +%s)-$RANDOM"
API="https://api.drumbeats.io/v1/ping/<monitor-id>"

curl -sf "$API/start?run_id=$RUN_ID"
if process_message; then
  curl -sf "$API/success?run_id=$RUN_ID"
else
  curl -sf "$API/failure?run_id=$RUN_ID"
fi
Node.js
import { randomUUID } from "node:crypto";

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

async function handleMessage(message: QueueMessage) {
  const runId = `msg-${randomUUID()}`;
  await fetch(`${API}/start?run_id=${runId}`);
  try {
    await processMessage(message);
    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: String(err) }),
    });
    throw err;
  }
}
import { randomUUID } from "node:crypto";

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

async function handleMessage(message: QueueMessage) {
  const runId = `msg-${randomUUID()}`;
  await fetch(`${API}/start?run_id=${runId}`);
  try {
    await processMessage(message);
    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: String(err) }),
    });
    throw err;
  }
}
Python (queue worker)
import uuid, requests

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

def handle(message):
    run_id = f"msg-{uuid.uuid4()}"
    requests.get(f"{API}/start", params={"run_id": run_id})
    try:
        process(message)
        requests.get(f"{API}/success", params={"run_id": run_id})
    except Exception as exc:
        requests.post(
            f"{API}/failure",
            params={"run_id": run_id},
            json={"payload": str(exc)},
        )
        raise
import uuid, requests

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

def handle(message):
    run_id = f"msg-{uuid.uuid4()}"
    requests.get(f"{API}/start", params={"run_id": run_id})
    try:
        process(message)
        requests.get(f"{API}/success", params={"run_id": run_id})
    except Exception as exc:
        requests.post(
            f"{API}/failure",
            params={"run_id": run_id},
            json={"payload": str(exc)},
        )
        raise

Catching hung runs

Drumbeats opens an incident when a start is not followed by success / failure within max_duration_seconds. The check runs server-side, so a worker that has died mid-run still surfaces — you do not need a finalizer to detect the hang.

For long-running jobs with progress signals, send log pings while the work continues:

bash
curl -sf -X POST "$API/log?run_id=$RUN_ID" \
  -H "Content-Type: application/json" \
  -d '{"payload": "Processed batch 3 of 12"}'
curl -sf -X POST "$API/log?run_id=$RUN_ID" \
  -H "Content-Type: application/json" \
  -d '{"payload": "Processed batch 3 of 12"}'

log events are stored on the run and reset the inactivity timer (the equivalent of "still alive"). They count as beats; see Beats and usage.

Common patterns

Webhook handler

python
from flask import Flask, request
import requests, uuid

app = Flask(__name__)
API = "https://api.drumbeats.io/v1/ping/<monitor-id>"

@app.route("/webhook", methods=["POST"])
def webhook():
    run_id = f"wh-{uuid.uuid4()}"
    requests.get(f"{API}/start", params={"run_id": run_id})
    try:
        handle(request.json)
        requests.get(f"{API}/success", params={"run_id": run_id})
        return {"ok": True}
    except Exception as exc:
        requests.post(f"{API}/failure", params={"run_id": run_id}, json={"payload": str(exc)})
        return {"error": str(exc)}, 500
from flask import Flask, request
import requests, uuid

app = Flask(__name__)
API = "https://api.drumbeats.io/v1/ping/<monitor-id>"

@app.route("/webhook", methods=["POST"])
def webhook():
    run_id = f"wh-{uuid.uuid4()}"
    requests.get(f"{API}/start", params={"run_id": run_id})
    try:
        handle(request.json)
        requests.get(f"{API}/success", params={"run_id": run_id})
        return {"ok": True}
    except Exception as exc:
        requests.post(f"{API}/failure", params={"run_id": run_id}, json={"payload": str(exc)})
        return {"error": str(exc)}, 500

CI / one-off scripts

For scripts that run inside a CI pipeline, use the pipeline's run ID as run_id so the Drumbeats record links back to the pipeline:

The last call uses the exit-code endpoint: 0 becomes success, anything else becomes failure. See Ping API: exit codes.