A scheduled meter-polling service holds its own machine token — why cache it instead of acquiring one per outbound call?
answer
- one acquisition, many calls
- the window the server already granted
- seconds from receipt, not a timestamp
- compute the absolute expiry once
- still a bearer secret in memory
basics
~20 sA machine token stays valid for the whole expires_in window the authorization server granted, so one acquisition serves thousands of polls. Acquiring per call adds a round trip to every request and hammers a shared, rate-limited endpoint for nothing.
solid answer
~50 sThe token a service holds for itself is not tied to a request or to a person: it is a credential with a fixed lifetime, and every poll inside that lifetime can present the same bytes. A token response carries `expires_in` as **seconds counted from when the response was issued**, so the cache entry stores the token plus an absolute instant you compute once at receipt — receipt time plus `expires_in`, less a safety margin — rather than the raw number. Acquiring per outbound call buys nothing: it puts a full round trip in front of every meter read, turns a quiet dependency into your busiest one, and collides with per-client issuance limits at exactly the moment every site controller is polling. Cache in process by default, never log the token, and treat a rejection from the meter service as a reason to re-acquire once — not as a reason to abandon the cache.
code
json · 12 lines{
"access_token": "<secret bytes - never logged, never persisted by default>",
"token_type": "Bearer",
"scope": "meter.read",
"expires_in": 3600,
"_derived_at_receipt": {
"received_at": "2026-09-19T05:30:02Z",
"not_after": "2026-09-19T06:30:02Z",
"serve_until": "2026-09-19T06:29:02Z"
}
}go deeper
Recall that a machine caller has no session: it holds a credential and a token with a fixed lifetime, and the token is reused for every call inside that lifetime.
Explain that expires_in is seconds from issuance, so the holder computes an absolute expiry at receipt and subtracts a margin for clock skew and in-flight time.
Show the operational consequences: the metrics that reveal a stale cache, why a rejection that is not about expiry must not trigger a retry loop, and why acquisition failures should be visible separately from call failures.
Frame the cache as a coupling decision. It decides whether an authorization-server outage stops your fleet immediately or an hour later, and whether the token endpoint is on your critical path for every request or once per lifetime.
## The two things a machine caller holds A site controller that reads a utility's meter service on a schedule has no browser, no cookie and nobody sitting in front of it. Its identity is a **client registration** at the utility's authorization server, and what it presents to the meter service is a short-lived **access token** obtained against that registration. There is no session behind it and nothing to refresh with: when the token runs out, the service obtains another one. That single fact puts a cache at the centre of the design rather than making it an optimisation added later. The entry is small, and its shape matters more than its size: - the token bytes, treated as a secret for their whole life; - the `token_type` that says how they must be presented; - the **absolute** instant they stop being useful, computed once at receipt; - the earlier instant at which you intend to replace them; - the identity of what they are good for — the registration, the requested `scope` and the callee — which is the cache **key**, not a field inside the entry. ## Why acquiring per call is the wrong default | dimension | acquire per call | serve from cache | |---|---|---| | latency per poll | two round trips: authorization server, then meter service | one round trip to the meter service | | load on the token endpoint | one request per meter read, peaking when every controller wakes | one request per token lifetime per key | | exposure to throttling | every poll depends on a shared, rate-limited endpoint | only the refresh depends on it | | blast radius of an authorization-server outage | total: no reads at all | none until the held token expires | | failure diagnosis | two dependencies fail as one symptom | the failing dependency is visible on its own | The last row is underrated. With a cache, an authorization-server problem shows up as *acquisition failures* while meter reads keep succeeding, and the two signals stay separable. Without one, every incident looks the same. ## `expires_in` is a duration, not a deadline RFC 6749 defines `expires_in` as a **number of seconds**, relative to when the response was produced. Two mistakes follow from forgetting that: 1. **Storing the raw number and comparing it to a clock.** Three thousand six hundred is not a timestamp. The entry must record the instant of receipt, or the derived expiry, at the moment the response lands. 2. **Assuming your clock and the issuer's agree.** They do not exactly. Anything you compute from `expires_in` should carry a margin larger than the skew you tolerate plus the time an acquisition takes at its worst, so the token you serve is still accepted when it arrives at the meter service rather than expiring in flight. The value is also **RECOMMENDED rather than REQUIRED** by the specification. If a response omits it, you have been given no promise at all: pin a short, conservative local lifetime from configuration and let a rejection be the authoritative signal. ## Failure shapes a cache introduces - **Expired in flight.** The token was valid when read from the cache and stale when it arrived. The correct handling is one re-acquire and one retry, and the margin above is what keeps this rare. - **A rejection that is not about expiry.** If the meter service refuses the call because the token does not carry the authority the endpoint needs, re-acquiring produces an identical token and the retry loop becomes the incident. Distinguish *I do not accept this credential* from *this credential is not enough* before you retry anything. - **Silent staleness.** A cache with no metric on the age of what it serves hides a refresh that has been failing for an hour. Export the remaining life of the entry you served, not just a hit rate. - **Leakage.** The token is a bearer credential: anything that presents it is treated as the service. Keeping it in a log line, an error report or a crash dump hands the meter service's data to whoever reads those. A cache is exactly the component where that slip is easiest to make, because the token now has a name and a lifetime and looks like ordinary state. ## What the cache is not It is not durable state. The default is per process and in memory, and it dies with the process — which is correct, because the loss costs one acquisition. Writing it to disk or to a shared store converts a short-lived secret into a stored one that inherits the handling burden of the credential itself, and is only worth it when acquisition is genuinely scarce. It is also not a place to hold a token obtained on somebody's behalf: the entry here belongs to the service, and nothing in it identifies a person.
- The token response omits `expires_in` entirely. What do you cache?You cache the token with a lifetime you chose, not one you were told. Pin a short conservative TTL in configuration, well under any lifetime the provider has documented, and treat a rejection at the meter service as the authoritative signal to re-acquire once. Never default to an hour because that is what the last provider did.
- Should the cached machine token ever be written to disk or to a shared store?By default no. In-process is correct: losing it costs one acquisition. Persisting it turns a short-lived secret into stored data that needs the same protection, access control and deletion story as the credential behind it, and a shared copy means one leak spans the fleet. It is justified only where acquisition is genuinely scarce — a harsh issuance quota — and then the store is a new dependency to reason about.
- How do you tell an expired token from a token that never had the authority?By the shape of the refusal, not by retrying. A challenge that says the credential is not accepted points at expiry or a signature problem and a single re-acquire is reasonable; a refusal that says the caller lacks what the endpoint requires means an identical token will be refused identically, so the fix is in what was requested, not in the cache.
saying these in an interview costs you the question
- Acquires a fresh token before every outbound call because it feels safer
- Stores the raw expires_in number and compares it against a wall clock
- Keeps the token until a call fails, with no margin for skew or in-flight time
- Logs the cached token at debug level to prove the cache works
- Retries the same rejected call in a loop without asking why it was rejected
- Assumes every provider returns expires_in, so a missing value means forever