Skip to content

Scheduling Health Checks

pg_health_framework deliberately does not schedule or trigger runs itself — that choice is left entirely to whatever fits your own environment: a cron job, an external orchestrator, or simply a person running psql by hand. Call the collection and evaluation engines (or the combined convenience call that does both in one step — see Running Checks End to End) from wherever makes sense for your setup.

The seeded suites make the wiring itself trivial: they partition every built-in check by recommended cadence (see The Catalog), so a complete schedule is six entries — with plain cron, for example:

* * * * *      psql -d yourdb -c "CALL pghf.run_and_evaluate('seeded-1m');"
*/5 * * * *    psql -d yourdb -c "CALL pghf.run_and_evaluate('seeded-5m');"
*/15 * * * *   psql -d yourdb -c "CALL pghf.run_and_evaluate('seeded-15m');"
0 * * * *      psql -d yourdb -c "CALL pghf.run_and_evaluate('seeded-1h');"
0 2 * * *      psql -d yourdb -c "CALL pghf.run_and_evaluate('seeded-daily');"
0 3 * * 0      psql -d yourdb -c "CALL pghf.run_and_evaluate('seeded-weekly');"

(seeded-on-demand — the pg_upgrade readiness checks — is deliberately not on the list: run it by hand, with a target version, when you're actually planning an upgrade.) The cadences are recommendations, not requirements — schedule a suite as often or as rarely as suits you, and because evaluation history is one stream per check regardless of suite, Alert Status merges every cadence into one current-state view.

A seventh entry, if you have any on-event checks: the evaluation sweep

Everything above schedules polled checks. If you've registered any on-event check — one invoked by a trigger rather than a suite — it needs one more cron line, and it's easy to miss because nothing about the six-entry schedule above hints that it's needed:

* * * * *      psql -d yourdb -c "CALL pghf.evaluate_pending_event_alerts();"

The trigger that invokes an on-event check can only ever collectpghf.execute_check_event() deliberately never commits, so it's safe to call from inside a trigger, but that also means it can never call pghf.evaluate_run() (which commits internally, the very thing a trigger can't allow). Without this line, an on-event check's data lands in pghf.raw_results exactly as expected, and its threshold, debounce, and events simply never fire — the same as if you'd never scheduled the polled suites above at all. Once a minute is a reasonable default (it's safe to run tighter than that, or to overlap with a slow-running previous call, if you want the freshest possible severity on a bursty event source — an internal lock keeps overlapping calls from interfering with each other). See On-Event Checks for the full model, including why collection and evaluation can't happen in the same call.

A second, tight loop: polling only what's already critical

The six-line schedule above answers "how often do I find out about a new problem." It says nothing about how fast you find out a problem went away — and for a check on an hourly or daily cadence, that gap can be hours. p_critical_only — a boolean parameter on pghf.execute_check_run() and pghf.run_and_evaluate(), default false — exists to let you design a second, much tighter polling loop for exactly that question, without paying the cost of running your whole suite that often.

CALL pghf.run_and_evaluate('seeded-1h', p_critical_only => true);

What it does, precisely. For every check the suite would otherwise run, this reads pghf.alert_status.current_status for that check and skips it — no invocation, SKIPPED in pghf.raw_results with an [engine]-prefixed note, exactly like the version/topology exclusions described in How Threshold Evaluation Works — unless that status is exactly 'critical' right now. A check that's never been evaluated at all is treated as not critical (excluded, not run) — this is deliberately a tool for watching an already-known problem more closely, not a way to get a check its first-ever result; your normal baseline schedule is still what establishes that. And it's an exact match: a check currently at 'error_in_performing_check' (which ranks even higher than critical on the severity ladder) is not included — that's a distinct condition from a threshold breach, not "critical."

Why this is cheap when nothing is wrong. The exclusion check is one indexed lookup (alert_status is keyed on check_uid) before the check's own routine is ever invoked. On a healthy fleet — the overwhelmingly common case — every member of the suite gets excluded, meaning a p_critical_only => true pass over your whole hourly suite costs a handful of cheap lookups and touches zero real check bodies, however expensive those checks normally are. That's the property that makes running this every 30 seconds a reasonable thing to consider, where running the whole suite every 30 seconds usually isn't.

The DBA-designed pattern this enables — layer a second schedule on top of your baseline one, over the same suite (or any suite containing the checks you care about watching closely):

0 * * * *      psql -d yourdb -c "CALL pghf.run_and_evaluate('seeded-1h');"
*/2 * * * *    psql -d yourdb -c "CALL pghf.run_and_evaluate('seeded-1h', p_critical_only => true);"

Ordinary hours: the 2-minute loop does nothing 30 times in a row, at negligible cost. The moment the hourly pass confirms something critical, the same 2-minute loop starts actually running that one check every 2 minutes — 30x fresher data on the live problem than the hourly baseline alone would give you, with no change to the baseline schedule and no extra load from the checks that are still healthy.

What faster polling actually buys you — expediting recovery without a human in the loop. Hysteresis (seeded on every built-in check outside seeded-on-demand) requires more than one clean confirmation before a critical clears — by default, 2 consecutive clean evaluated runs. That's runs, not elapsed time: a DBA who manually re-runs a check twice right after applying a fix clears it immediately, as covered in the Debounce chapter — a tight p_critical_only loop is the unattended, automated version of that same idea. Once whatever actually fixes the underlying condition happens — a DBA's change, autovacuum catching up, a connection spike subsiding on its own — the polling loop (not a person watching a dashboard) supplies the 2 consecutive clean runs hysteresis needs, and the check clears on its own within a couple of polling intervals instead of waiting out the rest of the baseline cadence. The same freshness also feeds events' fire_on_clear notifications and actions' cooldown-gated repeat firing sooner.

This is a capability, not a recommendation. Exactly like the seeded cadences themselves (see The Catalog), this project has no opinion about whether, or how tightly, you should poll criticals — p_critical_only is a building block for your incident-response design, not a policy this framework ships with an answer for. Some reasonable shapes, all equally valid, none of them anything this project implements or assumes:

  • A fixed tight cron loop (as above), running unconditionally alongside the baseline schedule.
  • A loop that only runs during business hours, or only against a subset of suites your team actually gets paged for.
  • Nothing scheduled at all — instead, an event or pg_relay_notifier notification that fires when a check first goes critical kicks off a short-lived tight loop (a cron entry an incident script installs, or an external job) that polls every 30 seconds until the check clears, then removes itself.
  • Calling it by hand from psql while actively investigating something, exactly as any other manual run_and_evaluate() call.

Two things worth knowing before you lean on it. First, p_critical_only composes with pg_relay: the exclusion is checked once at submission (a non-critical check is never enqueued) and re-checked again at worker-dispatch time, covering the case where another run confirms the check back to non-critical in the gap between enqueue and a worker actually picking it up. Second, an excluded check still produces a SKIPPED row in pghf.raw_results on every poll — cheap individually, but a very tight interval (seconds, not minutes) across a large suite run indefinitely will accumulate rows over time same as any other schedule does; factor it into your retention purge cadence the same way you would the baseline schedule's own volume.

Reading results back out afterwards works exactly the same way for any historical run, whether it was triggered by a schedule or run manually — there's nothing schedule-specific about the reporting functions described in How Threshold Evaluation Works.

If you'd like something to happen automatically the very moment a check's severity actually changes — rather than only finding out when someone goes and looks — that's what the optional events layer is for. See Events at a Glance next.

Continue to Events at a Glance.