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, Kubernetes CronJobs.
  • 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 knowUse 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:

text
https://api.enori.io/ping/job_4f2a91c0e8b7426d9a1c5e3f80d6b7a2

Your 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:

PingURLMeaning
Success/ping/{token}The run finished successfully. This is the default and the simplest.
Success (explicit)/ping/{token}/okIdentical to the bare URL, just self-describing when pasted into shared docs.
Failure/ping/{token}/failThe run happened and went wrong.
Start/ping/{token}/startThe 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:

StateWhat it means
Up / HealthyThe last ping arrived on time, and it wasn't a failure.
LateThe deadline has passed but you're still inside the grace period. A warning state — no alert yet.
DownEither the grace period expired with no ping (overdue), or the job explicitly pinged /fail.
RunningA /start ping arrived and no result has followed yet.
PausedYou 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 /fail ping 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:

bash
curl -fsS -m 10 --retry 3 "https://api.enori.io/ping/YOUR_TOKEN" > /dev/null

The 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

bash
#!/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/null

The 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:

bash
./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

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,
    )
    raise

The traceback comes with the failure ping, so the reason is in Enori before you open a log file.

Node.js

javascript
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

yaml
# .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

yaml
# .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"
      fi

Store the token as a masked CI/CD variable.

Kubernetes CronJob

yaml
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
                  fi

In 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

bash
# /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)

powershell
$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

dockerfile
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 1

For 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.

bash
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-run
bash
enori-run --token YOUR_TOKEN -- ./my-cron-job.sh

Use $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:

FieldTypeWhat it does
statusstringok, success, fail, error, failure or start.
messagestringA short label, shown in run history. Truncated at 1000 characters.
executionTimeMsnumberHow long the run took. If you send /start pings, Enori computes this for you.
exitCodenumberThe process exit code. Non-zero makes the ping a failure.
itemsProcessednumberA simple count. Kept for compatibility — prefer metrics.
bodystringOutput tail — a stack trace, the last lines of stderr. Capped at 10 KB.
runIdstringGroups several pings into one logical run. Max 128 characters.
metricsobjectYour 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:

bash
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:

bash
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:

CellWhat it tells you
StatusHEALTHY, RUNNING, OVERDUE, FAILED, MAINTENANCE or PENDING (waiting for a first ping), with a one-line explanation underneath.
Last pingHow long ago the last ping arrived, and its exact timestamp.
ScheduleYour cron expression in plain English, plus "Next run in 4h 12m" — or "Overdue by …".
Avg durationThe average run time, with a slower/faster/stable trend hint.
Copy ping URLThe 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:

CardWhat it shows
SLO & drift · 7dSuccess rate, error budget, and drift — see below.
ScheduleYour 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.
AlertsWhich of the three alert toggles are on, and which channels are attached.
Last ping originThe IP address and user-agent of the last ping. Handy for confirming the ping came from the box you think it did.
Last failuresRecent failures with their messages.
Custom metricsYour own numbers, charted.
ConfigurationInterval, grace, max runtime, timezone — plus the Dead-Man toggle.
Maintenance, Status pages, Recent incidentsThe 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:

ToggleFires when
Job failsThe job ran and reported a failure — a /fail ping or a non-zero exit code.
Job is overdueThe job went silent — no ping by its deadline.
Job recoversA 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:

  1. Check the URL. Copy it fresh from Copy ping URL and compare character for character. A truncated token returns a 404 that a silent curl will happily swallow.
  2. 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. An ERR trap (see the Bash example) reports the failure instead of vanishing.
  3. Check outbound network access. A locked-down box or a CI runner without egress can't reach api.enori.io. Run the curl by hand from the same host.
  4. 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

SettingValue
Schedule typeCron Schedule (default) or Simple Interval
Cron expressionStandard 5-field, validated on save. Presets: every 5 min, every 15 min, hourly, daily 2 AM, daily 6 AM, weekly Sun, monthly 1st
TimezoneDefaults to your browser's timezone (cron mode only)
Interval presets5 min, 15 min, 1 hour, 6 hours, 1 day, 1 week, or custom — minimum 60 seconds
Default interval1 hour
Grace periodOn by default at 5 minutes. Range 1–60 minutes
Maximum runtimeOff 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 methodsGET or POST (HEAD too, except on /start)
Ping rate limit60 per minute per source IP
Ping request size15 KB; body capped at 10 KB and truncated beyond it
Custom metricsMax 20 keys per ping; keys ^[a-z][a-z0-9_]{0,47}$; finite numbers only
Run IDMax 128 characters; ?rid= wins over a runId in the body
MessageTruncated at 1000 characters
Failures before alertFirst failure, 2, 3 or 5 — default 2
Alert togglesJob fails · Job is overdue · Job recovers — all on for a new monitor
SLO window7 days, cron schedules only. Miss allowance fixed at 5% of expected runs
Check NowNot available — job monitors are driven by incoming pings
Monitors per planBase 10 · Pro 50 · Business 200
Run-history retentionBase 30 days · Pro 60 days · Business 90 days


Feedback or corrections: support@enori.io