Revoked a Sume API key and it still works? Up to 15 seconds

After you revoke a Sume API key, the API can keep accepting it for up to 15 seconds by default, because the key lookup is cached. What that means for a leak.

5 min readSume
All posts

Yes: in current Sume code a revoked API key can keep working for up to 15 seconds, because the API caches the key lookup and the cache is not cleared when you press Revoke. After that window the key is refused for good, and the Authentication docs list a missing, malformed or revoked key as a 401.

That gap is small, but it matters if the reason you are revoking is a leak. This post explains where the 15 seconds comes from, what a caller can do inside it, and how to measure it on your own key. The figures come from apps/api/src/api-key-cache.ts and apps/api/src/auth.ts on the main branch, read 2026-10-10. The deployment can set its own value, so treat 15 seconds as the code default, not as a promise.

Why a revoked key is still accepted

Every authenticated request has to look the key up in the database. That lookup was the hottest query in the API, so the API keeps a short-lived in-memory cache of the result, keyed by a hash of the key. A cache entry is written when the key is looked up and expires a fixed time later. It does not slide forward when the key is used again.

The code comment states the trade directly: caching a credential trades revocation latency for throughput, so a key revoked in the dashboard stays accepted until its entry expires. The window is a security setting, not a speed setting, and setting it to 0 turns the cache off. Revoked records are cached as well, so once the API has seen a key as revoked it does not keep re-reading the database to learn that again.

Key-lookup cache defaults, from apps/api/src/api-key-cache.ts (read 2026-10-10)
SettingDefaultWhat it controls
Known key TTL15,000 msHow long a looked-up key record, including a revoked one, is reused
Unknown key TTL5,000 msHow long a key the ledger did not find is remembered as unknown
Max entries5,000Size cap on the in-memory map; expired entries go first, then the oldest
TTL set to 0Cache offEvery request reads the ledger again, so revocation is immediate

What a caller can do in the window

Anything the key's scopes allow, until the entry expires. That includes paid submits. A job accepted at second 10 is a real job with a real charge, and revoking the key afterwards does not undo it. The window is at most one TTL from the moment the API last filled the entry, so the worst case is revoking right after a fill.

Two details narrow the risk. First, revocation is not a refund path in either direction: spend made by the key stays attributed to it, as covered in revoked key spend stays attributed. Second, the cache lives in the memory of each API process, so you cannot assume every instance expires at the same second. Plan on the longest case.

Measure the window on your own key

Use a throwaway key, not a production one. The script calls GET /v1/me, which the Authentication docs show as the key check, prints the status every second, and stops when the API answers 401 or 403. Each poll is a read, so it uses the read budget, not your submit budget. Run it, revoke the key in the API Keys dashboard, press Enter, and read how many seconds pass before the refusal.

import os, time, urllib.request, urllib.error

def status(key):
    req = urllib.request.Request(
        "https://api.sume.com/v1/me",
        headers={"x-api-key": key},
    )
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            return resp.status
    except urllib.error.HTTPError as err:
        return err.code

def main():
    key = os.environ.get("SUME_API_KEY", "")
    if not key:
        raise SystemExit("Set SUME_API_KEY to the key you will revoke")
    input("Revoke that key in the dashboard, then press Enter: ")
    start = time.monotonic()
    while True:
        code = status(key)
        print(f"{time.monotonic() - start:5.1f}s  HTTP {code}")
        if code in (401, 403):
            break
        time.sleep(1)

main()

A rotation order that does not depend on the window

The documented rotation is: create a replacement key, deploy it, verify it with GET /v1/me, then revoke the old key. Because the old key may live for one more TTL, finish the steps below in order and do not count revocation as instant.

  • Stop the code that uses the old key first, so nothing legitimate is in flight when you revoke.
  • Revoke, then wait at least 15 seconds before you call the old key dead. Check it with the script above or one GET /v1/me.
  • If the key leaked, list recent jobs after the wait and cancel anything queued that you did not start. Exposed API key: what to do covers the usage and spend check.
  • Do not rotate in a loop because of a 503 api_key_auth_unavailable. That is a Sume-side condition, not a bad key, as explained in 503 api_key_auth_unavailable.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume