ObservabilityXI · Blackbox MonitoringBlackbox
blackbox_exporter Overview
What you'll learn
- Describe the blackbox_exporter probe model: target, module, and per-probe metric stream
- Name the seven first-party modules and the question each one answers
- Write a blackbox.yml that exposes tcp_connect, http_2xx, icmp, and dns variants as separate modules
- Configure the Prometheus scrape job that drives blackbox_exporter with relabeling
- Identify the four boundaries where a blackbox probe can break and the metric that reveals each
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
At 03:12 the on-call engineer opens a panel: probe_success for
twelve external services. Eleven are green. One is red. The
service inside the red row is up by every internal metric the
team has, but a customer in Frankfurt cannot reach it. The single
panel that resolves the ambiguity is the one driven by
blackbox_exporter. This lesson is the foundation that makes that
panel exist.
What it is
blackbox_exporter is a Prometheus exporter that turns external
protocol probes into the standard Prometheus exposition format.
It exposes a single HTTP endpoint, conventionally /probe, that
accepts a target parameter and a module parameter, performs
the probe, and returns one sample per attempt in the same form
any other exporter would. Prometheus then scrapes that endpoint;
the act of scraping is the probe. The exporter does not pull
targets of its own and does not schedule work; it is queried.
The canonical alternative is smokeping or a generic synthetic
service (Pingdom, Grafana Synthetic Monitoring). The exporter is
the right answer when the team already runs Prometheus, because
the metric shape matches everything else in the stack. A pure
synthetic service is the right answer when the probe must run
from many geographies or from inside a managed browser; the
exporter covers the per-region core probes that run from a host
or container you control.
Why a sysadmin cares
Three production failure classes disappear the day the exporter is in place:
- Route blackouts. The service is up internally; the user-facing URL is down because of a load balancer, a reverse proxy, a CDN edge, or a DNS record. Internal white-box metrics cannot see this.
- External dependency failure. A managed Postgres, an S3-compatible object store, a third-party API. The application is up. The dependency is not reachable from the production path. Internal metrics describe only the application.
- Silent configuration drift. A 503 that only manifests for one percent of users because a default backend fell out of a load-balancer pool at 02:00. The probe catches it within seconds because the probe path is the same path the user takes.
The exporter is a load-bearing component of the observability platform. It is also small, runs in a single container, and costs almost nothing in CPU. The reason to skip it is “we have internal monitoring”; the cost of skipping it is the next incident where the user is right and the internal panel is wrong.
How it works
The exporter exposes one HTTP handler, /probe. Prometheus sends
a request with two query parameters: target, the endpoint to
probe, and module, the named probe configuration from the
exporter’s config file. The exporter executes the probe,
collects per-protocol metrics, and returns them in Prometheus
exposition format. There is exactly one scrape per target; the
exporter does not multiplex internally.
prometheus.yml
|
v
scrape job: blackbox_http
|
| each scrape rewrites the target into
| the static_config on the exporter
|
v
blackbox_exporter :9115/probe?target=X&module=Y
| ----- module: http_2xx / tcp_connect / icmp / dns ----
|
| probe runs, measurements returned as Prometheus metrics
|
v
probe_success, probe_duration_seconds,
probe_http_status_code, probe_ssl_earliest_cert_expiry,
...
The scrape job drives the cadence. The exporter drives the protocol details. Prometheus stores the result alongside every other metric in the platform. Grafana and Alertmanager consume it through the same query and routing pipelines as any other metric.
The module catalogue
A module is a named probe configuration. The exporter ships a small set of first-party modules; each module is a vocabulary for one protocol. The first-party modules and their questions:
http_2xx— open a TCP connection, perform a TLS handshake if the URL scheme ishttps, send an HTTP request, treat any response in the 2xx range (by default) as success. Surfacesprobe_http_status_code,probe_http_content_length,probe_http_redirects,probe_ssl_earliest_cert_expiry.http_4xx— same probe, but success is defined as a 4xx status code. Useful for catching “expected absence” — for example, a page that must return 404.http_xxx— a custom variable: success when the response is in the listed set (e.g.,http_2xxsucceeds on[200, 204]). Useful when the application has a single canonical success status that is not the default.tcp_connect— open a TCP connection totarget:port. No application handshake. Success is a successful connect.icmp— issue an ICMP echo request. Success is any reply within the timeout. RequiresCAP_NET_RAWor root.dns— issue a DNS query for a configured name and type. Validate the answer against regexes and authority sections.ssl— perform a TLS handshake without an HTTP request. Surfacesprobe_ssl_earliest_cert_expirywithout an application probe. The lesson on TLS probes covers this.irc,pop3s,sip,ssh_proxyprotocol— also shipped, used in narrow contexts. Most production deployments use only the six above.
module | protocol question asked
----------------+------------------------------------
http_2xx | does the URL return success?
http_4xx | does the URL return the expected 4xx?
tcp_connect | is a TCP port reachable?
icmp | does the host reply to ping?
dns | does the resolver return the expected answer?
ssl | is the certificate chain valid and fresh?
Each module exposes the same outer metrics (probe_success,
probe_duration_seconds) plus protocol-specific ones. The shape
is the same across modules, which is what makes dashboards and
alerts reusable.
The metric shape
The exporter does not invent a new metric namespace per module. The convention is fixed and stable across versions:
probe_success— gauge, value1on success and0on failure. This is the headline metric.probe_duration_seconds— gauge, total wall-clock duration of the probe including DNS resolution, connect, TLS, and any redirects. Useful for latency drift.probe_failed_due_to_regex— gauge,1if the probe’s response-body or DNS-answer regex did not match,0otherwise.probe_http_status_code— gauge, the final HTTP status code observed.probe_http_redirects— counter, the number of HTTP redirects followed.probe_ssl_earliest_cert_expiry— gauge, Unix timestamp of the earliest certificate expiry across the chain.probe_dns_lookup_time_seconds— gauge, time spent in DNS resolution.
All of these carry the labels Prometheus attaches during the
scrape (instance, job) and any labels the user added via
relabeling. The exporter does not label by itself; module name
goes on as a label through relabeling, never on the wire.
How to configure it
Three layers are required to put a probe into production: the exporter’s own config, the scrape job on Prometheus, and (optionally) the relabeling layer that maps each probe target to a high-cardinality, low-cardinality label set.
1. The exporter config: blackbox.yml
# /etc/blackbox/blackbox.yml
modules:
# HTTP success probe used for the public checkout flow.
# tls_config controls the verifier, not the application TLS.
http_2xx_checkout:
prober: http
timeout: 5s
http:
method: GET
preferred_ip_protocol: ip4 # try IPv4 first
ip_protocol_fallback: true # fall back to IPv6
follow_redirects: true # honour 3xx up to 10 hops
fail_if_body_matches_regexp:
- "checkout disabled"
fail_if_header_matches: # refuses to serve when ...
- header: content-type
allow: false
regexp: "text/html"
- header: cache-control
allow: false
regexp: "no-store"
tls_config:
insecure_skip_verify: false # validate the chain
# TCP connect for database-port reachability (see lesson on TCP).
tcp_connect_pg:
prober: tcp
timeout: 3s
# ICMP for host liveness (see lesson on ICMP).
icmp_router:
prober: icmp
timeout: 2s
icmp:
preferred_ip_protocol: ip4
# DNS probe for the public resolver (see lesson on DNS).
dns_a_shop:
prober: dns
timeout: 3s
dns:
preferred_ip_protocol: ip4
query_name: shop.example.com
query_type: A
validate_answer_rrs:
fail_if_matches_regexp: []
fail_if_not_matches_regexp:
- "^(10\\.20\\.[0-9]{1,3}\\.[0-9]{1,3})$"
# SSL handshake for certificate-only monitoring (see lesson on TLS).
ssl_shop:
prober: http # SSL probes ride the http prober
timeout: 5s
http:
method: GET
preferred_ip_protocol: ip4
ip_protocol_fallback: true
tls_config:
insecure_skip_verify: false
# An http_xxx custom success class.
http_health_200_204:
prober: http
timeout: 5s
http:
valid_status_codes: [200, 204]
method: GET
preferred_ip_protocol: ip4
ip_protocol_fallback: true
Each module is independent. A module can be reused by many
scrape jobs; a target can be probed by many modules. The naming
discipline matters: name the module after the question, not
the protocol. dns_a_shop and dns_aaaa_shop are clearer than
dns1 and dns2.
2. The Prometheus scrape job
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: blackbox_http
metrics_path: /probe
params:
module: [http_2xx_checkout]
scrape_interval: 30s
scrape_timeout: 10s
static_configs:
- targets:
- https://shop.example.com/checkout
labels:
service: shop
env: prod
region: eu-west-1
relabel_configs:
# The exporter needs the real target on every scrape URL.
- source_labels: [__address__]
target_label: __param_target
# Pick up the service label from the static_config.
- source_labels: [service]
target_label: service
# Stamp the module name onto every sample as a label.
- target_label: module
replacement: http_2xx_checkout
# Keep instance set to the prober, not the target.
- source_labels: [__address__]
target_label: __tmp_target_address
- source_labels: [__tmp_target_address]
regex: '(.*):(.*)'
replacement: '${1}'
target_label: instance
The pattern to internalise: __param_target is rewritten from
__address__ so the exporter knows what to probe. The exporter
instance itself (localhost:9115) is what Prometheus scrapes;
the instance label is normally rewritten to the real target so
that alerts group by service, not by prober host.
3. Validation of the exporter config
# The exporter has no built-in check; the binary refuses to start
# with an invalid config, which is itself a check.
blackbox_exporter --config.check --config.file=/etc/blackbox/blackbox.yml
journalctl -u blackbox_exporter --since '5 min ago' | tail -20
# Validate the Prometheus side.
promtool check config /etc/prometheus/prometheus.yml
How to validate it
Three layers must be confirmed: the exporter is up, the scrape is flowing, and the probe actually exercises the path you think it does.
# 1. The exporter is up. Reads /metrics from the exporter itself,
# not from the probe.
curl -sf http://localhost:9115/metrics | grep -E '^blackbox_'
# ... illustrative ...
# blackbox_exporter_build_info{version="0.26.0"} 1
# blackbox_exporter_config_last_reload_successful 1
# blackbox_exporter_probe_total 18
# 2. Smoke-test the probe manually before trusting Prometheus.
curl -sf "http://localhost:9115/probe?module=http_2xx_checkout&target=https://shop.example.com/checkout" \
| grep -E '^(probe_success|probe_duration_seconds|probe_http_status_code)'
# probe_duration_seconds 0.214
# probe_http_status_code 200
# probe_success 1
# 3. Confirm Prometheus is receiving the series.
up{job="blackbox_http"}
# {instance="shop.example.com", job="blackbox_http", service="shop"} 1
probe_success{service="shop"}
# 1
# 4. The /-/ready endpoint of the exporter reports modules loaded.
curl -sf http://localhost:9115/-/ready
# ready
If probe_success is 1 and probe_http_status_code is 200,
the probe is exercising the right path. If probe_success is 0,
inspect probe_failed_due_to_regex and the exporter stderr log
for the underlying reason.
How it can fail
The probe is the simplest component of the platform, and its failure modes are predictable.
-
Wrong target URL.
__param_targetis missing the scheme or carries the exporter’s address instead of the real target. Every probe returnsprobe_success=1againstlocalhost, dashboards stay green, the route outage continues. Symptom: theinstancelabel isblackbox:9115notshop.example.com. -
Module returns success against the wrong question. The module expects JSON but the application returns HTML for a 200; the regex matches accidentally on every page. The probe returns green; the user-facing flow is broken. Symptom:
probe_failed_due_to_regexis0but the application SLO is breached. -
Module timeout shorter than the slowest legitimate response. SRE introduces a new dependency that adds 800 ms on cold cache. The probe fires
for: 1mbefore the dependency responds. Symptom:probe_success=0withprobe_duration_secondsclose to the timeout value. -
ICMP blocked upstream. Many cloud providers and most corporate firewalls drop ICMP beyond the perimeter. The probe gets
probe_success=0for healthy hosts. Symptom:probe_success{module="icmp_router"}=0while every application-level probe is green. -
Relabeling rewrites break cardinality. A relabel rule copies the entire URL into a label. Five hundred targets times the unique path component produces a series storm. Symptom: Prometheus memory rises, TSDB head block churns.
-
The exporter host’s own egress is broken. Every probe fails because the exporter cannot reach the public internet. Symptom: all
servicelabels red at once; the pattern is every target, not one target. This is the signal that the problem is local. -
Module drift on upgrade. A 0.x version renames a regex match key. The old key silently no-ops. The probe keeps returning green. Symptom: behaviour matches an older major release even after a restart with a new binary.
How to troubleshoot it
Order matters. Start at the boundary where you have evidence.
- Is the exporter alive?
curl http://exporter:9115/-/readyandcurl http://exporter:9115/metricsfirst. A missingblackbox_exporter_build_infoseries means the exporter is not the version you think, or it died. Look at systemd / container logs. - Does Prometheus see the exporter?
up{job="blackbox_*"}in PromQL. Ifup=0, the problem is at the scrape boundary, not at the target. - Does the probe itself work? Re-run the exact URL the
Prometheus scrape would produce, against the exporter
manually:
curl "http://exporter:9115/probe?module=...&target=...". The headers and body tell you which boundary failed. - Compare probe paths to internal paths. When a probe
reports failure but
up{job="node"}for the internal service is green, you have isolated the failure to the external path. - Check
probe_duration_seconds. A drift from 200 ms to 4 s whileprobe_success=1is a brownout that has not yet failed; it precedes the failure. - Check the exporter host’s own egress. A
curlfrom the exporter host to the target is the fastest test. - Compare modules against each other. If
tcp_connect_pgis green andhttp_2xx_checkoutis red, the failure is in the application path, not the network. If both are red on one target only, the target is the problem.
Security implications
The exporter accepts arbitrary target query parameters when
exposed without auth. That single endpoint, unauthenticated, can
be coerced into probing any IP or port the exporter host can
reach. In a multi-tenant environment that becomes a port-scan
primitive. Bind the exporter’s /probe to a network Prometheus
can reach but untrusted users cannot, or run it on a private
network.
tls_config.insecure_skip_verify: true makes the probe accept a
TLS handshake against an expired, self-signed, or wrong-host
certificate. A single misconfigured module means
probe_success=1 while every TLS-using user is rejected.
The dashboard is green; the platform is broken. Never ship a
production module with insecure_skip_verify: true unless
the lesson’s TLS section has been read and the trade-off
accepted.
The metric labels are produced by relabeling. A relabel rule that copies a user-controlled URL fragment into a label is a cardinality attack. Whitelist the labels.
Performance implications
The exporter is cheap per probe. The cost scales as the
cardinality of the instance and service label sets, the
scrape interval, and the time budget of the slowest legitimate
response.
- Probe interval. Default 30 s is reasonable. Faster intervals (5 s, 10 s) improve time-to-alert on outages but raise scrape load proportionally.
- Timeout. Each timeout is a goroutine held in the
exporter until the deadline. A misconfigured
timeout: 60son a hundred-target scrape job can occupy the exporter for the entire scrape interval. - Cardinality.
instance=hostplusservice=...plusmodule=...is the recommended minimum. Anything more, like full URLs in labels, is a budget problem. - Timeouts and the scrape budget.
scrape_timeouton Prometheus must be longer than any module’s timeout. A module withtimeout: 5sscraped every 30 s withscrape_timeout: 5sraces against the exporter.
Production guidance
- Treat the
blackbox.ymland the scrape job as production code. Both go through review and CI. - Pin the module names that alerts and dashboards reference. Never rename a module without a coordinated migration.
- Run at least one probe from outside the application’s network (a separate exporter host in a different VPC or region) and one from inside (so the user-path questions differ).
- Validate the exporter config with
--config.check. Validate the Prometheus side withpromtool check config. Both must pass before the change ships. - Add a single SLO:
probe_success == 1for eachservicelabel. Alert when it is0for two scrapes in a row. Do not import the metric into a SLO with a higher bar than the probe can meet.
Verification
You should now be able to answer:
- What problem does
blackbox_exportersolve that white-box metrics do not? - Which module answers each of these questions: “is the checkout page reachable from outside?”, “is TCP port 5432 open?”, “does the host reply to ping?”, “does DNS return the expected A record?”
- How is
__param_targetwired from a Prometheus scrape job to the exporter’s/probehandler? - Which four boundaries can a blackbox probe failure live at, and which metric reveals each?
- What is the operational cost of running the probe path
with
insecure_skip_verify: true?
Quiz
Knowledge check · 8 questions
Q1. What is the primary purpose of blackbox_exporter?
Q2. Which module answers the question "is the application responding with a 200 over TLS"?
Q3. blackbox_exporter probes targets of its own accord and pushes the results to Prometheus.
Q4. In a Prometheus scrape job for blackbox_exporter, what role does the __param_target relabel rule play?
Q5. Name the gauge metric that records whether a probe succeeded.
Q6. Which of these are first-party modules shipped with blackbox_exporter? Select all that apply.
Q7. Where should you look first when probe_success drops for every target at once?
Q8. Setting tls_config.insecure_skip_verify: true on a production module results in:
Passing score: 75%. Answers are checked in this browser.