Cron Job & Heartbeat Monitoring
Your backups, syncs and nightly reports don't fail loudly — they just stop running. A job monitor waits for each run to check in, and tells you when one doesn't.
What is a job monitor?
A job monitor (also called a heartbeat monitor, a cron monitor, or a dead man's switch) is the one monitor type that never checks anything. It sits and waits.
You give your cron job, scheduled task or background worker a unique URL. Every time the job runs, it sends a small HTTP request to that URL — a ping. Enori knows when the next ping is due, and if it doesn't arrive in time, or if the job pings to say it failed, you get alerted.
Example: "My nightly backup runs at 02:00. If it hasn't checked in by 02:05, wake me up."
This is the inverse of every other monitor. A website monitor asks "is it answering?". A job monitor asks "did it run at all?" — which is the only question that matters for work that has no endpoint to poll.
When to use one
- Cron jobs and scheduled tasks. Linux crontab, systemd timers, Windows Scheduled Tasks — anything on a clock.
- CI-driven scheduled work. GitHub Actions on a
schedule:trigger, GitLab CI scheduled pipelines, KubernetesCronJobs. - Background workers and queues. A nightly database cleanup, a report generator, a data sync, a cache warmer.
- Batch jobs that run in shards. Several parallel workers can report into one monitor as a single logical run — see run IDs.
- Anything where silence is the failure. A job that dies at startup, gets commented out of a crontab during a migration, or is running on a box that was decommissioned, produces no error anywhere. It just stops. This is the check that catches that.
When to use something else
| You want to know | Use this instead |
|---|---|
| "Is my website up and returning 200?" | Website monitor — it makes the request. A job monitor never initiates anything. |
| "Is my API healthy right now?" | Website monitor, or an API flow monitor for a multi-step sequence. |
| "Is this port / host reachable?" | Port monitor or a Ping monitor. |
| "How are all my jobs doing?" | The Jobs overview — one dashboard across every job you own. This page is about creating and configuring an individual one. |
A job monitor requires a change to your job. It cannot work by observation — your job has to make one HTTP call. If you can't modify the job, this monitor type isn't an option for it.
How it works
The ping
Each job monitor gets its own secret token and, from it, a ping URL:
https://api.enori.io/ping/job_4f2a91c0e8b7426d9a1c5e3f80d6b7a2Your job requests that URL when it runs. That's the whole protocol. A bare curl at the end of a crontab line is a complete, working integration.
There are four things a job can say, each its own URL:
| Ping | URL | Meaning |
|---|---|---|
| Success | /ping/{token} | The run finished successfully. This is the default and the simplest. |
| Success (explicit) | /ping/{token}/ok | Identical to the bare URL, just self-describing when pasted into shared docs. |
| Failure | /ping/{token}/fail | The run happened and went wrong. |
| Start | /ping/{token}/start | The run is beginning. Optional — it's what enables duration and stuck-job detection. |
| Exit code | /ping/{token}/{code} | 0 counts as success, anything else as failure. Built for shell: curl .../ping/$TOKEN/$?. |
Either GET or POST works on all of them (as does HEAD, except on /start). Use GET when you just need to say "it ran"; use POST when you want to attach details — an error message, a duration, custom numbers. Details are always optional.
What counts as Up, Late and Down
Enori computes an expected-by time from your schedule, then adds your grace period. That combined deadline is what everything hangs off:
| State | What it means |
|---|---|
| Up / Healthy | The last ping arrived on time, and it wasn't a failure. |
| Late | The deadline has passed but you're still inside the grace period. A warning state — no alert yet. |
| Down | Either the grace period expired with no ping (overdue), or the job explicitly pinged /fail. |
| Running | A /start ping arrived and no result has followed yet. |
| Paused | You paused the monitor. Enori expects nothing and alerts on nothing. |
Two separate things make a job Down, and they're worth keeping straight because you can alert on them independently:
- It went silent — no ping by the deadline. This is overdue, and it's what a dead man's switch is for.
- It reported a failure — it ran, it knew it went wrong, it told you. This is a
/failping or a non-zero exit code.
A new monitor doesn't go Down the moment you create it. Enori waits one full interval plus your grace period for the first heartbeat before it starts complaining — so a monitor for a nightly job created at lunchtime stays quiet until the night's run is genuinely late.
The stuck-job case
If your job sends /start but never sends a result, Enori eventually declares it timed out — a job that hung, deadlocked, or was killed without a chance to report.
This happens whether or not you set a maximum runtime. If you set one, that's the limit. If you don't, the default is twice your interval. Once a run times out, the monitor stays Down without re-alerting until a fresh ping arrives.
Note the trade: if your job never sends /start, none of this applies. Stuck-run detection is the thing you're buying with the extra ping.
One thing that doesn't apply
There's no Check Now for a job monitor — the button is disabled and reads "Job monitors rely on incoming pings". Enori can't make your job run. To prove your setup works, send a ping yourself (the detail page has a one-click Test from this browser button for exactly this).
Setting one up
Go to Monitors, click Add Monitor, and pick the Cron Jobs card. The wizard has two steps — Type and Configure — and everything below is on the Configure step.
Screenshot: the monitor-type picker with the Cron Jobs card selected.
1. Monitor Name (required)
What you'll want to read in an alert at 3am. e.g. "Nightly DB backup".
2. Group (optional)
Bundle related monitors together, or leave it ungrouped.
3. Schedule Type
Two modes, and the choice matters:
- Cron Schedule (the default) — you give a real cron expression. Enori computes each expected fire time from the actual schedule, so a job at
0 2 *is expected at 02:00 sharp. This mode also unlocks the schedule preview, drift measurement and the job SLO. - Simple Interval — you just say "every hour". Enori expects the next ping one interval after the last one it received. Simpler, and right for a worker that loops rather than one that's scheduled.
Pick cron if your job runs on a clock. Pick interval if it runs in a loop.
4a. Cron mode — Cron Expression (required)
A standard 5-field cron expression: minute hour day month weekday. There are one-click presets above the field — Every 5 min, Every 15 min, Hourly, Daily 2 AM, Daily 6 AM, Weekly Sun, Monthly 1st — and the field itself accepts anything valid. The help text links to crontab.guru if you want to check one.
The expression is validated when you save. An expression Enori can't parse is rejected with "Invalid CronExpression format" rather than saved as a monitor that could never fire.
Use the same expression your scheduler uses. If your crontab says 0 2 , put 0 2 here — the two must agree or every run will look early or late.
4b. Cron mode — Timezone
Defaults to your browser's timezone, and this is the field most worth pausing on. Your cron expression is interpreted in this zone.
If your server's crontab runs in UTC and you leave this set to, say, Europe/Sofia, then a 0 2 job will run at 02:00 UTC but be expected* at 02:00 local — and you'll get a false overdue alert every night, drifting twice a year with daylight saving. Set this to whatever zone your scheduler actually uses.
4c. Interval mode — How often does your job run? (required)
Presets: Every 5 min, Every 15 min, Every hour, Every 6 hours, Every day, Every week, plus Custom — a number and a unit (Minutes / Hours / Days). The minimum is 60 seconds.
If you don't touch this, a new job monitor defaults to 1 hour.
5. Add grace period
On by default, at 5 minutes. Range 1 to 60 minutes.
This is slack between "the run is late" and "you get paged". Cron jobs don't start at the exact second — a loaded box, a slow container pull, or a job that queues behind another will all push the start out. The grace period absorbs that.
Turning the toggle off means Enori alerts the instant a ping is late. The form warns you about this directly: "Alerts immediately when late (may cause false alerts)." It's the right setting only for a job whose timing you genuinely trust to the second.
Rule of thumb: set it a little longer than the worst start delay you've actually seen. Five minutes covers most jobs; a heavy job on a shared box may want fifteen.
6. Maximum runtime
Off by default. When on, choose from: 1, 2, 5, 10, 15 or 30 minutes, 1 hour, or 2 hours.
If your job sends a /start ping and hasn't reported a result within this time, Enori calls it timed out and raises it as Down. As noted above, leaving this off doesn't disable stuck-run detection — it falls back to twice your interval.
Only meaningful if you send /start pings. Without them there's no run to time.
7. Description (optional)
A note to yourself — "Nightly database backup at 2 AM UTC". It appears next to the job on the Jobs overview timeline.
8. Alert settings
Below the divider on the same step:
- When to alert — consecutive failures before you're notified. Defaults to 2, which absorbs a single bad night. The monitor still shows Down from the first failure; this only gates the alert.
- Notify me via — which alert channels to use. Your email channel is pre-selected if you have one.
- Repeat Alerts — whether to keep reminding you while it stays Down.
New job monitors are created with failure, overdue and recovery alerts all switched on. You can turn individual ones off afterwards in Edit (see Alerts below).
Click Create Monitor. Enori issues the ping token immediately, and the detail page opens on a Send your first heartbeat panel with your URL ready to copy.
Screenshot: the Cron Jobs Configure step with a cron expression, timezone and grace period filled in.
Wiring it into your job
Open the monitor and go to the Integration tab. It carries ready-made snippets for 13 environments — with your token already in them — and a copy button on each. The examples below are those snippets.
The Copy ping URL button in the header strip is the faster route when you just need one URL: it drops down all four endpoints (Success, Success /ok, Start, Fail) with a one-line explanation of each.
The one-liner
Enough for most jobs. Append it to the command in your crontab:
curl -fsS -m 10 --retry 3 "https://api.enori.io/ping/YOUR_TOKEN" > /dev/nullThe flags earn their keep: -f fails on an HTTP error instead of silently succeeding, -s keeps cron from emailing you curl's progress meter, -m 10 stops a network problem from hanging your job, and --retry 3 rides out a blip.
Bash, with start and failure reporting
#!/usr/bin/env bash
set -Eeuo pipefail
TOKEN="YOUR_TOKEN"
BASE="https://api.enori.io"
curl -fsS -m 10 "$BASE/ping/$TOKEN/start" > /dev/null
trap 'curl -fsS -m 10 "$BASE/ping/$TOKEN/fail" > /dev/null' ERR
# your job here
./do-work.sh
curl -fsS -m 10 "$BASE/ping/$TOKEN" > /dev/nullThe ERR trap is what makes this reliable — any failing command reports the failure, without you remembering to handle each one.
The shell exit-code shortcut
If you'd rather not write a trap, let the exit code speak:
./do-work.sh; curl -fsS -m 10 "https://api.enori.io/ping/YOUR_TOKEN/$?"0 records a success, anything else a failure — and the code itself is stored and shown in your run history.
Python
import traceback, requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.enori.io"
requests.get(f"{BASE}/ping/{TOKEN}/start", timeout=5)
try:
do_work()
requests.get(f"{BASE}/ping/{TOKEN}", timeout=5)
except Exception:
requests.post(
f"{BASE}/ping/{TOKEN}/fail",
json={"body": traceback.format_exc()[:10000]},
timeout=5,
)
raiseThe traceback comes with the failure ping, so the reason is in Enori before you open a log file.
Node.js
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.enori.io";
const ping = (path, body) =>
fetch(`${BASE}/ping/${TOKEN}${path}`, {
method: body ? "POST" : "GET",
headers: body ? { "Content-Type": "application/json" } : {},
body: body ? JSON.stringify(body) : undefined,
}).catch(() => {});
await ping("/start");
try {
await doWork();
await ping("");
} catch (err) {
await ping("/fail", { body: err.stack?.slice(0, 10000) });
throw err;
}Node 18+ for the built-in fetch. Note the .catch(() => {}) — a failed ping must never crash the job it's monitoring.
GitHub Actions
# .github/workflows/nightly-job.yml
jobs:
run:
runs-on: ubuntu-latest
steps:
- name: Start ping
run: curl -fsS -m 10 "https://api.enori.io/ping/${{ secrets.ENORI_JOB_TOKEN }}/start"
- name: Do work
run: ./do-work.sh
- name: Success ping
if: success()
run: curl -fsS -m 10 "https://api.enori.io/ping/${{ secrets.ENORI_JOB_TOKEN }}"
- name: Failure ping
if: failure()
run: curl -fsS -m 10 "https://api.enori.io/ping/${{ secrets.ENORI_JOB_TOKEN }}/fail"Store the token as a repository secret named ENORI_JOB_TOKEN. The if: success() / if: failure() conditions are what make the two outcomes distinct.
GitLab CI
# .gitlab-ci.yml
nightly-job:
before_script:
- curl -fsS -m 10 "https://api.enori.io/ping/$ENORI_JOB_TOKEN/start"
script:
- ./do-work.sh
after_script:
- |
if [ "$CI_JOB_STATUS" = "success" ]; then
curl -fsS -m 10 "https://api.enori.io/ping/$ENORI_JOB_TOKEN"
else
curl -fsS -m 10 "https://api.enori.io/ping/$ENORI_JOB_TOKEN/fail"
fiStore the token as a masked CI/CD variable.
Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-job
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: job
image: your-image:latest
command:
- /bin/sh
- -c
- |
set -e
TOKEN="YOUR_TOKEN"
BASE="https://api.enori.io"
curl -fsS -m 10 "$BASE/ping/$TOKEN/start" || true
if ./do-work.sh; then
curl -fsS -m 10 "$BASE/ping/$TOKEN" || true
else
curl -fsS -m 10 "$BASE/ping/$TOKEN/fail" || true
exit 1
fiIn a real deployment, inject TOKEN and BASE from a secret with envFrom rather than baking them into the manifest. The || true on each ping keeps a ping failure from changing the pod's exit status.
systemd
# /etc/systemd/system/nightly-job.service
[Unit]
Description=Nightly job with Enori ping
[Service]
Type=oneshot
ExecStart=/usr/local/bin/do-work.sh
ExecStartPost=/usr/bin/curl -fsS -m 10 "https://api.enori.io/ping/YOUR_TOKEN"
OnFailure=nightly-job-fail.service
# /etc/systemd/system/nightly-job-fail.service
[Unit]
Description=Report nightly job failure to Enori
[Service]
Type=oneshot
ExecStart=/usr/bin/curl -fsS -m 10 "https://api.enori.io/ping/YOUR_TOKEN/fail"ExecStartPost runs only on success; OnFailure= triggers the companion unit when the main one exits non-zero. Two units, both outcomes covered.
PowerShell (Windows Scheduled Tasks, Azure Automation)
$ErrorActionPreference = "Stop"
$Token = "YOUR_TOKEN"
$Base = "https://api.enori.io"
try {
Invoke-WebRequest -UseBasicParsing -Uri "$Base/ping/$Token/start" -TimeoutSec 10 | Out-Null
& .\do-work.ps1
Invoke-WebRequest -UseBasicParsing -Uri "$Base/ping/$Token" -TimeoutSec 10 | Out-Null
}
catch {
$body = @{ body = ($_ | Out-String).Substring(0, [Math]::Min(10000, ($_ | Out-String).Length)) } | ConvertTo-Json
Invoke-WebRequest -UseBasicParsing -Uri "$Base/ping/$Token/fail" -Method POST -Body $body -ContentType "application/json" -TimeoutSec 10 | Out-Null
throw
}Docker HEALTHCHECK
FROM your-base-image
# ... build steps ...
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -fsS -m 10 "https://api.enori.io/ping/YOUR_TOKEN" || exit 1For heartbeat-style monitoring of a long-running container, not one-shot job runs.
The enori-run wrapper
If you'd rather not touch the job's code at all, wrap it. enori-run is a small dependency-free bash script that sends the start ping, runs your command, and reports success or failure with the duration and output tail attached — including on SIGTERM/SIGINT.
curl -sSL https://github.com/hpatsev/enori/releases/latest/download/enori-run.sh \
-o /usr/local/bin/enori-run && chmod +x /usr/local/bin/enori-runenori-run --token YOUR_TOKEN -- ./my-cron-job.shUse $HOME/.local/bin instead if you don't have root. The download link is on the Integration tab.
Sending more than "it ran"
Every ping can carry a JSON body (POST, Content-Type: application/json). Every field is optional:
| Field | Type | What it does |
|---|---|---|
status | string | ok, success, fail, error, failure or start. |
message | string | A short label, shown in run history. Truncated at 1000 characters. |
executionTimeMs | number | How long the run took. If you send /start pings, Enori computes this for you. |
exitCode | number | The process exit code. Non-zero makes the ping a failure. |
itemsProcessed | number | A simple count. Kept for compatibility — prefer metrics. |
body | string | Output tail — a stack trace, the last lines of stderr. Capped at 10 KB. |
runId | string | Groups several pings into one logical run. Max 128 characters. |
metrics | object | Your own numbers. See below. |
Two rules worth knowing. The URL wins over the body: a POST to /fail carrying {"status":"ok"} is recorded as a failure, always. And an unrecognised status is treated as a failure rather than silently ignored — Enori would rather over-report than swallow a run it can't classify.
Whole requests are capped at 15 KB; a body over 10 KB is truncated and marked as such rather than rejected.
Don't put secrets in
body. It's stored as plain text and shown in the UI. Connection strings and API keys have a habit of appearing in stack traces — the Integration tab carries the same warning.
Custom metrics
Attach numbers your job knows about and Enori will chart them over time:
curl -fsS -m 10 -X POST "https://api.enori.io/ping/YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"metrics":{"orders_processed":1482,"queue_depth":3}}'Rules: up to 20 keys per ping, each key matching ^[a-z][a-z0-9_]{0,47}$ (lowercase, starts with a letter, underscores fine, max 48 characters), and every value a finite number. A ping that breaks these rules is rejected with a 400 explaining which key was wrong — so a typo tells you immediately rather than silently dropping data.
Metrics appear in the Custom metrics card on the detail page, with a picker when you're recording more than one.
Grouping a sharded run with a run ID
When one logical run is executed by several workers, tag every ping with the same run ID and Enori collapses them into a single run:
RID="nightly-$(date +%Y%m%d-%H%M%S)"
curl -fsS -m 10 "https://api.enori.io/ping/$TOKEN/start?rid=$RID"
./shard-1.sh & ./shard-2.sh & wait
curl -fsS -m 10 "https://api.enori.io/ping/$TOKEN?rid=$RID"?rid= on the URL takes precedence over a runId in the body. Run IDs show in the run history and the recent-pings table, so you can see which shard did what.
Dead-Man mode
On the detail page, the Configuration card in the right rail has a Dead-Man mode toggle.
Normally a job monitor watches two things: did it check in, and did it report success. Dead-Man mode drops the second. Only a missed heartbeat matters — a /fail ping stops being an alerting event, and /start pings are ignored entirely (so there's no duration or stuck-run tracking).
Use it when the only signal you trust is silence: "as long as something arrives, I'm happy; if nothing arrives, something is badly wrong." A device heartbeat, a watchdog, a canary process. For a job with real success and failure outcomes, leave it off — you'd be throwing away half your signal.
The health banner changes wording to match: an overdue job in Dead-Man mode reads "Dead Man — heartbeat missed".
Reading the results
The job detail page opens on Overview, with Calendar and Integration tabs alongside.
The header strip
Five cells across the top:
| Cell | What it tells you |
|---|---|
| Status | HEALTHY, RUNNING, OVERDUE, FAILED, MAINTENANCE or PENDING (waiting for a first ping), with a one-line explanation underneath. |
| Last ping | How long ago the last ping arrived, and its exact timestamp. |
| Schedule | Your cron expression in plain English, plus "Next run in 4h 12m" — or "Overdue by …". |
| Avg duration | The average run time, with a slower/faster/stable trend hint. |
| Copy ping URL | The dropdown with all four endpoints. |
Under it, a health banner appears only when something needs attention, showing the single most serious issue: heartbeat overdue, last ping failed, error budget burning fast, or a run taking unusually long.
Run history
The main panel, with 24H / 7D / 30D / 90D range buttons.
At 24H each bar is one run: height is how long it took, colour is how it ended — green for success, amber for a run over twice your average, red for a failure. Hover any bar for its exact time, duration, exit code and run ID. Below the chart, the last five runs are listed with their messages.
At 7D and longer each tile is one day, coloured by that day's success rate (green ≥99%, amber 95–99%, red below 95%). Hovering gives the counts and the day's average and maximum duration.
Two things to know. Daily tiles are rebuilt hourly, so today's tile can be up to an hour behind — the panel says so, and points you at 24H for live data. And if your bars are flat, you're not sending durations: send a /start ping before each run, or put executionTimeMs in the body of the final ping.
Recent pings
Every ping as it arrived — OK, FAIL or START, with its timestamp, run ID, message and duration. A ping that carried a body can be expanded in place to read the stored output tail.
The Calendar tab
A month, week or day view of runs, plus:
- A summary bar — Executions, Success Rate, Avg Duration, and P95 Duration with a "Getting slower" / "Getting faster" trend. Counts for Overdue, Timeout, Slow and Overlaps appear when there are any.
- An overlap warning — "N job overlaps detected in this period", naming which jobs ran at the same time. Useful for finding two heavy jobs quietly colliding at 02:00 every night.
- A 30-day duration trend chart.
- Export — the period's data as JSON or CSV.
The right rail
Drag-reorderable cards, and Reset rail order puts them back:
| Card | What it shows |
|---|---|
| SLO & drift · 7d | Success rate, error budget, and drift — see below. |
| Schedule | Your cron in plain English, the raw expression, the timezone, and the next three fire times. The fastest way to confirm a cron expression means what you think. |
| Alerts | Which of the three alert toggles are on, and which channels are attached. |
| Last ping origin | The IP address and user-agent of the last ping. Handy for confirming the ping came from the box you think it did. |
| Last failures | Recent failures with their messages. |
| Custom metrics | Your own numbers, charted. |
| Configuration | Interval, grace, max runtime, timezone — plus the Dead-Man toggle. |
| Maintenance, Status pages, Recent incidents | The usual per-monitor cards. |
SLO and drift
For cron-scheduled monitors only (an interval schedule has no fixed fire times to measure against), Enori keeps a rolling 7-day picture:
- Success rate — successful runs as a share of all runs.
- Error budget — misses used against misses allowed. The allowance is 5% of expected runs, and is not configurable today.
- Drift — how far each run's actual start lands from its scheduled time. The card shows the P95, and expands into a histogram from early through on-time to late.
Drift is the early-warning signal for a scheduler under strain. A job that has quietly slipped from starting at 02:00:03 to starting at 02:04:30 hasn't failed anything yet — but it's telling you something.
The card waits 24 hours after you create a monitor before showing numbers ("Computing baseline…"), because a 7-day window on a one-hour-old monitor would count every fire time from before it existed as a miss.
Runs falling inside a maintenance window are excluded from the SLO entirely — neither a success nor a miss — so announced downtime doesn't burn your budget.
Alerts
Job monitors use Enori's normal alerting — channels, escalation policies, on-call rotas. The Alerts guide covers all of that. What's specific here is what you can alert on.
Open the monitor, click Edit, and under Alert Settings you'll find three independent toggles:
| Toggle | Fires when |
|---|---|
| Job fails | The job ran and reported a failure — a /fail ping or a non-zero exit code. |
| Job is overdue | The job went silent — no ping by its deadline. |
| Job recovers | A successful heartbeat arrives after a problem. |
These are genuinely separate. A job that always reports its own errors properly might only need overdue — you already have failure handling. A dead man's switch needs overdue and nothing else. A job you want to hear about either way keeps both.
Failure tolerance sits below them: First failure (immediate), 2, 3 or 5 consecutive failures. It applies to both reported failures and missed runs. The monitor's status still flips to Down on the first one; the tolerance only delays the alert and the incident. Raise it for a job that's known to be flaky in a way you can live with.
Repeat alerts and escalation policy are configured in Alert Settings on the detail page, not in the edit modal.
When a job goes Down, Enori opens an incident you can acknowledge and resolve, and pushes a notification to the bell as well as your channels.
Maintenance windows
During an active maintenance window, a job monitor stops alerting — both for missed pings and for reported failures. Its status shows Maintenance instead of Down, no incident is opened, and the runs are excluded from your uptime and SLO numbers.
This is the right tool for a night when you know the job won't run — a migration, a scheduler restart, a planned freeze. Without it, a job you deliberately skipped will page you on schedule.
Pausing
Pausing a job monitor (the ⋯ menu → Pause) stops Enori expecting heartbeats at all — no overdue detection, no alerts.
Pings sent to a paused monitor are accepted but ignored: the request succeeds with a status of ignored, and nothing is recorded. Your job won't start erroring because you paused its monitor, but nothing shows up in the history either. Resume to start expecting pings again.
Changing a monitor later
Open the monitor and click Edit (or the Edit › pill on any rail card). You can change: name, group, tags, schedule type, cron expression, timezone, interval, grace period, maximum runtime, description, the three alert toggles, failure tolerance, and alert channels.
Changing the schedule recomputes the next expected ping immediately, so a corrected cron expression or timezone takes effect at once rather than after the next run.
Dead-Man mode is toggled on the detail page's Configuration card, not in this modal.
FAQ
Do I have to send a /start ping?
No. A single success ping per run is a complete integration. /start buys you three things: run durations, the Running state, and stuck-job detection. If any of those matter, send it.
My job takes longer than its interval. Is that a problem?
It's the case /start exists for. Send /start when the run begins and Enori holds the deadline open while it's running rather than declaring it overdue mid-run. Also set Maximum runtime to something above its realistic worst case, otherwise the default of twice the interval may cut in first.
What happens if my job runs twice, or a ping is sent twice?
Each ping is recorded. Two success pings look like two successful runs. If that's a sharded job where several workers each ping, give them all the same run ID and they'll collapse into one run.
Is there a limit on how often I can ping?
Yes — 60 pings per minute per source IP. Comfortable for any job schedule, including a fleet of jobs pinging from one host. It exists to stop a runaway loop, not to constrain normal use.
Can I use GET? My environment can't easily send a POST.
Yes. Every endpoint works with GET, which is why the curl one-liner is the most common integration. You only need POST to attach a body, metrics or a duration.
Why is my job Late every night by a few minutes?
Almost always the grace period being shorter than the job's real start jitter. Check the drift histogram on the SLO card — if runs cluster a few minutes late, raise the grace period to match reality. If the drift is a consistent whole number of hours, it's the timezone (see below).
My job runs fine but Enori says it's overdue, every day at the same offset.
A timezone mismatch. The Timezone field defaults to the browser you created the monitor in, which is often not the zone your server's scheduler uses. Compare the next three fire times on the Schedule rail card against when your job actually runs — if they're offset by a fixed number of hours, correct the timezone in Edit.
Can I monitor a job that doesn't run on a fixed schedule?
Use Simple Interval and set it to the longest gap you'd consider acceptable, plus grace. Enori will complain only when the gap exceeds it. For a genuinely irregular job, a job monitor's value drops — it has to know when to expect something.
Does a missed run count against my uptime?
Yes. Missed and failed runs count as downtime and flow into SLOs and reports like any other monitor. Runs inside a maintenance window don't.
How long is my run history kept?
By plan: 30 days on Base, 60 on Pro, 90 on Business. Daily rollups for the calendar are kept longer.
Is my ping URL secret?
Treat it as a credential. Anyone holding the token can post heartbeats for your job — including a false success that masks a real failure. Keep it in your secret store (a repository secret, a masked CI variable, a Kubernetes secret), not in a committed file. Teammates with view-only access to a shared monitor don't see the token.
Can I manage job monitors through the API?
Yes — see the API reference and the MCP server. The ping endpoints themselves need no API key; the token in the URL is the credential.
Troubleshooting
The monitor is Down but I know the job ran
The job ran; the ping didn't arrive. In order of likelihood:
- Check the URL. Copy it fresh from Copy ping URL and compare character for character. A truncated token returns a 404 that a silent
curlwill happily swallow. - Check the ping isn't being skipped. In a shell script with
set -e, any earlier failing command exits before the ping line is reached. That's often correct behaviour — but it means "no ping" and "job failed" look identical. AnERRtrap (see the Bash example) reports the failure instead of vanishing. - Check outbound network access. A locked-down box or a CI runner without egress can't reach
api.enori.io. Run thecurlby hand from the same host. - Look at Last ping origin. If it shows an IP or timestamp you don't recognise, something is pinging that isn't the job you think.
I get an overdue alert at the same time every day
Timezone or grace period. See the two FAQ entries above — the next three fire times on the Schedule card against your job's actual start time will tell you which.
The monitor says "No execution-time data captured"
You're not sending durations. Either send /start before each run and let Enori compute it, or include executionTimeMs in the final ping's body. Without one of those there's nothing to chart.
A run is stuck in "Running" and never finishes
A /start arrived and no result followed — the job hung, crashed hard, or was killed before it could report. Enori will time it out (at your maximum runtime, or twice the interval if you haven't set one) and mark it Down. It then stays Down without re-alerting until a new ping arrives, so you get one alert rather than one per check.
If this happens often, the fix is in the job: make sure the failure path pings too. A trap ... ERR in bash, try/finally in Python or Node, OnFailure= in systemd.
My ping returns 400
The payload broke a rule. The response says which: a metric key that isn't lowercase-with-underscores, more than 20 metric keys, a non-finite number, or a run ID over 128 characters. Fix the field named in the message.
My ping returns 404
The token doesn't match a monitor. Either it's mistyped/truncated, or the monitor was deleted. Copy the URL again from the detail page.
Nothing arrives, and a manual curl works fine
The gap is between your job and the shell you tested in. Check that the job runs as the user you think, that curl is on that user's PATH (cron's PATH is famously minimal — use the absolute /usr/bin/curl), and that the crontab line isn't swallowing the command after an unescaped %, which cron treats as a newline.
The success rate looks wrong on a brand-new monitor
Give it 24 hours. The SLO window is seven days long, so on a fresh monitor it would otherwise count fire times from before the monitor existed as misses. The card says "Computing baseline…" until it has enough history.
Reference: limits and defaults
| Setting | Value |
|---|---|
| Schedule type | Cron Schedule (default) or Simple Interval |
| Cron expression | Standard 5-field, validated on save. Presets: every 5 min, every 15 min, hourly, daily 2 AM, daily 6 AM, weekly Sun, monthly 1st |
| Timezone | Defaults to your browser's timezone (cron mode only) |
| Interval presets | 5 min, 15 min, 1 hour, 6 hours, 1 day, 1 week, or custom — minimum 60 seconds |
| Default interval | 1 hour |
| Grace period | On by default at 5 minutes. Range 1–60 minutes |
| Maximum runtime | Off by default. 1m, 2m, 5m, 10m, 15m, 30m, 1h, 2h. When off, stuck runs time out at 2× the interval |
| Ping endpoints | /ping/{token}, /ping/{token}/ok, /ping/{token}/fail, /ping/{token}/start, /ping/{token}/{exitCode} |
| Ping methods | GET or POST (HEAD too, except on /start) |
| Ping rate limit | 60 per minute per source IP |
| Ping request size | 15 KB; body capped at 10 KB and truncated beyond it |
| Custom metrics | Max 20 keys per ping; keys ^[a-z][a-z0-9_]{0,47}$; finite numbers only |
| Run ID | Max 128 characters; ?rid= wins over a runId in the body |
| Message | Truncated at 1000 characters |
| Failures before alert | First failure, 2, 3 or 5 — default 2 |
| Alert toggles | Job fails · Job is overdue · Job recovers — all on for a new monitor |
| SLO window | 7 days, cron schedules only. Miss allowance fixed at 5% of expected runs |
| Check Now | Not available — job monitors are driven by incoming pings |
| Monitors per plan | Base 10 · Pro 50 · Business 200 |
| Run-history retention | Base 30 days · Pro 60 days · Business 90 days |
Related documentation
- Jobs overview — every job you own on one dashboard, with a run calendar and timeline
- Alerts — channels, escalation policies and on-call rotas
- Maintenance windows — suppress alerts for a night the job won't run
- SLOs — reliability targets and error budgets
- Uptime reports — exportable uptime and SLA reports
- Notifications — the bell and the notification centre
- API reference and MCP server — programmatic management
Feedback or corrections: support@enori.io