Monitor a service across staging and production
This guide walks through the full environments workflow: creating environments in the catalog, attaching a system to them, and writing one HTTP health check that runs against each environment’s base URL using {{ environment.baseUrl }} templating. You end with per-environment health values, per-environment run history, and the system rollup — all from a single check configuration.
Read Environments first for the mental model.
What you need
Section titled “What you need”- A Checkstack instance with the HTTP plugin installed.
- The
catalog.environments.manageandhealthcheck.configuration.manageaccess rules (both are in the built-in administrator role).
1. Create the environments
Section titled “1. Create the environments”Open Catalog management from the sidebar, then select the Environments tab.
Create a staging environment
Section titled “Create a staging environment”- Click Add environment.
- Set Name to
staging. - Under Custom fields, add:
- Key:
baseUrl, Value:https://staging.example.com
- Key:
- Click Save.
Create a production environment
Section titled “Create a production environment”- Click Add environment again.
- Set Name to
production. - Under Custom fields, add:
- Key:
baseUrl, Value:https://api.example.com
- Key:
- Click Save.
2. Attach the system to both environments
Section titled “2. Attach the system to both environments”- Open Configuration -> Catalog and go to the Systems tab.
- Find the system you want to monitor (for example
Payments API) and use its environment chips (the attach menu on the row) to add bothstagingandproduction. You can also work the other way round from the Environments tab and attach systems to an environment there.
Membership saves immediately as you toggle it. The system now belongs to both environments; on the next health check run the executor reads this membership from the catalog and fans out accordingly.
3. Create the HTTP health check with a templated URL
Section titled “3. Create the HTTP health check with a templated URL”- Navigate to Health Checks and click Create Check.
- Select the HTTP Health Check strategy.
- In the editor:
- Name:
Payments API /healthz - Interval:
60
- Name:
Add the Request collector
Section titled “Add the Request collector”-
Select Add Collector and choose Request.
-
In the collector’s URL field, enter:
{{ environment.baseUrl }}/healthz -
Set Method to
GETand Expected status to200. -
Leave the other fields at their defaults.
The URL field is marked x-templatable. At run time, before the HTTP request is made, the executor renders {{ environment.baseUrl }} against the resolved environment’s custom fields. The result for staging is https://staging.example.com/healthz; for production it is https://api.example.com/healthz.
Templating is not limited to HTTP. Most connection and target fields across the built-in check types support {{ … }}, so the same “one config, many environments” pattern works for a database host, an SSH command, a DNS hostname, a gRPC host, and more. The available variables are the selected environment’s custom fields ({{ environment.<key> }}), the check metadata ({{ check.id }}, {{ check.name }}, {{ check.intervalSeconds }}), and the system metadata ({{ system.id }}, {{ system.name }}).
Assign to the system
Section titled “Assign to the system”- In the Systems section of the editor, select the
Payments APIsystem. - Save the health check.
4. Configure the environment fan-out
Section titled “4. Configure the environment fan-out”Open the Payments API /healthz check from the Health Checks list. The editor’s Assignment section lists the Payments API system with its per-system panels - open its Execution panel.
The Environments section offers three modes:
| Mode | Meaning |
|---|---|
| All environments | Run once per environment the system belongs to. Adding or removing the system from an environment updates the fan-out on the next tick. |
| Specific | Run only for the environments you select explicitly. |
| None | Opt out of fan-out; run exactly once with no environment context. |
Leave All environments selected (the default). With staging and production attached, the check now fans out to two runs per tick.
5. Watch the per-environment results
Section titled “5. Watch the per-environment results”After the first run, open the system’s health check drawer:
- The runs table shows one row per environment per tick. The Environment column identifies which environment each row belongs to.
- Run history groups by environment so you can compare staging and production latency and failure rates independently.
- The system overview shows one row per slice - one environment as probed from one location. A row that is degraded or unhealthy shows when it was last healthy (for example “Healthy until 2h ago”), so you can see at a glance since when that slice has been failing without opening the drawer.
Checks probed from more than one location
Section titled “Checks probed from more than one location”A check can run from the core and from one or more satellites (see Satellites). Each location is evaluated on its own, and the worst result decides the check’s status - the same way environments work.
This matters because the two are genuinely different situations: a service that is reachable from your core but not from a satellite in another region is not healthy, it is unreachable for the people that satellite speaks for.
- The system overview names the location on each row (for example EU West) as soon as a check runs from more than one place, so you can tell which location is failing. A check that only ever runs on the core shows no location - there is nothing to disambiguate.
- Removing a satellite from a check retires its slice: the history stays, but it moves under Old checks and stops counting toward the check’s status.
How the fan-out is counted on the dashboard
Section titled “How the fan-out is counted on the dashboard”The dashboard problem card counts slices, not checks. A single check that fans out to three environments where one environment is failing reads “1 of 3 checks failing”, not “1 of 1”. A system with a three-environment check plus a single-environment check, with one environment failing, reads “1 of 4 checks failing”. Locations multiply the same way: one environment probed from the core and one satellite is two slices. A check with no environments that runs only on the core counts as one slice, so a system with no environments reads exactly as before.
One notification per environment outage
Section titled “One notification per environment outage”When a fanned-out environment changes health, you get a single notification scoped to that environment (for example ”… is unhealthy in environment production”). The system rollup (”… is unhealthy”) describes the same outage, so it is not sent as a second, duplicate notification - the per-environment notification already tells you which environment is affected. The rollup notification is still sent for a system with no environments, and the rollup status change is still recorded and still drives automations (see section 6).
6. Read per-environment health in automations
Section titled “6. Read per-environment health in automations”The health triggers (healthcheck.system_degraded, healthcheck.system_health_restored, healthcheck.system_health_changed) fire for both per-environment changes and the system rollup:
- A per-environment change carries
trigger.payload.environmentId(the environment id) andtrigger.payload.systemId. - The system rollup change carries only
trigger.payload.systemId(noenvironmentId).
Open an incident only when production is unhealthy
Section titled “Open an incident only when production is unhealthy”trigger: healthcheck.system_degradedfilter: "trigger.payload.environmentId == 'production'"actions: - openIncident: title: "Production degraded: {{ trigger.payload.systemId }}"Alert on any environment change (default behavior)
Section titled “Alert on any environment change (default behavior)”An automation that does not reference environmentId continues to fire off the system rollup, exactly as it did before environments. The rollup is the worst status across all environments, so existing automations keep working without modification.
7. Use environment values in scripts
Section titled “7. Use environment values in scripts”If you are using a script collector instead of (or alongside) the HTTP collector, the resolved environment is available in two ways:
Shell script:
echo "checking ${CHECKSTACK_ENV_NAME} at ${CHECKSTACK_ENV_BASE_URL}"curl -sf "${CHECKSTACK_ENV_BASE_URL}/healthz" || exit 1The custom field baseUrl is injected as CHECKSTACK_ENV_BASE_URL (camelCase split to UPPER_SNAKE_CASE). See Script health checks for the full variable reference.
Inline TypeScript:
import { defineHealthCheck } from "@checkstack/healthcheck";
const baseUrl = context.environment?.fields.baseUrl ?? "http://localhost";const resp = await fetch(`${baseUrl}/healthz`);export default defineHealthCheck({ success: resp.ok, message: `${context.environment?.name ?? "env-less"}: HTTP ${resp.status}`,});context.environment is undefined when the run has no environment (the None assignment mode, or All environments with no membership).
What you built
Section titled “What you built”| Component | What it does |
|---|---|
staging environment | Carries baseUrl: https://staging.example.com |
production environment | Carries baseUrl: https://api.example.com |
Payments API system | Belongs to both environments |
Payments API /healthz check | One config, runs against each environment’s baseUrl per tick |
| Per-environment health | payments-api::staging and payments-api::production tracked independently |
| System rollup | Worst-status rollup, same id as before, picked up by existing automations |
To extend this pattern to more environments, add a new environment in the catalog, attach the system to it, and the next tick fans out automatically - no check config change needed.