skip to content

How do you read a worker's expvar /debug/vars output to tell whether it is still making progress?

level: seniorimportance: nice to knowfreq 26%

answer

  1. one number is a position, not a speed
  2. counters only ever go up
  3. two reads, a known interval apart
  4. the delta is the whole signal
  5. make sure both reads hit one process

basics

~20 s

Fetch the endpoint twice a known number of seconds apart, from the same process, and diff the two documents. expvar publishes raw monotonic counters with no rates or history, so only the delta shows what is still moving.

solid answer

~50 s

expvar hands you the current value of every published variable and nothing else — no rate, no timestamp, no history — so one snapshot cannot tell a busy worker from one that stalled an hour ago. The technique is two reads a known interval apart against the *same* process, diffed key by key: a counter whose delta is zero names the stage that stopped, and comparing a completed counter against an enqueued counter says whether the backlog is draining. Two caveats. The document is rendered variable by variable with no lock held across the whole response, so it is not a consistent snapshot — two counters in one payload can come from different instants. And it is strictly per-process, so behind a load balancer you may be diffing two different replicas; address the instance directly. Publishing a seconds-since-last-success gauge makes one read answer the question outright.

code

text · 5 lines
text
# t = 0s
{"worker.jobs_done":18422,"worker.queue_depth":9013,"worker.seconds_since_success":2}

# t = 10s
{"worker.jobs_done":18422,"worker.queue_depth":9271,"worker.seconds_since_success":12}

go deeper

for a junior

Remember that expvar counters only ever increase, so the value alone is not progress; you need a second reading to compare it against.

for a middle

Explain the diff concretely: same process, known interval, compare per key, and know that a flat counter means that stage did no work in the window.

for a senior

Demonstrate the incident habit — address one instance, take timed reads, and know the endpoint offers no rates, no history and no consistent cross-variable snapshot.

for a principal

Decide what every worker must publish so an on-call engineer can answer "is it stuck" from a single read, and hold teams to that minimum.

## What expvar gives you, and what it does not A request to `/debug/vars` returns the current value of every published variable as one JSON object. That is the entire contract. There is no rate, no timestamp on any value, no ring buffer of past readings, no reset, and no query language. Values published as counters only ever go up, from process start to process exit. This matters during an incident because a large number tells you nothing about the present. `"worker.jobs_done": 18422` is equally consistent with a worker finishing its 18,422nd job a moment ago and with one that finished it before lunch and has been wedged ever since. ## The technique: two reads, diffed The standard move is to fetch the endpoint twice, a known number of seconds apart, and compare the documents key by key. ``` $ curl -s localhost:8080/debug/vars # t = 0s $ sleep 10 $ curl -s localhost:8080/debug/vars # t = 10s ``` What the diff tells you: - **A counter with a non-zero delta** is a stage that did work. Divide by the interval and you have the rate the endpoint refused to give you. - **A counter with a zero delta** is the interesting one: that stage did nothing at all in the window. If `jobs_received` moved and `jobs_done` did not, the worker is taking work in and not completing it — a stall, not slowness. - **Two counters compared** answer the shape question. If a depth gauge grew while a completion counter also grew, the worker is keeping up badly; if depth grew while completions were flat, it is not consuming at all. Taking a third read at a different interval separates "slow" from "stopped" — slow still moves, just less. ## Three things that will mislead you **The snapshot is not atomic.** The handler walks the registry and renders each variable in turn. Each individual read is safe for concurrent use, but no lock is held across the whole document, so a variable near the start of the response can be from a slightly earlier instant than one near the end. For trends this is irrelevant. For exact accounting — "received minus done should equal depth" — it is not: expect the arithmetic to be off by the work of a few microseconds. **It is one process.** expvar has no idea that other replicas of the service exist. If your two reads went through a load balancer they may have hit two different instances, and the "delta" you computed is the difference between two unrelated processes' lifetime totals — which can be negative, and is meaningless either way. Address the instance directly, or at minimum publish something that identifies the process so you can tell when you have been bounced. **A restart resets everything.** Counters live in memory. A process that crash-looped between your two reads shows small numbers, not large ones, and the delta may be negative. Negative delta means restart, not underflow. ## Make one read enough The fix for the whole class of problem is to publish a value that is meaningful on its own. A gauge computed on demand — seconds since the last successful job, derived from a timestamp the worker stores on each success — turns "is it stuck" from a two-read comparison into a single number an on-call engineer can read at a glance. A large value is a stall regardless of how impressive the cumulative counters look. The same trick applies to depth: a current queue depth is self-describing where a cumulative enqueue count is not. This is worth doing before the incident, because during one the person reading the endpoint is often not the person who wrote it, and they may only get one shot at a process that is about to be restarted. ## The cost of scraping Rendering the document is not entirely free. Any variable published as a computed value runs its function on every request, and the `memstats` variable that expvar publishes by itself asks the runtime for a fresh memory reading each time rather than serving a cached one. A tight polling loop pays for that on every iteration. If a scrape is noticeably slower than the rest of the service, suspect a computed variable doing real work rather than the JSON encoding. ## What to remember One read of `/debug/vars` is a position, not a velocity. Two timed reads of the same process give you the velocity. Nothing gives you history, cross-variable consistency, or a view across replicas — and the durable fix is to publish a gauge that means something on its own.

  • Why is a single /debug/vars response not a consistent snapshot of the process?
    The handler renders each published variable in turn. Every individual read is safe for concurrent use, but no lock is held across the whole document, so a variable early in the output can come from a slightly earlier instant than one later in it. Fine for trends; wrong for exact cross-variable arithmetic.
  • What would you add to the worker so that one read answers "is it alive"?
    A computed gauge — an `expvar.Func` returning seconds since the last successful job, derived from a timestamp the worker stores on every success. That converts a monotonic total, which needs two reads to interpret, into a number that is self-describing in one: large means stalled, whatever the cumulative counters say.
  • You diff two reads and one counter is lower the second time. What does that mean?
    The process restarted between the reads, or the two reads hit different instances. expvar counters live in memory and never decrease within one process lifetime, so a negative delta is never underflow — it means you are not looking at the same running process you looked at before.

saying these in an interview costs you the question

  • Reads one snapshot and calls the worker healthy
  • Treats a cumulative expvar counter as a rate
  • Diffs two reads that landed on different replicas
  • Assumes the whole JSON document is one atomic instant
  • Expects expvar to keep history or allow a reset