Skip to content

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.

  • A Checkstack instance with the HTTP plugin installed.
  • The catalog.environments.manage and healthcheck.configuration.manage access rules (both are in the built-in administrator role).

Open Catalog management from the sidebar, then select the Environments tab.

  1. Click Add environment.
  2. Set Name to staging.
  3. Under Custom fields, add:
    • Key: baseUrl, Value: https://staging.example.com
  4. Click Save.
  1. Click Add environment again.
  2. Set Name to production.
  3. Under Custom fields, add:
    • Key: baseUrl, Value: https://api.example.com
  4. Click Save.

  1. Open Configuration -> Catalog and go to the Systems tab.
  2. Find the system you want to monitor (for example Payments API) and use its environment chips (the attach menu on the row) to add both staging and production. 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”
  1. Navigate to Health Checks and click Create Check.
  2. Select the HTTP Health Check strategy.
  3. In the editor:
    • Name: Payments API /healthz
    • Interval: 60
  1. Select Add Collector and choose Request.

  2. In the collector’s URL field, enter:

    {{ environment.baseUrl }}/healthz
  3. Set Method to GET and Expected status to 200.

  4. 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 }}).

  1. In the Systems section of the editor, select the Payments API system.
  2. Save the health check.

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:

ModeMeaning
All environmentsRun once per environment the system belongs to. Adding or removing the system from an environment updates the fan-out on the next tick.
SpecificRun only for the environments you select explicitly.
NoneOpt 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.


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.

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.

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) and trigger.payload.systemId.
  • The system rollup change carries only trigger.payload.systemId (no environmentId).

Open an incident only when production is unhealthy

Section titled “Open an incident only when production is unhealthy”
trigger: healthcheck.system_degraded
filter: "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.


If you are using a script collector instead of (or alongside) the HTTP collector, the resolved environment is available in two ways:

Shell script:

Terminal window
echo "checking ${CHECKSTACK_ENV_NAME} at ${CHECKSTACK_ENV_BASE_URL}"
curl -sf "${CHECKSTACK_ENV_BASE_URL}/healthz" || exit 1

The 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).


ComponentWhat it does
staging environmentCarries baseUrl: https://staging.example.com
production environmentCarries baseUrl: https://api.example.com
Payments API systemBelongs to both environments
Payments API /healthz checkOne config, runs against each environment’s baseUrl per tick
Per-environment healthpayments-api::staging and payments-api::production tracked independently
System rollupWorst-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.