ObservabilityXCIX · Missing MetricsMissingMetrics
Scrape Config Error
What you'll learn
- Distinguish a YAML syntax error, a schema error, and a semantic error in the scrape configuration
- Use promtool check config and promtool check service-discovery to catch each class before reload
- Configure scrape_interval, scrape_timeout, metrics_path, and scheme with the right invariants
- Roll back a bad reload without losing scrape continuity or TSDB head state
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A platform engineer adds a new scrape job for a service that
just shipped a custom /actuator/prometheus endpoint. They
edit prometheus.yml, kill -HUP Prometheus, and watch the
new target list. The new job has zero targets. The
/api/v1/targets endpoint shows the old jobs as healthy and
the new job as absent. The engineer opens the config, looks for
a typo, finds none, and reloads again. Same result. After
fifteen minutes they promtool check config the file and see
a YAML error two lines above the new job: an unquoted colon in
a label value that the YAML parser tolerated at the start of
the file but rejected when the new job added a value with a
colon in it. The fix is one line; the diagnostic was the
missing step.
A scrape config error is the third link in the chain. It is the link that the operator owns most directly, and the link where validation is cheapest. This lesson is the discipline of running the validation.
What it is
A scrape config error is any condition that prevents the scrape configuration from doing what the operator intended. There are three distinct classes, with different signatures and different fixes:
- YAML syntax error. The file does not parse. Promtool
reports a line and column; Prometheus refuses to load the
file at all;
kill -HUPis a no-op because the new file fails to load and the old file remains in memory. Symptom:promtool check configexits non-zero; Prometheus logserror loading config; the live configuration shown by/api/v1/status/configis the previous one. - Schema error. The YAML parses but the structure does
not match the Prometheus schema. A required field is
missing; a value is the wrong type; an enum value is
invalid. Symptom:
promtool check configexits non-zero; the error message names the field. - Semantic error. The YAML parses, the schema is valid,
the file loads, and the reload succeeds. The targets are
resolved, but they point at the wrong host, the wrong port,
the wrong path, or the wrong scheme. Symptom: the target
appears in
/api/v1/targetswithup == 0and a meaningfullastError;promtool check configexits zero because the schema is fine.
The third class is the most expensive. The first two are
caught by promtool check config; the third is caught by
promtool check service-discovery and the live /targets
API.
Why a sysadmin cares
The reload of a Prometheus configuration is the highest-blast-
radius reload in the observability stack. A bad reload can
silently disable monitoring for a fleet, can corrupt the TSDB
(if a write_relabel_configs change is wrong), or can page the
on-call for a synthetic problem. The cost of running
promtool check config before kill -HUP is one second. The
cost of skipping it is whatever the next incident costs.
Three production pains follow:
- Reload that drops jobs. A schema error causes Prometheus to refuse the entire file. The previous configuration stays in memory, but the operator does not know this and pages the on-call. The discipline is to read the reload response, not just send the signal.
- Reload that loads but does nothing. A semantic error
causes the reload to succeed but the targets to be wrong.
The
upseries stays at 1 for the old targets and at 0 for the new ones, withlastErrorthat names the problem. The discipline is to inspect/api/v1/targetsafter every reload. - Reload that breaks retention. A change to
storage.tsdb.retention.timeorstorage.tsdb.retention .sizethat is wrong truncates blocks the operator did not intend to truncate. The discipline is to validate retention changes against the current blocks before reload.
How it works
The Prometheus configuration load is two-phase: parse, then validate. The parse phase reads the YAML; the validate phase applies the schema and resolves references. Either phase can fail; the failure mode and the recovery differ.
prometheus.yml
|
v
+-----------+
| parse | YAML -> AST; any syntax error fails here
+-----------+
|
v
+-----------+
| validate | AST -> config; schema errors fail here
+-----------+ semantic errors pass through
|
v
+-----------+
| reload | atomic swap of the in-memory config
+-----------+
|
v
scrape pools reshuffle to match the new config
|
v
/api/v1/status/config reflects the new file
A failure in the parse or validate phase is loud: Prometheus
logs the error, refuses to load, and keeps the previous
configuration in memory. A failure in the semantic phase is
quiet: the new file is loaded, the targets resolve to the
wrong thing, and the operator sees up == 0 in the targets
list.
The reload is atomic. There is no window during which Prometheus is using part of the old configuration and part of the new. This is a feature: a bad reload does not corrupt the running state. It is also a trap: a bad reload looks like a successful reload because no error is visible.
Under the hood
How to configure it
A scrape config that surfaces every class of error and is easy to validate:
# /etc/prometheus/prometheus.yml
global:
scrape_interval: 30s
scrape_timeout: 10s
external_labels:
cluster: prod-eu-1
region: eu-west-1
scrape_configs:
- job_name: node
metrics_path: /metrics
scheme: http
scrape_interval: 15s # overrides global for this job
scrape_timeout: 5s # must be less than scrape_interval
static_configs:
- targets:
- 'node-1.internal:9100'
- 'node-2.internal:9100'
labels:
site: lon-2
env: production
relabel_configs:
- source_labels: [env]
regex: production
action: keep
- job_name: app
metrics_path: /actuator/prometheus
scheme: https
scrape_interval: 30s
scrape_timeout: 10s
tls_config:
ca_file: /etc/prometheus/tls/ca.crt
server_name: app.internal
static_configs:
- targets:
- 'app-1.internal:8443'
- 'app-2.internal:8443'
The invariants the configuration has to satisfy:
scrape_timeoutless thanscrape_interval. Violations are silently accepted by the schema but make every scrape race the next interval.metrics_pathstarts with/. The schema accepts bare paths but the scrape URL is malformed.schemeishttporhttps. Anything else is a schema error.- Each
job_nameis unique within the file. Duplicates are accepted by the schema but produce unpredictable behaviour. - TLS
ca_file,cert_file,key_filepaths exist and are readable by the Prometheus user. The schema does not check.
How to validate it
The validation ladder for every configuration change.
# Step 1: parse and schema check (catches class 1 and class 2)
promtool check config /etc/prometheus/prometheus.yml
# expected: SUCCESS; the configuration is valid
# Step 2: service-discovery check for the affected job
promtool check service-discovery /etc/prometheus/prometheus.yml node
# expected: a JSON array of targets with discoveredLabels and
# labels (final). Compare against the previous output.
# Step 3: reload Prometheus (atomic)
kill -HUP "$(pidof prometheus)"
# Step 4: confirm Prometheus loaded the new file
curl -s http://prom:9090/api/v1/status/config \
| jq '.data.yaml' | head -30
# expected: the new file content; compare against the file on disk
# Step 5: confirm the affected targets are healthy
curl -s 'http://prom:9090/api/v1/targets?state=active' \
| jq '.data.activeTargets[]
| select(.labels.job=="node")
| {instance: .labels.instance, health: .health,
lastError: .lastError}'
# expected: health == "up" for every target in the job
# Step 6: confirm the change did not break unrelated jobs
curl -s 'http://prom:9090/api/v1/targets?state=active' \
| jq '.data.activeTargets[] | .labels.job' | sort -u
# expected: the same set of jobs as before the reload
A reload that fails step 4 did not load. A reload that fails step 5 has a semantic error in the affected job. A reload that fails step 6 has broken an unrelated job and the operator must investigate the merge.
How it can fail
Six failure shapes appear repeatedly. The first three are schema errors; the second three are semantic errors.
- YAML indentation or quoting error. Tabs instead of
spaces; an unquoted colon in a value; a value that should
be quoted. Symptom:
promtool check configexits non-zero; the error names the line and column. The fix is to correct the YAML. - Missing required field. A
scrape_configsentry withoutjob_name; atls_configwithca_filebut withoutserver_namewhen the certificate uses SANs that do not include the target IP. Symptom:promtool check configexits non-zero with the field name. The fix is to add the field. - Wrong type.
scrape_interval: 15instead ofscrape_interval: 15s;targets:as a string instead of a list;labels:as a list instead of a map. Symptom:promtool check configexits non-zero with the expected type. The fix is to match the type. scrape_timeoutgreater thanscrape_interval. The schema accepts it; the scrape races the next interval. Symptom: scrapes time out intermittently;scrape_duration _secondsrises toward the interval; the next scrape starts before the previous one finishes. The fix is to lower the timeout or raise the interval.- Wrong
metrics_path. The exporter serves/metricsbut the job says/prometheus/metrics. Symptom:lastError: server returned HTTP status 404;up == 0;promtool check service-discoveryshows the wrong URL under__metrics_path__. The fix is to match the path. schememismatch.scheme: httpsagainst an HTTP-only exporter. Symptom:lastError: server gave HTTP response to HTTPS client;up == 0. The fix is to match the scheme.
How to troubleshoot it
Security implications
The configuration file holds credentials, certificate paths, and target lists. The leak path is the same as for any configuration file: backups, config-management diffs, version control. The discipline:
- Inline credentials are read by anyone with shell on the
Prometheus host. Use
password_fileandcredentials_fileat0600 prometheus:prometheus. - TLS key material is read at config load. Rotate a key and the scrape keeps failing with handshake errors until the reload reads the new file.
insecure_skip_verify: trueis never correct outside a lab. The setting turns off certificate verification for the affected job and exposes the metrics endpoint to a person-in-the-middle.- Treat config write access as production credential-level
access. Whoever can edit
prometheus.ymlcan redirect every scrape.
Performance implications
The configuration is not the dominant cost; the scrape is. But three configuration mistakes produce real performance damage:
sample_limitnot set. The default is 0 (unlimited). An exporter that adds a high-cardinality collector can inflate the series count overnight. Lesson 06 covers the defence.scrape_intervaltoo aggressive for the fleet. A 5-second interval quadruples the cost of a 15-second fleet. Match the interval to the SLA class.- No
scrape_timeoutand tight interval. A slow scrape blocks the next one, which pushes the entire job off-schedule. Lesson on scrape configs covers the discipline.
Production guidance
- Run
promtool check configin CI on every change toprometheus.yml. A pre-commit hook that runs the command catches the error before the operator does. - Run
promtool check service-discoveryagainst a representative target set in CI as well. The semantic errors are not caught bycheck config. - Always reload with
kill -HUPand confirm with/api/v1/status/configthat the file actually loaded. A silent no-op is worse than a loud failure. - Inspect
/api/v1/targetsafter every reload. The targets list is the live evidence that the reload did what was intended. - Keep a copy of the previous
prometheus.ymlon the Prometheus host. Acprollback is faster than agit revertwhen the page is open. - Document the reload procedure in the runbook. The on-call engineer at 02:14 should be able to roll back in under a minute from the runbook alone.
Verification
You should now be able to answer:
- What are the three classes of scrape config error, and how
does each one manifest in
promtooland the Prometheus logs? - Which two
promtoolsubcommands catch which classes of error? - What is the invariant between
scrape_intervalandscrape_timeout, and what is the failure shape when it is violated? - How do you confirm a reload actually loaded the new configuration without restarting Prometheus?
- Why is
insecure_skip_verify: truealmost always wrong, and what is the right alternative?
Quiz
Knowledge check · 8 questions
Q1. A scrape job has scrape_timeout: 20s and scrape_interval: 15s. The most likely symptom is:
Q2. promtool check config passes, but a new job has zero targets. The most likely cause is:
Q3. A reload that fails the parse or validate phase leaves the running configuration in memory unchanged.
Q4. You edited prometheus.yml and sent kill -HUP. /api/v1/status/config shows the previous configuration. The most likely cause is:
Q5. Name the promtool subcommand that exercises the service-discovery stages against a representative target set.
Q6. Which of these are read-only validation steps for a scrape configuration change?
Q7. A scrape job targets an exporter that serves on /actuator/prometheus. The job specifies metrics_path: /metrics. The symptom is:
Q8. The cheapest prevention for a bad scrape config reload is:
Passing score: 75%. Answers are checked in this browser.