skip to content

Inventory & Global Revocation

Users expect a list of where they are signed in and a button that ends all of it. Interviewers ask how a bulk kill stays cheap and why the current device usually survives it.

on this pageshow

questions

3

How do you end every server-side session for one user without deleting each record, while sparing the device in their hand?

level: seniorimportance: must knowfreq 55%

answer

  1. comparison, not enumeration
  2. monotonic number on the user record
  3. every session record carries a copy
  4. one write kills the whole set
  5. spare the caller by re-issuing

basics

~20 s

Keep a monotonic version on the user record and a copy in every session record. Bumping it invalidates the whole set in one write, and the per-request check becomes a comparison. The counter cannot spare a row, so re-issue the current device after the bump.

solid answer

~50 s

Put a monotonic integer — call it the session version — on the user record, and store a copy of its value in every session record you create. The per-request check already reads the session record; it now also compares that stored copy with the user's current value, and a mismatch means the caller is no longer authenticated. Revoking everything for one subject is then a single atomic increment rather than a scan-and-delete over an unknown number of rows, and it covers rows created while the bump is in flight. The price is that the counter is coarse: it addresses a subject, not a session, so it cannot exempt the device the user is holding. You spare that one by retiring its record and issuing a fresh one carrying the new value. And invalidated is not deleted — the dead rows still have to be reaped.

code

pseudocode · 23 lines
pseudocode
function revoke_all_sessions(subject, current_reference):
    # one atomic write ends every session record for this subject
    new_version = atomic_increment(user_store.session_version, subject)

    # the counter addresses a subject, not a row: it cannot exempt one.
    # spare the caller by retiring its record and issuing a fresh one.
    session_store.delete(current_reference)
    fresh = session_store.create(subject         = subject,
                                 session_version = new_version,
                                 created_at      = now())
    return set_session_reference(fresh.id)


function authenticate(request):
    record = session_store.read(reference_from(request))
    if record == null:
        return unauthenticated()                       # 401 / sign-in redirect

    if record.session_version != current_version_of(record.subject):
        session_store.delete(record.id)                # reap the row we touched
        return unauthenticated()                       # not 403: identity unknown

    return principal(record.subject)

go deeper

for a junior

Recall that ending every session for one user can be one write rather than many deletes, and that the device the user is holding is kept signed in by giving it a new session, not by skipping it.

for a middle

Explain the mechanics: a monotonic value on the user record, a copy in each session record, and a comparison on every request. Be able to say why the copy is captured at creation and never rewritten.

for a senior

Demonstrate the production judgement — atomicity against a mid-sweep write, the bump-then-re-issue ordering, reaping the invalidated rows, filtering the list by version, and the status code a failed comparison earns.

for a principal

Own the trade: the counter buys constant-cost revocation and gives up per-session granularity, so the system needs a second mechanism when one named session must die. Decide which you carry and what the coarse one commits you to operationally.

