How do refreshing a cached credential ahead of expiry and re-fetching only after a rejected call fail differently?
answer
- a clock against a verdict
- one fires early, one fires late
- a fraction of remaining validity
- the rejection is what sees a withdrawal
- both triggers, one held slot
basics
~20 sRefreshing ahead of expiry keeps a usable value ready but only tracks the expiry it can see, so it misses a value withdrawn early. Re-fetching on rejection catches exactly that, at the cost of at least one failed call.
solid answer
~40 sThe two triggers watch different things. A **proactive** refresh fires on a clock — a fraction of the credential's remaining validity, or a staleness bound you chose — so a value is always in hand and a failed refresh has slack before anything breaks. Its blind spot is anything that happens off that clock: a value withdrawn or replaced early looks fine until the clock next fires. A **reactive** refetch fires when the other side rejects the call, so it detects early withdrawal immediately, but only by spending a real request, and only if the caller can distinguish a rejected credential from a denied action or a transport failure. Most durable designs run both: the bound caps ordinary staleness, and the rejection path is the correction for anything that happened inside it.
code
pseudocode · 27 lines# one holder per process; both triggers write the same slot
held = { value: null, refreshAfter: 0 }
function credential():
if held.value == null or now() >= held.refreshAfter:
refresh()
return held.value
function refresh():
try:
v = store.fetch(CREDENTIAL_NAME)
except StoreUnreachable:
if held.value == null:
raise # nothing held yet: the caller must fail
held.refreshAfter = now() + RETRY_BACKOFF
return # keep serving what we hold, try again soon
held.value = v
# refresh at two thirds of whichever bound is shorter
life = min(v.expiresAt - now(), STALENESS_BOUND)
held.refreshAfter = now() + (2 * life / 3)
function callProvider(request):
reply = provider.send(request, credential())
if reply.reason == CREDENTIAL_NOT_ACCEPTED and request.safeToRetry:
refresh() # reactive: withdrawn inside the bound
reply = provider.send(request, held.value) # one retry, then give up
return replygo deeper
Know that a held credential needs a rule for going back to the store, and that there are two: a clock, and a call being rejected. Be able to say which one notices a value that stopped working early.
Explain the mechanics of each: refresh at a fraction of remaining validity so a failed refresh has slack, and refetch on a rejection that specifically means the credential is not accepted. Name each one's blind spot.
Show the operational edges — the idle worker that learns nothing, the rejection signal that cannot be distinguished from a denied action, the retry that is unsafe on a call with an effect, and one holder rather than one refresh per in-flight call.
Decide which of the two the estate's guarantee rests on. If the incident promise is 'withdrawn credentials stop working within N minutes', that promise is carried by the bound, and the rejection path is a bonus you cannot count on for idle workloads.
A process that holds a credential for reuse needs a rule that says when to go back to the store. There are only two kinds of trigger available, they watch different events, and a design that carries just one has a predictable blind spot. ## Trigger one: refresh ahead of expiry The process refreshes on a clock. Where the credential carries a stated validity, the usual rule is to refresh at some fraction of the remaining life — say two-thirds — so that a failed refresh still leaves usable time to retry in. Where the value carries no expiry at all, the clock is a **staleness bound** the owner chose, and the refresh is simply periodic. What this buys: - **A valid value is always in hand.** Refreshing early means the failure of one refresh attempt is not yet a failure of the work. - **Retries have somewhere to happen.** With a third of the life still ahead, the process can back off and try again rather than fail live calls. - **The cost is predictable.** One read per bound per process is a read rate you can multiply out before you deploy it. What it does not see: **anything that happens off the clock.** A credential the other side stopped accepting an hour into a twelve-hour validity is, as far as the clock is concerned, perfectly fresh. The process keeps presenting it until the refresh is due. ## Trigger two: re-fetch after a rejection The process uses what it holds until the other side refuses it, then fetches again and retries once. This is the only trigger that observes the thing that actually matters — whether the value still works — rather than a proxy for it. Its costs are real: - **At least one call fails.** Detection is paid for in a rejected request, which may be a customer's request. - **It needs a clean signal.** The caller must be able to tell "this credential is not accepted" apart from "this credential is fine but not allowed to do that" and from a transport failure. Where the other side's error signalling is coarse, a reactive trigger either misses the event or refetches on failures that a new value cannot fix. - **The retried call must be safe to retry.** A rejected call that already had an effect on the other side is not a call you want to send twice. - **An idle holder never learns.** A process that makes no calls for six hours discovers nothing for six hours. ## Side by side | | refresh ahead of expiry | re-fetch on rejection | |---|---|---| | what it watches | a clock: stated validity or a chosen bound | the other side's verdict on a real call | | detects early withdrawal | no, not until the clock fires | yes, on the first call after it | | cost of detection | one read per bound, per process | one failed call | | when a refresh fails | slack remains before work breaks | the call has already failed | | blind spot | anything that changes off the clock | anything that never makes a call | ## Why the durable answer is both Run the bound as the routine mechanism and the rejection as the correction: 1. Refresh proactively at a fraction of the credential's validity, or on the staleness bound where there is no stated validity. 2. On a rejection that specifically means the credential is not accepted, refresh immediately and retry the call once. 3. Have both paths write the **same** held slot, so a rejection-driven refresh also resets the clock and the two triggers cannot fight. That combination gives an upper bound on ordinary staleness plus fast convergence when something happened inside it. Neither property is available from one trigger alone. ## Two details that decide whether this works **The fraction, not the number.** Refreshing "every ten minutes" against a credential whose validity is five minutes is a bug that looks like a policy. Anchor the proactive trigger to the credential's own validity where it has one, and only fall back to an invented bound where it does not. **One refresh per process, not one per call.** If every in-flight call independently notices the rejection and refetches, the correction that was supposed to be cheap becomes a burst against the store at the worst moment. Route both triggers through a single holder so one refresh serves the callers waiting on it. ## What a weak answer sounds like "We refresh when it fails" — with no bound, no statement of what the rejection signal actually is, and no answer to what happens to a process that is idle when the credential is withdrawn. The strong version names the clock, names the rejection signal, and says which of the two catches which failure.
- The credential the store hands back carries no expiry at all. What does 'refresh ahead of expiry' mean then?Nothing — there is no expiry to be ahead of, so the proactive trigger has to fire on a staleness bound the owner picks and can defend. The important part is that the bound is then a choice rather than something the credential told you, so it has to be written down and justified rather than inherited.
- Why is a rejection-triggered refetch unsafe on some calls?Because the retry sends the request a second time. If the other side rejected the credential after already acting on the request, or if the call is not safe to repeat, the retry can duplicate an effect. Gate the retry on the call being safe to repeat, and otherwise refresh the held value but surface the original failure.
- What breaks if the rejection signal cannot be told apart from an authorization denial?The trigger fires on the wrong events and misses the right ones. Refetching because an action was denied wastes a store read and changes nothing, since a fresh copy of the same credential is still not allowed to do it; meanwhile a genuinely withdrawn credential may surface as an error the caller does not treat as a credential problem, leaving the bound as the only trigger that will ever fire.
saying these in an interview costs you the question
- Treats a rejected call as the only trigger a held value ever needs
- Refreshes exactly at expiry, leaving no slack for a failed refresh
- Picks a fixed refresh interval unrelated to the credential's own validity
- Retries every rejected call once without asking whether it is safe to repeat
- Lets every in-flight call refetch independently instead of one holder
- Assumes a clock-based refresh will catch a value withdrawn early