# Better Stack Heartbeats vs a Dedicated Cron Monitor: Period, Grace, Pricing

By Dan Ochoa · Published 2026-10-30 · Updated 2026-10-31
Canonical: https://cronradar.com/blog/better-stack-heartbeats

> How Better Stack heartbeats work: period and grace in seconds, the secret URL and /fail, no cron expressions, pricing, and how CronRadar compares.

**A Better Stack heartbeat is a secret URL your job requests on each run; when no request arrives within the heartbeat's period plus its grace period, Better Stack opens an incident and alerts the on-call team.** The period is a number of seconds, not a cron expression, and a job reports a failure by appending `/fail` or its exit code to the URL. A dedicated cron monitor such as CronRadar reads the job's cron schedule instead and accepts start, complete, and fail check-ins with optional outcome values; Better Stack is the better fit when heartbeats should page the same on-call rotation as the rest of your incidents.

This guide covers the Better Stack Terraform provider 0.22.4, the current release on the [Terraform Registry](https://registry.terraform.io/providers/BetterStackHQ/better-uptime/0.22.4) (checked October 30, 2026), and Better Stack's hosted heartbeats as its documentation, API reference, and pricing page described them on that date. It compares heartbeat features only, not Better Stack's uptime checks, status pages, or telemetry products. For other options, see [Sentry Crons vs a dedicated cron monitor](https://cronradar.com/blog/sentry-crons-vs-dedicated-cron-monitoring), [Dead Man's Snitch vs CronRadar](https://cronradar.com/blog/dead-mans-snitch-comparison), and [Uptime Kuma push monitors](https://cronradar.com/blog/uptime-kuma-cron-monitoring).

## How a Better Stack Heartbeat Is Configured

In the dashboard you go to Heartbeats → Create heartbeat, give it a name, set **Expect a heartbeat every** to the task's frequency, set the **grace period**, "the extra time allowed before an incident is raised if the heartbeat is missed," choose on-call escalation settings, and then copy the secret URL from the heartbeat's detail page ([heartbeat guide](https://betterstack.com/docs/uptime/cron-and-heartbeat-monitor/)).

The same settings are fields of the [Create a heartbeat](https://betterstack.com/docs/uptime/api/create-a-hearbeat/) API, `POST https://incidents.betterstack.com/api/v2/heartbeats` with a bearer token:

| Field                                                                            | What it controls                                                                                                                                                                                                                                              |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `period`                                                                         | Required. How often to expect a heartbeat, in seconds: at least 30, and shorter than one year                                                                                                                                                                 |
| `grace`                                                                          | Required. How late a heartbeat may arrive, in seconds, before it counts as missed: at least 0, shorter than one year. Better Stack recommends about 20% of `period`                                                                                           |
| `server_timezone`                                                                | Optional IANA time zone, such as `Europe/Berlin`, for DST-aware missed-beat detection. Requires a `period` of at least 3,600 seconds                                                                                                                          |
| `email`, `sms`, `call`, `push`, `critical_alert`                                 | How the on-call person is notified. Only `email` defaults to `true`; `critical_alert` is a push that ignores the mute switch and Do Not Disturb                                                                                                               |
| `team_wait`                                                                      | Seconds before the incident escalates to the whole team; leave it blank to never escalate to the team                                                                                                                                                         |
| `policy_id`                                                                      | An escalation policy. Without one, Better Stack emails the primary on-call, escalates to the rest of the team if nobody acknowledges, and triggers all of your integrations ([escalation policies](https://betterstack.com/docs/uptime/escalation-policies/)) |
| `maintenance_from`, `maintenance_to`, `maintenance_days`, `maintenance_timezone` | A daily maintenance window on chosen weekdays, in a Rails time zone name, UTC by default                                                                                                                                                                      |
| `heartbeat_group_id`, `metadata`, `paused`                                       | A heartbeat group; metadata such as an owner or environment, settable only at creation and changed later through the metadata API; and pausing                                                                                                                |

None of these fields is a schedule. The heartbeat knows how often to expect a request and how long to wait, nothing about which days or times the job runs.

### Pending, Up, Down, and Validating

A new heartbeat stays Pending until its first request arrives. The guide says "the monitoring period begins from the first heartbeat received, and incidents are only raised after the expected frequency and grace period have elapsed" ([heartbeat guide](https://betterstack.com/docs/uptime/cron-and-heartbeat-monitor/)). A heartbeat for a job that was never deployed therefore never starts its monitoring period, and the guide describes no alert for that case.

After that, the API's `status` field reports `up` when requests arrive on time, `down` when one didn't, `validating` when requests arrive on time again and the incident waits to be resolved, `maintenance` inside the maintenance window, and `paused` ([Get a single heartbeat](https://betterstack.com/docs/uptime/api/get-a-single-hearbeat/)). An incident stops alerting other team members once someone acknowledges it ([acknowledging](https://betterstack.com/docs/uptime/acknowledging-an-incident/)).

### Heartbeats in Terraform

The provider's `betteruptime_heartbeat` resource takes the same settings except `metadata` and exposes the heartbeat URL as the computed `url` attribute ([resource docs](https://github.com/BetterStackHQ/terraform-provider-better-uptime/blob/v0.22.4/docs/resources/betteruptime_heartbeat.md)). The provider reads its token from `BETTERUPTIME_API_TOKEN` when `api_token` is omitted ([provider.go](https://github.com/BetterStackHQ/terraform-provider-better-uptime/blob/v0.22.4/internal/provider/provider.go#L40-L46)):

```terraform
terraform {
  required_providers {
    betteruptime = {
      source  = "BetterStackHQ/better-uptime"
      version = "0.22.4"
    }
  }
}

# The API token comes from the BETTERUPTIME_API_TOKEN environment variable.
provider "betteruptime" {}

resource "betteruptime_heartbeat" "nightly_backup" {
  name   = "Nightly database backup"
  period = 86400 # one request a day
  grace  = 3600  # up to an hour late before an incident
}

output "nightly_backup_heartbeat_url" {
  value     = betteruptime_heartbeat.nightly_backup.url
  sensitive = true
}
```

In 0.22.4 the `url` attribute isn't marked sensitive ([resource_heartbeat.go](https://github.com/BetterStackHQ/terraform-provider-better-uptime/blob/v0.22.4/internal/provider/resource_heartbeat.go#L26-L31)), so mark any output that exposes it `sensitive = true`, which redacts it in the CLI's apply output. Terraform still stores it in state, and `terraform output -raw` prints it in plain text ([Terraform docs](https://developer.hashicorp.com/terraform/language/manage-sensitive-data)), which is how you'd read it to install on the job host.

## The Heartbeat URL Is the Secret

A heartbeat URL has the form `https://incidents.betterstack.com/api/v1/heartbeat/<HEARTBEAT_TOKEN>`, and the guide calls it a secret URL. The documented requests carry no other credential, so anyone who has the URL can mark the job as having run or report it failed. Keep it out of crontab lines, scripts in version control, and job logs: store it in a file readable only by the job's account, or in your platform's secret store.

The guide documents three requests ([heartbeat guide](https://betterstack.com/docs/uptime/cron-and-heartbeat-monitor/#reporting-failures)):

- **The URL itself** records a heartbeat. The guide's example is a plain `curl` at the end of the job.
- **The URL plus `/fail`** explicitly reports a failure.
- **The URL plus the exit code**, with the job's output sent as the request body, reports a failure with both, "to help diagnose the failure."

It documents no start request and no run duration, so a run that hangs sends nothing and shows up as a missed heartbeat once the period and grace pass.

This wrapper sends the plain request after a successful run, and the exit code with the output's last 10 KB after a failed one. The guide states no size limit for the body; the 10 KB cut is this script's choice. Each request gives up after five seconds, a failed request is ignored, and the script exits with the job's own code:

```bash
#!/bin/sh
# /opt/backup-betterstack.sh: run the backup, then report to a Better Stack heartbeat.
# The file holds only the heartbeat URL and is readable only by this account.
heartbeat_url=$(cat /etc/betterstack/nightly-backup.url 2>/dev/null)

report() {
  [ -n "$heartbeat_url" ] || return 0
  curl --fail --silent --show-error --max-time 5 --output /dev/null "$@" || true
}

output=$(/opt/backup.sh 2>&1)
rc=$?
printf '%s\n' "$output"

if [ "$rc" -eq 0 ]; then
  report "$heartbeat_url"
else
  # The exit code in the path reports a failure; the body carries the output.
  printf '%s' "$output" | tail -c 10000 | report --data-binary @- "$heartbeat_url/$rc"
fi
exit "$rc"
```

Schedule it with `0 3 * * * /opt/backup-betterstack.sh`. With `--data-binary @-`, curl reads the POST body from standard input and keeps its newlines, and output that happens to start with `@` isn't taken for a file name, as it would be with `-d "$output"` ([curl manpage](https://curl.se/docs/manpage.html#--data-binary)). The backup's output is printed again so cron's mail or your log collector still gets it.

## A Period Is Not a Cron Schedule

Because the heartbeat expects one request per period, it fits jobs that run at a fixed interval. Schedules that aren't a fixed interval need workarounds:

- **Weekdays only.** `0 3 * * 1-5` has gaps of one day and three days, and one period can't match both. The guide says only that "the monitoring period begins from the first heartbeat received"; it doesn't say whether each later deadline is measured from the last request, so test how a weekday heartbeat behaves over a weekend before relying on it. [Cron monitoring vs uptime monitoring](https://cronradar.com/blog/cron-monitoring-vs-uptime-monitoring) works through what a fixed interval costs on a weekday job. Per-weekday maintenance windows exist, but the docs don't say what happens to a heartbeat that falls due inside one once the window ends, so test that too.
- **Several fixed times a day,** such as 06:00 and 18:00, are a fixed interval only when they're evenly spaced; `0 6,20 * * *` has gaps of 14 and 10 hours, and one period can't match both.
- **Monthly jobs** run on calendar dates, and months aren't a fixed number of seconds.

For daylight saving time, Better Stack is ahead. Set `server_timezone` and the period is evaluated "against wall-clock time, keeping daily and cron-style schedules aligned across daylight saving time changes," for periods of an hour or more ([resource_heartbeat.go](https://github.com/BetterStackHQ/terraform-provider-better-uptime/blob/v0.22.4/internal/provider/resource_heartbeat.go#L44-L48)). CronRadar evaluates every schedule in UTC. A backup at 03:00 Europe/Berlin runs at 01:00 UTC in summer and 02:00 UTC in winter, so CronRadar's schedule docs say to use the earlier time, `0 1 * * *`, with a grace of at least an hour plus the run time ([schedule syntax](https://docs.cronradar.com/schedules)). In summer, that extra hour delays every missed-run alert. [Cron and daylight saving time](https://cronradar.com/blog/cron-daylight-saving-time) covers the general problem.

On grace: Better Stack's 20% suggestion is 17,280 seconds, close to five hours, for a daily job. The request goes out when the job finishes, so the grace has to cover the start delay plus how much the run time varies; anything beyond that only delays the alert. [Choosing a grace period](https://cronradar.com/blog/cron-grace-period) shows how to size it from measured runs.

## Better Stack Heartbeat Pricing

From the [pricing page](https://betterstack.com/pricing), checked October 30, 2026:

- **Free for personal projects:** $0 a month, listing "10 monitors & heartbeats, 1 status page" and "Slack & e-mail alerts."
- **Heartbeats:** "10 heartbeats included," then each additional 10 heartbeats cost $20 a month, or $17 a month billed annually.
- **Responder license:** $34 per license per month, or $29 billed annually, which the page describes as "Access to Incidents, including on-call, monitoring and status pages, with unlimited phone call and SMS alerts." The paid side of the page "starts at just $34" and "includes a single responder."

Better Stack's roles page says Admin, Team lead, and Responder each consume a responder license, and the free Member role can't access monitors or heartbeats ([managing roles](https://betterstack.com/docs/uptime/team-management/manage-roles/)). With the default roles, everyone who needs to see or edit heartbeats uses a responder license.

The pricing page doesn't total a heartbeat-only bill, so the figures below add list prices on monthly billing, assuming one responder license is the minimum paid setup. Confirm the complete quote with Better Stack. CronRadar's columns assume every job is in one schedule class, with unlimited collaborators:

| Jobs | Better Stack, 1 responder | Better Stack, 5 responders | CronRadar, hourly or slower | CronRadar, every 5–59 minutes | CronRadar, under 5 minutes |
| ---: | ------------------------: | -------------------------: | --------------------------: | ----------------------------: | -------------------------: |
|   10 |                       $34 |                       $170 |                          $0 |                            $0 |                         $0 |
|   25 |                       $74 |                       $210 |                       $7.50 |                           $15 |                        $30 |
|   50 |                      $114 |                       $250 |                         $20 |                           $40 |                        $80 |
|  100 |                      $214 |                       $350 |                         $45 |                           $90 |                       $180 |

The license buys more than heartbeats: the same seat covers on-call, monitoring, and status pages. Per job, Better Stack's block rate is $2 a heartbeat monthly or $1.70 annually, so for jobs that run more often than every 5 minutes, annual billing comes in below CronRadar's $2 Fast price once the responder licenses are already paid for.

CronRadar's columns use its list prices: Free covers up to 10 active monitors, and on Grow the first 10 stay free and each further monitor costs $2 Fast (gap under 5 minutes), $1 Frequent (5 minutes to under an hour), or $0.50 Standard (hourly or less often) a month, billed in arrears on the month's peak ([pricing](https://cronradar.com/pricing)).

## How CronRadar Monitors the Same Job

CronRadar is a hosted monitor for scheduled jobs only. Four things differ from a Better Stack heartbeat ([concepts](https://docs.cronradar.com/concepts)); [Dead Man's Snitch vs CronRadar](https://cronradar.com/blog/dead-mans-snitch-comparison) covers in more detail how CronRadar decides a run is late, stuck, or never ran:

- **A schedule instead of a period.** A monitor holds a standard five-field cron expression evaluated in UTC, plus a `gracePeriod` in seconds, minimum 60 and default 90. Every check-in moves the expected time to the schedule's next occurrence, so `0 3 * * 1-5` expects nothing on weekends.
- **Start, duration, and stuck runs.** Besides a plain heartbeat, a job can send `start`, then `complete` or `fail`, paired by a `run_id`. `start` and `complete` record the run's duration, and `fail` opens an incident immediately with an optional `message` of up to 1,000 characters. A run that sent `start` and hangs goes `critical` as stuck only when the next expected run plus grace passes, about a day later for a daily job, which is later than a success-only heartbeat goes missing; a timeout inside the job turns a hang into a prompt `fail` ([cron job timeouts](https://cronradar.com/blog/cron-job-timeout)).
- **Outcome checks instead of output.** A `complete` sent by POST can carry up to five numeric or boolean values. Once expectations such as `dump_bytes >= 1` are configured in the job's settings, a missing or violated value marks the run as an outcome failure (`outcomefailed`) and opens an incident. That's the [run that exits 0 and does nothing](https://cronradar.com/blog/scheduled-job-silent-success), which a heartbeat records as a success.
- **An alert for a job that never ran.** A monitor created in the dashboard, by Hangfire discovery, or by Quartz.NET discovery for a job on a repeating simple trigger, is `pending` until its first expected run plus grace passes, then turns `critical` and sends a never-executed alert. A monitor created by its own first check-in can't flag a job that never runs, the same gap a Better Stack heartbeat has.

The credential is handled differently too. With Basic auth, as used here, a CronRadar check-in URL holds only the job's monitor key, and the application's API key goes in the Authorization header as the username with an empty password ([HTTP API](https://docs.cronradar.com/api-reference)). That key can report for every job in its application, so it needs the same care as a heartbeat URL, but it stays out of URLs, proxy logs, and shell history.

The [Dead Man's Snitch guide](https://cronradar.com/blog/dead-mans-snitch-comparison) has a complete CronRadar wrapper for this same 03:00 backup: it reads the API key from a file only the job's account can read, sends `start`, then `complete` or `fail`, gives up on each request after five seconds, ignores errors, and exits with the backup's own code. To also check that the dump isn't empty, drop its Dead Man's Snitch line and replace its `report complete` line with a `complete` sent by POST that carries the dump's size:

```bash
  # In the wrapper's success branch, in place of "report complete":
  dump_bytes=$(wc -c 2>/dev/null < /var/backups/db-latest.dump || echo 0)
  curl --fail --silent --show-error --max-time 5 --output /dev/null \
    --request POST --user "$CRONRADAR_API_KEY:" \
    --header "Content-Type: application/json" \
    --data "{\"outcomes\":{\"dump_bytes\":$dump_bytes}}" \
    "$url/complete?run_id=$run_id" || true
```

The `dump_bytes` value is stored only after you configure an expectation for that key; unconfigured keys are accepted and dropped ([HTTP API](https://docs.cronradar.com/api-reference)).

For .NET, the `CronRadar` 1.0.13 package wraps a job with `WrapAsync` but can't send outcome values; that needs the pending 1.0.14 release. `CronRadar.Hangfire` 0.1.0 and `CronRadar.Quartz` 0.1.0 discover recurring jobs, though Quartz 0.1.0 monitors only jobs on repeating simple triggers; [Hangfire monitoring](https://cronradar.com/blog/hangfire-monitoring) and [Quartz.NET monitoring](https://cronradar.com/blog/quartz-net-monitoring) cover them, including the published Hangfire 0.1.0's retry caveat. The Node.js, Python, Celery, PHP, and Laravel SDKs aren't installable, so those jobs report over HTTP as above, and HTTP check-ins don't discover jobs automatically.

## Better Stack and CronRadar Side by Side

Better Stack facts checked October 30, 2026, from the pages linked above.

|                        | Better Stack heartbeat                                                                                                                            | CronRadar                                                                                        |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| When a run is due      | Every `period` seconds, plus `grace`                                                                                                              | Next time of a five-field cron schedule in UTC, plus `gracePeriod`                               |
| Time zone              | `server_timezone` for DST alignment, periods of an hour or more                                                                                   | UTC only                                                                                         |
| Signals from a job     | Success request; `/fail`; exit code with output in the body                                                                                       | Heartbeat, or `start`, `complete`, and `fail` with a message of up to 1,000 characters           |
| Credential             | Secret token in the URL                                                                                                                           | Application API key, sent here in a Basic auth header                                            |
| Run duration           | Not documented                                                                                                                                    | From `start` to `complete`                                                                       |
| A hung run             | Missed heartbeat at the usual deadline                                                                                                            | Stuck at the next expected run plus grace, if it sent `start`                                    |
| A job that never runs  | Stays Pending                                                                                                                                     | `pending`, then `critical` with a never-executed alert, if the monitor was created ahead of time |
| Result checks          | Job output attached to a failure                                                                                                                  | Up to five numeric or boolean outcomes checked on `complete`                                     |
| Notifications          | Email, SMS, phone call, push; Slack, Microsoft Teams, webhook, and Zapier integrations, triggered by default or chosen per escalation-policy step | Email, Slack, webhook, Discord, PagerDuty                                                        |
| On-call and escalation | On-call schedules, `team_wait`, multi-step escalation policies                                                                                    | None; PagerDuty is the hand-off                                                                  |
| Created from           | Dashboard, REST API, Terraform                                                                                                                    | First check-in with `schedule`, dashboard, `POST /sync`, Hangfire and Quartz.NET 3.x discovery   |
| Who can see it         | A responder license per Admin, Team lead, or Responder                                                                                            | Unlimited collaborators                                                                          |
| Price beyond 10        | $20 per 10 a month ($17 annually), plus responder licenses                                                                                        | $2, $1, or $0.50 per monitor a month by schedule class                                           |

The Better Stack notification row combines the API's alert fields, the [escalation policies](https://betterstack.com/docs/uptime/escalation-policies/) page, and the heartbeat section of the [pricing page](https://betterstack.com/pricing).

## When Better Stack Fits Better

- **Your incidents already run through Better Stack.** A missed backup pages the same on-call schedule, with phone calls, SMS, critical push, and multi-step escalation, none of which CronRadar has.
- **Jobs run on a local clock that observes daylight saving time.** `server_timezone` keeps a daily heartbeat aligned; CronRadar needs a UTC schedule and an extra hour of grace.
- **You want the failing job's output on the incident.** CronRadar's `fail` message is capped at 1,000 characters, and it deliberately doesn't collect logs.
- **Alerts must reach Microsoft Teams or Zapier,** which aren't among CronRadar's five channels.
- **You manage monitoring in Terraform,** with Better Stack's own provider and its heartbeat resource.
- **Many jobs run more often than every 5 minutes,** you bill annually, and you already pay for responder licenses, so the $1.70 per-heartbeat block rate undercuts CronRadar's $2 Fast rate.

## When a Dedicated Cron Monitor Fits Better

- **The schedule isn't a fixed interval:** weekdays only, uneven times like `0 6,20 * * *`, or the 1st of the month.
- **You want more than "it ran":** run duration, stuck runs, and outcome checks on what the run produced.
- **A job that never runs should alert.** Create the monitor in the dashboard, or let Hangfire discovery create it, and it alerts if the first run never arrives.
- **Several people need to see job health** without a license each.
- **Most jobs run hourly or less often,** at $0.50 a month each beyond the free 10.
- **Your jobs are Hangfire recurring jobs, or Quartz.NET 3.x jobs on repeating simple triggers,** that you want discovered instead of wired one at a time.

To move a heartbeat over, take the schedule from the crontab or scheduler rather than from the period, write it in UTC, and run both monitors for one normal schedule window. Confirm a real run reached CronRadar and a test alert reached its recipient before you delete the heartbeat ([migration guide](https://docs.cronradar.com/migrations)). For more options, see [cron monitoring tools compared](https://cronradar.com/comparisons/best-cron-monitoring-tools).

## Frequently Asked Questions

### Do Better Stack heartbeats support cron expressions?

No. A heartbeat takes a `period` and a `grace`, both in seconds, and the create-heartbeat API has no schedule field. For periods of an hour or more, `server_timezone` keeps the period aligned with wall-clock time across daylight saving changes, but a weekday-only or monthly schedule still has to be approximated with a period.

### How do I report a failed job to a Better Stack heartbeat?

Request the heartbeat URL with `/fail` appended, or append the job's exit code and send its output as the request body, which Better Stack says helps diagnose the failure. Send the plain URL only after a successful run, and keep the wrapper's own exit code equal to the job's.

### Is the Better Stack heartbeat URL a secret?

Yes. The URL carries a token, and Better Stack's guide calls it a secret URL; the documented requests use no other credential. Anyone with it can report the job as healthy or failed, so keep it in a file only the job's account can read or in a secret store, not in a crontab or repository.

### How much do Better Stack heartbeats cost?

As of October 30, 2026, the free personal option lists "10 monitors & heartbeats." On paid plans, 10 heartbeats are included and each additional 10 cost $20 a month, or $17 billed annually. Each Admin, Team lead, or Responder needs a responder license at $34 a month, or $29 billed annually.

### What happens if a Better Stack heartbeat never gets its first request?

It stays Pending. Better Stack starts the monitoring period at the first heartbeat, so by its guide's description a job that was never deployed doesn't produce an incident. Send one request when you install the job, and check that the heartbeat has left Pending.