## Why not simply delete the rows The obvious implementation of *sign out everywhere* is to list the subject's session records and delete each one. For a yard supervisor with four tablets that works. It stops being adequate for three reasons, all of which an interviewer will reach for: - **It is not atomic.** Between the read that lists the rows and the last delete, a request carrying a still-valid reference can create or refresh a record, and that session survives a button the user believes ended everything. - **It scales with the wrong number.** The cost is proportional to how many sessions the subject has, and the pathological case is not one supervisor pressing a button but a deprovisioning run doing it for a whole depot at once. - **It needs an index you may not want.** Enumerating by subject means keeping every session record findable by subject, which is a second access path on a structure otherwise read only by its own key. ## The version counter The alternative is to make invalidation a **comparison rather than a deletion**. Keep a monotonic integer on the user record — a session version — and copy its current value into every session record at creation. On each authenticated request the check becomes two steps: 1. Load the session record named by the presented reference. No record, no session. 2. Compare the record's stored version with the subject's current version. Different, and the caller is not authenticated. Revoking everything is now one atomic increment of a single field on a single row. Nothing is enumerated, nothing is scanned, and a session record written a millisecond before the increment lands is invalidated by it just as surely as one written a week earlier, because it carries the old value. | | Enumerate and delete | Bump a version | |---|---|---| | Writes to revoke | One per session record | One | | Atomic across the set | No — rows created mid-sweep survive | Yes — a single field decides | | Can spare one session | Yes, by skipping its row | No — the value addresses a subject | | Per-request cost | Unchanged | One more value to have on hand | | Cleanup | Done by the delete itself | Still owed: rows are invalid, not gone | ## Sparing the device in their hand Users pressing *sign out everywhere else* expect to stay signed in where they are pressing it. The counter cannot deliver that directly — there is no way to exempt one row from a value that belongs to the subject — and the attempt to bolt on an exemption flag is how this design rots: an exempt row is a row the next bump also fails to kill. The honest move is **delete and re-issue**, in this order: 1. Increment the subject's version and read the new value. 2. Delete the caller's own session record, so the reference the browser is holding is dead like every other. 3. Create a fresh record carrying the new version, and set the new session identifier on the same response. The caller experiences continuity; the value in the cookie is not the value it was a moment ago. Doing it in this order matters: bump first, so a race in the opposite direction cannot leave the new record carrying a stale version, and re-issue rather than reuse, so a reference that may have leaked is retired along with the rest. ## Invalidated is not deleted The records the bump killed are still in the store, still occupying space, and still visible to anything that reads them naively. Two obligations follow: - **Reap them.** A sweeper removes records whose stored version is behind the subject's current one, and the per-request check can delete a record the moment it finds one stale, which cleans the hot rows for free. - **Filter the devices list by version at read time.** Otherwise the user presses the button, sees the list unchanged, and reports it as broken — the rows are dead but the surface is still drawing them. ## What the check costs, and when it is not instant The comparison is only as immediate as the value it compares against. Read the subject's current version authoritatively on each request and the kill takes effect on the very next request. Serve that value from a cache instead and the kill lags by however long the cache may hold a stale copy — which is a real and often correct choice, and a decision to make deliberately rather than discover during an incident. One detail worth getting right in the answer: a request whose session record fails the version comparison is **not** a `403 Forbidden`. The server no longer knows who is calling, so it is `401 Unauthorized` on an API or a redirect to sign in on a browser surface. `403` would mean the caller is identified and refused, which is precisely what is no longer true.

  • What happens to the session records the bump invalidated?
    They stay in the store until something removes them. Give the per-request check a delete on the mismatch branch so active rows clean themselves, and run a background sweep for rows whose version is behind the subject's current one and are never touched again. Without both, the store grows and cold rows linger indefinitely.
  • The user presses the button and the devices list still shows every tablet. What went wrong?
    The list is reading rows without comparing their stored version against the subject's current one. The sessions are genuinely dead — the next request from any of them fails — but the surface is drawing corpses. Filter the list by version at read time, and reap in the background.
  • Why increment the version before re-issuing the caller's record rather than after?
    So the fresh record cannot be created carrying the old value. If you create first and bump second, the record you just issued is one of the rows the bump invalidates, and the user who pressed the button is the one signed out. Bumping first makes the ordering unambiguous.

A yard runs on paper gate passes, each printed with a shift date that also stands on the board at the gate. When a batch of passes goes missing the supervisor does not chase every pass around the sidings: a new date goes up on the board, and every pass printed before it stops matching. Anyone legitimately on site collects a reprinted pass on the way past. One change at the board, no enumeration, and the person standing at the gate is not exempted — they are simply handed a new pass.

saying these in an interview costs you the question

  • Adds an exempt flag so the current row survives future bumps
  • Believes a scan-and-delete is atomic across the subject's rows
  • Treats invalidated records as removed and skips any reaping
  • Returns 403 to a request whose session version no longer matches
  • Expects one counter to end a single named session
  • Reuses the caller's old session identifier after the bump
open as a page

What must each row of a signed-in-devices list carry, and how far can a user trust those fields?

level: middleimportance: should knowfreq 45%

basics

~20 s

Four fields do the work: device label, last-seen time, approximate location, and which row is the current session. Only the last is a client fact the server establishes itself; the label is asserted by the client, the location inferred from an address.

open as a page

A per-user session version is compared on every request; where do you hold it, and what revocation lag do you accept?

level: principalimportance: should knowfreq 35%

basics

~20 s

Read it wherever the request already loads the user, and the kill is immediate. Cache it and the kill lags by the cache lifetime, so pick that lag against the threat behind the button, write through on the revoke path, and publish the number.

open as a page