Pulse Monitoring
Pulse checks implement a dead-man's-switch pattern: your service pings Emit Vision on a schedule, and if pings stop arriving for longer than expected, the check transitions through late → down and fires an alert.
This replaces external services like healthchecks.io for all fleet projects except emit-vision itself (see Watchdog policy below).
How it works
Each pulse check has five statuses:
| Status | Meaning |
|---|---|
| new | Created but hasn't received its first ping yet |
| up | Receiving pings on schedule |
| late | Overdue — past expected interval but within grace period |
| down | Missed deadline — alert fires |
| paused | Monitoring suspended, pings accepted silently |
A check fires a down alert once when transitioning to down, and a recovery alert once when it returns to up. No repeated alerts while the status is stable.
Creating a check
- Open your project in the Emit Vision dashboard
- Navigate to Pulse in the sidebar
- Click New Check
- Set the name (e.g., "backup-cron"), expected interval (how often your service pings), and grace period (extra time before alerting)
- Optionally configure a notification channel (Slack, webhook, email, PagerDuty)
- Click Create and copy the ping URL shown
Ping URL format
GET https://api.emitvision.com/v1/pulse/<slug>
Authorization: Bearer <your-ingest-key>
Both GET and POST are supported. The GET method works with minimal tools like wget or curl in sidecar containers.
Docker Compose sidecar
Add this service to your docker-compose.prod.yml as a shared (un-profiled) service — not under a blue/green slot profile, so exactly one instance pings regardless of active slot:
services:
pulse-ping:
image: alpine:3.20
restart: unless-stopped
environment:
- EMIT_VISION_INGEST_KEY=${EMIT_VISION_INGEST_KEY}
- PULSE_URL=https://api.emitvision.com/v1/pulse/my-check
- PING_INTERVAL=300
entrypoint: /bin/sh
command:
- -c
- |
while true; do
wget -q --header="Authorization: Bearer $${EMIT_VISION_INGEST_KEY}" \
-O /dev/null "$$PULSE_URL" || true
sleep $$PING_INTERVAL
doneReplace my-check with your check's slug and set PING_INTERVAL to match your check's expected interval in seconds.
In blue-green deployments, place this service outside any slot profile (like you would postgres or redis). If it runs inside a slot, you'll get double-pings from both slots or silence after a switch.
Explicit failure signaling
Use the /fail endpoint to immediately mark a check as down — useful for crash loops or failed cron jobs:
# Cron job pattern: ping on success, fail on error
./backup.sh && wget -qO /dev/null "$PULSE_URL" \
|| wget -qO /dev/null "$PULSE_URL/fail"This transitions the check to down instantly without waiting for the silence timeout.
Cron job monitoring
Pulse works for periodic jobs too. Set the expected interval to your cron schedule's period plus some slack:
- Cron runs every 5 minutes → set interval to 6 minutes, grace to 2 minutes
- Cron runs hourly → set interval to 65 minutes, grace to 10 minutes
Ping at the end of each successful run. If the job hangs or crashes, the silence triggers the alert.
Migrating from healthchecks.io
For each project with an existing uptime-ping or dms-ping sidecar:
- Create a check in the Emit Vision dashboard matching your current ping interval
- Update the sidecar in
docker-compose.prod.yml:- Change the URL from
https://hc-ping.com/...tohttps://api.emitvision.com/v1/pulse/<slug> - Add the auth header:
--header="Authorization: Bearer $EMIT_VISION_INGEST_KEY"
- Change the URL from
- Deploy and verify the check status flips to
upin the Pulse dashboard - Delete the healthchecks.io check once confirmed
- Remove the
HEALTHCHECKS_IO_UUIDenv var from your compose file
This removes healthchecks.io as a provisioning dependency for the project.
Watchdog-of-the-watchdog
Emit Vision hosting the fleet's dead-man's switch creates a circular dependency: if emit-vision goes down, all silence detection stops.
Policy: Emit Vision itself keeps one external healthchecks.io check (its existing sidecar stays pointed there). Every other project migrates to pulse. This is a deliberate architectural exception — not an oversight.
Do not migrate emit-vision's own uptime sidecar to pulse.