Fleets: one monitor for many machines
A fleet watches the same job running on many machines as a single monitor. You declare the schedule and the rules once, each machine is evaluated on its own against them, and a failure produces one alert naming the machines involved. The failure it prevents is the one where three shops out of two hundred stop syncing and nobody notices, because two hundred separate monitors is either two hundred alerts or an inbox filter that hides all of them.
When a fleet is the right shape
Use a fleet when the machines are interchangeable: the same job, the same schedule, the same meaning of failure. A POS terminal in every shop. A sync agent on every branch server. A scraper on every worker node.
Use separate monitors when the jobs differ. An extract, a transform and a load are three different jobs that happen to run on one host, and they want three monitors and a dependency chain, not a fleet.
Setting one up
- Fleets are on Business, with an instance cap of 500. Downgrading pauses your fleets rather than deleting them: instances and history survive, and they resume on an upgrade or a complimentary plan.
- In the dashboard, open Fleets and choose New fleet. Give it a name. The slug fills itself in from the name and stops following it once you edit it directly, so your ping URLs do not change under you.
- Set the schedule: how often each machine reports, and how late it may be. Grace should cover the slowest normal run, not the average one.
- Set Expected instances if you know the number. Without it the denominator is however many machines have registered, which means a machine that never came online at all is invisible rather than missing.
- Save. The fleet page shows one ping URL for the whole fleet, ending in a placeholder for the machine name.
- Put that URL in the job on every machine, substituting a stable identifier for the placeholder. Each machine registers itself on its first ping.bash
# Every machine runs the same line. Each substitutes its own name. # $(hostname) works; so does a site id, a serial, or anything stable. 0 * * * * /opt/agent/run.sh && \ curl -fsS -m 10 --retry 5 \ https://pulsewatcher.up.railway.app/ping/pwf_<token>/$(hostname)
- Confirm it worked: within a minute the fleet page lists each machine that has pinged. New arrivals sit in
learninguntil they have reported on schedule once, and a machine in learning can never breach a fleet. When they move toreporting, the fleet is live.
Spreading a large fleet across the minute
0 * * * * on ten thousand machines means ten thousand pings in the same second. Ingestion is built for that and holds up: 10,000 instances inside a 60 second window measured p50 154ms, p99 276ms, no dropped pings and a queue that drained in 14 seconds. The part that does not hold up is the network in between. If those machines sit behind one NAT gateway, the burst reaches us as thousands of new connections from a single source IP, and that is the pattern edges and firewalls treat as an attack. We have measured connections being refused at that shape.
Above roughly 200 machines, give each one a fixed offset instead. Note that this is not a random sleep. A random delay each hour would make the gap between one machine's pings vary between 55 and 65 minutes, and a fleet with a tight grace would start alerting about machines that are working. An offset derived from the machine's own name is stable, so every interval stays exactly one hour and the fleet still arrives spread out.
# Above 200 machines, spread the arrivals. Same line everywhere: # each host derives its own offset from its own name, so machine A always # pings at +266s and machine B always at +38s. 0 * * * * sleep $(( $(hostname | cksum | cut -d' ' -f1) % 300 )); \ /opt/agent/run.sh && \ curl -fsS -m 10 --retry 5 \ https://pulsewatcher.up.railway.app/ping/pwf_<token>/$(hostname)
Machines that are meant to be off
A desktop switched off at 17:00 is not an incident. The same silence from a payment gateway at 03:00 is. By default a fleet is active every hour of every day, so every machine is expected to report around the clock. Give it active hours and time only counts toward lateness inside those hours.
That last sentence is the whole feature, and it is worth being precise about. Suppose the machines ping hourly and work 09:00 to 17:00. One reports at 16:55 and is switched off at 17:00. At 09:01 the next morning it has been silent for sixteen hours, which against an hourly schedule would be very missing indeed. But only six of those minutes were inside its window: five on Monday, one on Tuesday. It is on time, and so is the rest of the fleet, so nothing fires and nothing needs suppressing.
A machine that stops during the working day is caught normally. If it last reported at 10:00 and it is now 11:10, that is seventy minutes of window time, and it is missing. It stays missing when the window closes at 17:00 rather than turning into “off, as expected”, because it broke while it was supposed to be working and that does not stop being true at five o'clock.
| Setting | Default | What it does |
|---|---|---|
| Active hours | Always active | Days, a start, an end and a timezone. Only time inside the window counts toward lateness. |
| A missed run outside these hours | Shows as quiet | quiet is informational and never alerts. warn produces a warning. alert judges the machine on the wall clock, as if it were always on. |
| Missing after | Immediately past grace | Extra seconds past grace before late becomes missing. For when one skipped run is not news and three in a row is. |
| Dormant after | Never | Days of silence after which a machine stops counting toward the fleet but stays listed. |
Individual machines override any of these. The case it exists for is the one always-on server sitting inside a fleet of desktops: give that instance alert and it is judged on the wall clock while everything around it keeps office hours.
Timezones are per instance, so one set of hours across three countries is three different windows, which is usually what people mean. Daylight saving is handled: a window states local hours and keeps them across the change.
Instance keys
The last path segment is the instance key. It must start with a letter or digit and may contain letters, digits, dot, underscore, colon and hyphen, up to 128 characters. Case is preserved and compared exactly, so EDGE-07 and edge-07 are two different machines.
Pick something stable. A key derived from a container id or a boot-time random value creates a new instance on every restart, and the fleet fills with machines that ping once and go quiet forever. If that has already happened, fix the key at the source and turn auto_register off so unknown keys are rejected rather than joining.
Reporting outcomes, not just presence
A bare ping means the run succeeded. Instances can also report failure or an exit code, exactly as a monitor does, and the fleet evaluates the result per machine.
# report the exit code so the fleet can classify the run 0 * * * * /opt/agent/run.sh; \ curl -fsS -m 10 --retry 5 \ "https://pulsewatcher.up.railway.app/ping/pwf_<token>/$(hostname)/exit/$?" # or report failure explicitly, with stderr as the body 0 * * * * /opt/agent/run.sh 2>/tmp/agent.err || \ curl -fsS -m 10 --retry 5 --data-binary @/tmp/agent.err \ https://pulsewatcher.up.railway.app/ping/pwf_<token>/$(hostname)/fail
const base = 'https://pulsewatcher.up.railway.app/ping/pwf_<token>/' + os.hostname()
try {
const rows = await syncStock()
// metrics ride along on the success ping
await fetch(`${base}?rows=${rows}`)
} catch (err) {
await fetch(`${base}/fail`, { method: 'POST', body: String(err.stack ?? err) })
}What counts as a fleet failure
| Mode | Fires when | Use it when |
|---|---|---|
| any_missing | One or more instances missing | The default. Right when every machine matters individually, such as a POS terminal per shop. |
| threshold_count | At least N instances missing | Set alert_threshold_count. Right when a handful of machines being offline is normal operations and ten is an outage. |
| threshold_pct | At least N% of the denominator missing | Set alert_threshold_pct. Right for a fleet that grows and shrinks, where a fixed count would become wrong as it grew. |
| threshold_either | Whichever of the count or the percentage trips first | Set both alert_threshold_count and alert_threshold_pct. Right for a fleet that intends to grow, because one percent of fifty is nobody and one percent of ten thousand is a hundred machines. |
| all_missing | Every instance silent | Usually means your side broke, not theirs. A network partition or a bad deploy of the agent itself. |
Only missing instances count. A machine in learning, late or retired does not, for reasons set out in the state reference.
One alert, naming the machines
This is the point of the feature. Three shops going quiet produces one message that says which three, not three messages and not a message that says only "3 instances".
🔴 Pharmacy POS agents: 3 of 200 instances missing Not reporting: shop-014, shop-088, shop-141 Expected every 1h, 5m grace. Last seen 1h 12m ago. https://pulsewatcher.vercel.app/app/fleets/<id>
Up to 12 machines are named individually. Past that the alert names 12 and adds a count, because a message listing 180 hostnames is a message nobody reads.
Configuration is declared once and evaluated per machine
A fleet carries the same configuration surface a monitor does. Each setting is a fleet default, applied to every instance and evaluated against that instance's own data.
| Setting | How it applies | What that means in practice |
|---|---|---|
| Metric rules | Declared on the fleet, evaluated per instance | A floor of rows >= 1000 is checked against each machine’s own number, not against a total. |
| Exit-code rules | Declared on the fleet, evaluated per instance | Exit 100 means the same thing on every machine without writing the rule 200 times. |
| Duration and max duration | Fleet default, measured per instance | Each machine gets its own baseline, so a slow site is compared with itself. |
| Overlap policy | Fleet default, detected per instance | Defaults to ignore rather than the monitor default of warn, because fleet agents are often long-running and legitimately overlap. |
| Error grouping | Fingerprinted per fleet, counted per instance | The same traceback on 12 machines is one group that says 12, not 12 groups. |
| Completion budget | Fleet default, judged per instance | Never averaged. See below. |
| Runbook | Fleet default | Delivered inside the fleet alert. |
| Warn delivery and escalation | Fleet default | Same digest and escalation rules as a monitor. |
| New-error alerting | Fleet default, off | Defaults to false rather than the monitor default of true. Turning it on for a 200-machine fleet on day one is a lot of first sightings. |
Two defaults deliberately differ from a monitor's. Overlap starts at ignore and new-error alerting starts off, because turning either on across a fleet that already exists would light up every machine at once for behaviour that has been normal all along.
Rules on a fleet
- Open the fleet page. The Metric rules and Exit codes cards at the bottom are the same editors the monitor page uses.
- Add a rule. The metric name field suggests names the fleet has actually seen, taken from the latest values every instance reported.
- Save. The rule applies to every instance from its next ping. There is no per-machine copy to keep in sync.
- Confirm it worked: the fleet page shows a rollup across instances for each metric, with the min, median, p95 and max, and it names the instances furthest from the median. Outliers are measured against the median rather than the mean, so a handful of broken machines cannot drag the threshold out to meet themselves.
A breach on many machines is aggregated the same way an outage is: grouped by cause, one message, the machines named. Twelve instances exiting 100 is one line saying exit 100 on 12 instances followed by which twelve.
Completion budgets ask each machine, not the average
A fleet can pass "is anyone missing" while quietly failing "did each machine run as often as it was supposed to". An instance reporting six days in seven is never absent long enough to be called missing, and loses a seventh of its work every week.
Set a budget on the fleet and every instance is judged against it separately. Never averaged: 199 machines at 100% and one at 40% averages to 99.7% and reads as healthy while that machine drops three runs in five.
📉 Pharmacy POS agents: 1 of 200 instances below budget 1 of 200 instances ran less than 95% of their scheduled times over 30 days. Worst: shop-088 at 40%. shop-088: 40% (60 of 100 scheduled runs missed) https://pulsewatcher.vercel.app/app/fleets/<id>
- An instance needs at least 5 scheduled runs behind it before a percentage means anything. Below that it is skipped, not failed.
- The window starts when the instance first reported, so a machine added yesterday is not measured against a month it was not present for.
- A late run still counts as a run. A budget asks whether the work happened. Punctuality is what grace and duration regression are for.
- The window is 1 to 90 days, defaulting to 30. It stops at 90 because fleet slots are computed rather than stored, and a year across 500 machines is millions of calculations to answer one question.
- Retired instances are out of the estate and are not judged.
Silencing one machine
A single instance can be snoozed from the instance grid on the fleet page without silencing the fleet. Use it for the shop that is closed for refurbishment while the other 199 keep reporting. Maintenance windows can also be scoped to a fleet, which suppresses notifications while state and history keep recording.
Variations
200 shops, every one matters
Hourly period, 5 minute grace, any_missing, expected_instances 200. Any shop going quiet is worth knowing about.
Laptops that are not always on
Daily period, 6 hour grace, threshold_pct at 20%, retirement after 30 days. Individual machines are offline all the time; a fifth of them at once is a real problem.
Autoscaled workers
auto_register on, no expected_instances, retirement after 1 day, all_missing. The count is meant to change. Every worker silent at once is the only signal that means anything.
Nightly batch across regional servers
Cron schedule so the expected time follows local midnight per timezone, threshold_count at 2, a completion budget of 95% over 30 days, and a metric floor on rows processed. Catches both the machine that stopped and the machine that is quietly doing nothing.
Long-running agents
/start at the beginning of each run, max_duration_seconds set, overlap policy at warn. Catches the agent that hangs rather than the agent that stops.
What can go wrong
Instances appear and disappear constantly
The key is not stable. Something is deriving it from a container id, a pod name or a boot-time value. Fix the key, delete the strays, then turn auto_register off.
A machine that never came online is not reported missing
Without expected_instances, the denominator is the machines that have registered, and one that never pinged has not registered. Set the expected count.
The fleet hit its instance cap
500 on Business. Further unknown keys are refused. Retire the machines that have gone for good, or ask for the cap to be raised on your account.
Retired machines are still counted
retire_after_days is null by default, which means never. A decommissioned machine stays in the denominator until you set it.
A webhook consumer reads a fleet breach as healthy
A rule breach and a blown budget both report missing: 0, because in both cases every machine reported. Branch on details.reason, which is missing_instances, rule_breach or completion_budget.
{
"version": "1",
"event": "fleet.breached",
"sent_at": "2026-08-12T09:00:00.000Z",
"fleet": { "id": "…", "name": "Pharmacy POS agents", "slug": "pharmacy-pos" },
"details": {
"reason": "missing_instances",
"missing": 3,
"total": 200,
"missing_pct": 1.5,
"missing_instances": ["shop-014", "shop-088", "shop-141"],
"returned_instances": [],
"detail": "3 of 200 instances missing"
},
"dashboard_url": "https://pulsewatcher.vercel.app/app/fleets/…"
}Every field
| Field | Type | Default | Range | Effect |
|---|---|---|---|---|
| name | text | (required) | 1 to 80 characters | The name on alerts. |
| slug | text | (from the name) | lowercase, url safe | Used in URLs. |
| schedule_kind | enum | period | period, cron | Fixed interval, or a cron expression for schedules an interval cannot express. |
| period_seconds | int | 3600 | 60 to 2,592,000 | How often each instance is expected to report. |
| grace_seconds | int | 300 | 0 to 86,400 | How late an instance may be before it counts as missing. |
| cron_expression | text | null | 1 to 100 chars | Used when schedule_kind is cron. |
| cron_timezone | text | UTC | IANA name | Which clock the cron expression is read in. |
| expected_instances | int | null | 1 to 10,000 | The denominator. Without it, the denominator is however many instances have registered, so a machine that never came online at all is invisible. |
| alert_mode | enum | any_missing | see the table above | What counts as a fleet-level failure. |
| alert_threshold_count | int | null | 1 or more | Required by threshold_count. |
| alert_threshold_pct | number | null | 0.1 to 100 | Required by threshold_pct. |
| auto_register | boolean | true | An unknown instance key joins the fleet on its first ping. Off means unknown keys are rejected. | |
| retire_after_days | int | null | 1 to 365 | Silence after which an instance leaves the denominator. Null means never. |
| alert_after_seconds | int | 0 | 0 to 86,400 | Wait this long after a breach before sending. |
| repeat_interval_seconds | int | 0 (off) | 0, or 900 to 604,800 | Re-send while still breached. |
Every fleet is on Pro or above, so nothing in this table has a per-plan difference beyond the instance cap. The monitor configuration a fleet also carries is documented on the monitor page with the same defaults, except the two noted above.
Related
- Monitors when the jobs are different from each other. A fleet is for the same job repeated.
- Dependencies when jobs run in order. A fleet and a chain solve different problems and compose fine.
- Duration and completion budgets for the machine that runs but not often enough.
- Troubleshooting fleets when instances are reported missing and you can see them running.