Metrics from Short-Lived Jobs with the Pushgateway
A scrape-based metrics system asks a process for its current values on an interval. A job that runs for four seconds is almost never up when that happens, so its metrics endpoint is read approximately never. This page covers pushing instead: the registry, the grouping key that decides what is overwritten, the timestamp that makes alerting possible, and the several ways this component is misused. It is a task article under telemetry from serverless and batch Python, part of the Python telemetry pipelines and delivery section.
Prerequisites
pip install "prometheus-client>=0.20.0,<1.0.0"
export PUSHGATEWAY_URL="http://pushgateway.observability.svc:9091"
export JOB_NAME="nightly-reconciliation"
Implementation
Step 1 — Use a registry local to the job. The default registry accumulates whatever any imported library has registered, including platform collectors describing the Python process. Pushing all of that means the gateway serves a job's process metrics forever after it exits, which is both misleading and a source of series nobody wants. A local registry contains exactly what you put in it.
from prometheus_client import CollectorRegistry, Counter, Gauge
registry = CollectorRegistry() # NOT the default registry
rows_processed = Counter(
"job_rows_processed_total", "Rows processed by this run",
registry=registry)
rows_rejected = Counter(
"job_rows_rejected_total", "Rows rejected by this run",
registry=registry)
duration = Gauge(
"job_duration_seconds", "Wall-clock duration of the run",
registry=registry)
last_success = Gauge(
"job_last_success_timestamp_seconds", "When this job last completed successfully",
registry=registry)
Step 2 — Choose a grouping key that identifies the job. The gateway stores pushed metrics under a grouping key, and a push replaces everything under that key. A key of job name alone works for a singleton job; job name plus a stable instance works for a job that runs in several places. Including a run identifier or a timestamp in the key is the classic mistake: every run creates a new permanent group, and the gateway accumulates them without limit.
Step 3 — Push once, at the end, with a success timestamp. The timestamp is what makes alerting possible. Because the gateway keeps serving the last values indefinitely, no metric goes missing when a job stops running — the only observable change is that a timestamp stops advancing.
import time
from prometheus_client import push_to_gateway
started = time.time()
try:
result = run_reconciliation()
rows_processed.inc(result.rows)
rows_rejected.inc(result.rejected)
finally:
duration.set(time.time() - started)
last_success.set_to_current_time() # only on the success path in real code
push_to_gateway(
os.environ["PUSHGATEWAY_URL"],
job=os.environ["JOB_NAME"],
registry=registry,
grouping_key={"instance": os.environ.get("SHARD", "0")},
)
Expected Output: the gateway serving the job's values, ready for the next ordinary scrape.
# TYPE job_rows_processed_total counter
job_rows_processed_total{job="nightly-reconciliation",instance="0"} 100000
job_rows_rejected_total{job="nightly-reconciliation",instance="0"} 158
job_duration_seconds{job="nightly-reconciliation",instance="0"} 41.208
job_last_success_timestamp_seconds{job="nightly-reconciliation",instance="0"} 1.789e+09
Step 4 — Use replace semantics, not add. A push with the same grouping key replaces the previous values, which is what you want: a rerun corrects the record. The additive variant exists for the case where several independent producers contribute to one group, and using it by mistake produces counters that climb without bound across runs and a duration that is the sum of every run ever.
Step 5 — Delete the group when the job is retired. Because the gateway serves values forever, a decommissioned job continues to report a successful run from whenever it last executed. Anything alerting on the age of that timestamp will fire eventually — which is the good outcome — but a dashboard showing the job as healthy is actively misleading in the meantime.
# remove a retired job's group explicitly
curl -X DELETE "$PUSHGATEWAY_URL/metrics/job/nightly-reconciliation/instance/0"
Step 6 — Alert on the timestamp, never on the metric. This is the consequence of everything above. job_last_success_timestamp_seconds aged past the expected interval is the only reliable signal that a job is not running, because every other metric keeps its last value indefinitely.
When to use something else
The Pushgateway solves a specific problem and is frequently reached for when a different tool fits better. Three alternatives deserve consideration first.
Span attributes. If the job is already producing a trace — and it should be, per telemetry from serverless and batch Python — then the counts can live as attributes on the run span. There is no extra component, the numbers are attached to the run that produced them rather than to a label set, and high-cardinality values such as a run identifier are free. The cost is that deriving a time series from them depends on the backend supporting it.
Structured log records. A single completion record carrying the counts is the simplest possible mechanism, works with any log pipeline, and is trivially queryable. Many teams find that a job's numbers are consulted a handful of times a month, in which case a log query is entirely adequate and the metrics pipeline adds nothing.
The OpenTelemetry metrics SDK with a push exporter. The metrics SDK pushes by design, so a job that exports OTLP metrics with an explicit flush at the end needs no gateway at all. This is the natural choice for a fleet already sending OTLP, and it avoids the grouping key semantics entirely, since the collector applies its normal aggregation.
The case where the gateway is genuinely the right answer is narrow but real: an existing Prometheus deployment is the only consumer, the numbers must be a proper time series, and adding an OTLP path for one job is disproportionate. Recognising that case rather than defaulting to it saves a component and a class of subtle failures.
The properties that surprise people
Three behaviours of this component account for nearly every support question about it, and all three follow from one design decision: it is a cache of last-known values, not a time series database.
It does not record history. Pushing a value every hour does not create an hourly series inside the gateway; it overwrites. The series exists only because Prometheus scrapes the gateway on its own interval and stores each observation. If the scrape interval is longer than the job's period, runs are silently skipped — two runs between scrapes means the first is never recorded anywhere. Matching the scrape interval to the job frequency, or accepting that only the most recent run is captured, is a decision to make deliberately.
It has no notion of the job being over. A metric pushed once is served until something deletes it. There is no expiry, no staleness marker and no way for the gateway to know that a job has been decommissioned. This is why the success timestamp is not an optional extra: it is the only mechanism by which absence becomes observable.
Its labels override the scrape's. Metrics served by the gateway carry the job and instance labels from the grouping key, and Prometheus must be configured with honour-labels enabled for those to survive the scrape rather than being replaced with the gateway's own target labels. Getting this wrong produces metrics that all appear to come from the gateway itself, which makes them impossible to attribute to the job that produced them — a configuration error that looks like an instrumentation error.
Configuration options
| Decision | Recommended | Failure if wrong |
|---|---|---|
| Registry | a local CollectorRegistry |
platform metrics served forever |
| Grouping key | job name plus stable instance | unbounded group accumulation |
| Push mode | replace | counters that climb across runs |
| Success timestamp | always pushed | no way to alert on absence |
| Deletion on retirement | explicit DELETE |
a dead job reporting health |
| Alert basis | timestamp age | an alert that can never fire |
| Push timing | once, at the end | partial values from a failed run |
Verification
Confirm the gateway holds exactly one group per job, and that the timestamp advances.
# what the gateway is currently serving
curl -s "$PUSHGATEWAY_URL/metrics" | grep -E '^job_(last_success|duration)'
# how many distinct groups exist — should equal the number of jobs, not runs
curl -s "$PUSHGATEWAY_URL/api/v1/metrics" \
| python3 -c 'import json,sys; print(len(json.load(sys.stdin)["data"]))'
Expected Output: one group per job, with a timestamp from the most recent run.
job_last_success_timestamp_seconds{job="nightly-reconciliation",instance="0"} 1.789e+09
job_duration_seconds{job="nightly-reconciliation",instance="0"} 41.208
4
A group count that grows between checks means the grouping key includes something that varies per run, which is the failure from step 2 and gets expensive quickly.
Common mistakes
Pushing the default registry. Error signature: process metrics from a job that exited hours ago, served as current. Root cause: the default registry includes platform collectors. Remediation: build a registry containing only the job's own metrics.
A run identifier in the grouping key. Error signature: a gateway whose memory and scrape size grow every hour. Root cause: every run creating a permanent group. Remediation: job name plus a stable instance; the run identifier belongs on a span or a log record.
Alerting on the metric rather than its age. Error signature: an alert that never fires, for a job that has not run in a month. Root cause: the gateway serving stale values indefinitely. Remediation: alert on the age of the success timestamp.
Using the additive push mode. Error signature: a duration metric of several thousand seconds for a job that takes forty. Root cause: values added to the existing group rather than replacing it. Remediation: use the replacing push, which is the default in the client library.
Never deleting retired jobs. Error signature: a dashboard showing healthy jobs that no longer exist. Root cause: groups persisting after decommissioning. Remediation: delete the grouping key as part of retiring the job, and audit the gateway's group list periodically.
Frequently Asked Questions
Why not just expose a metrics endpoint from the job?
Because nothing will scrape it. A scrape happens on an interval, and a job that runs for four seconds will almost never be up when one occurs. The endpoint exists, is never read, and produces the impression of instrumentation without any of its effect.
What is the grouping key and why does it matter?
It is the set of labels that identifies a group of pushed metrics in the gateway. A push replaces everything under the same key, so a key that includes a run identifier accumulates one group per run forever, while a key of job name plus a stable instance overwrites cleanly.
Do pushed metrics disappear when the job finishes?
No, and that is the property to understand. The gateway keeps serving the last pushed values indefinitely, so a job that stopped running entirely continues to report its final success. Alerting must therefore be on the age of the success timestamp, not on the value itself.
Is the Pushgateway the only option?
No. Recording the job's numbers as span attributes or structured log fields and deriving metrics downstream avoids the extra component entirely, and keeps the numbers attached to the run that produced them. The gateway is the right answer when an existing Prometheus deployment is the only consumer.