Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why → how

Why constant-time comparison is a thing

An ordinary equality check leaks the secret it's supposed to protect — one byte at a time, through the clock. Constant-time comparison exists because == is faster than it should be.

Security intermediate Apr 29, 2026 · updated Aug 25, 2026 · 10 min read

On this page

The picture version

Four pictures for a reader who has wondered why == isn’t good enough. The prose below fills in the seams the pictures skip.

1 · The problem

The clock answered a question nobody asked it.

a username nobody has fast a real one, wrong password a visible beat longer ← that account exists Nobody told you which accounts exist. The delay did. the server did more work in one case than the other, and “how long did that take” turned out to be a channel nobody meant to open now shrink the same idea down to a single line of code
That is a timing side channel at the scale of a whole request. The rest of this post is the same bug at the scale of one comparison — and the login form is worth holding on to, because it is the version you can see without instruments.

2 · The single line

Because == stops at the first byte that differs.

the real tag, and three guesses real    guess A guess B guess C rejected after 1 byte rejected after 2 rejected after 3 — slowest each extra matching byte costs one more iteration — and one more tick Fix byte 0, sweep byte 1, repeat. The tag falls out one byte at a time. they never stole the key — they read the comparison’s own reply through the clock and a linear search of 256 values per byte is nothing next to guessing the whole tag at once
Early exit is the obviously correct way to write a comparison — why keep checking once you know the answer? It is a disaster the moment one of the two strings is a secret, and it’s why compare_digest and friends exist in your standard library.

3 · The fix

Touch every byte, and record the answer with arithmetic instead of a branch.

walk to the end every time, whatever you find a[i] b[i] ⊕ diff |= a[i] ^ b[i] XOR is zero exactly when two bytes match, and OR keeps any non-zero bit forever There is still one branch at the end — and that one is fine. it branches on a value that depends on the whole comparison, not on any one position — so it tells the attacker only what they were always going to be told Constant-time doesn’t mean no branches. It means no branch on a secret.
The fuller rule: no branch, no memory access, and no instruction whose duration depends on secret data. Which is why the hard part isn’t writing this loop — it’s stopping the optimiser from noticing that diff can never go back to zero and helpfully restoring the early exit.

4 · Keep this card

The whole thing on one index card.

constant-time compare = always touch every byte + combine the results without branching on a secret ∴ use the one in your standard library, not one you wrote
Picture to keep: a bouncer who reads every name on the guest list all the way to the bottom before saying yes or no — even when the very first name already settled it — so nobody outside can time the door and work out how far down the list their guess got. Where the analogy breaks: a bouncer only has to control his own behaviour. Your code also has to survive a compiler that would happily “help” by restoring the early exit, and a CPU whose caches make some reads faster than others.

Why it exists

You have probably met a login form that rejects a made-up username the instant you hit enter, but takes a visible beat longer when the username is real and only the password is wrong. Nobody told you which accounts exist. The delay did — the server did more work in one case than the other, and “how long did that take” turned out to be a channel nobody meant to open.

Now shrink that idea down to a single line of code. The first time you read security code and see hmac.compare_digest(a, b) instead of plain a == b, it looks paranoid. The two strings are right there. Why not just compare them?

Because == short-circuits.

The usual way to compare two byte strings — and what mainstream languages generally do for == on strings and byte arrays — is to walk them left-to-right and bail out the moment a byte mismatches. That’s the obviously correct way to write the function — why keep checking after you already know the answer? — and it’s a disaster the moment one of those strings is a secret.

Imagine a server that authenticates webhook calls by checking an HMAC in a request header against the expected value. An attacker who controls the header can submit guesses and time how long the comparison takes. A guess whose first byte matches the real tag takes a tiny bit longer to reject than one whose first byte is wrong, because the loop ran one more iteration. The attacker fixes byte 0, sweeps byte 1, and so on. They have not stolen the secret key; they have read the comparison’s reply byte by byte through the clock.

That whole class of bug is called a timing side channel, and it’s the reason compare_digest, crypto.timingSafeEqual, and subtle.ConstantTimeCompare exist in your standard library.

Why it matters now

Two reasons it keeps mattering.

First, the network is no longer the noise barrier it used to be. The classic objection — “you can’t actually measure nanosecond differences over the internet” — was always an overstatement, and modern infrastructure makes it worse. Your service and your attacker are frequently a few milliseconds apart — same cloud region, sometimes the same datacentre — rather than continents apart. Brumley and Boneh’s Remote Timing Attacks Are Practical (USENIX Security, 2003) demonstrated key recovery against an OpenSSL-based server across a local network, and the network distance between attacker and target has only shrunk since. Given enough samples, an attacker can average away jitter and resolve timing differences far smaller than the noise in any single request.

Second, modern services are covered in shared-secret comparisons: webhook signatures (Stripe, GitHub, Slack), API tokens, password reset tokens, session IDs, CSRF tokens, license keys, and the tag check in an HMAC-signed JWT. Each one is a place where == against attacker-controlled input is a slow, quiet leak.

The fix is mechanical and cheap. The cost of not applying it is that you leave a channel open on every secret you compare with the wrong operator — whether anyone can practically walk through it depends on how much signal they can gather, which is not a thing you get to decide for them.

The short answer

constant-time compare = always touch every byte + combine results without branching

Picture to keep: a bouncer who reads every name on the guest list all the way to the bottom before saying yes or no — even when the very first name already settled it — so nobody outside can time the door and work out how far down the list their guess got. (Where the analogy breaks: a bouncer only has to control his own behaviour. Your code also has to survive a compiler that would happily “help” by restoring the early exit, and a CPU whose caches make some reads faster than others.)

A constant-time equality function takes the same amount of time to run no matter where (or whether) the inputs differ. It walks both buffers to the end every time and folds the per-byte differences together with bitwise operations, so the CPU never takes a data-dependent branch. The function still returns “equal” or “not equal” — it just refuses to leak how it got there.

How it works

Build it by breaking it. Back to the webhook server, checking the attacker’s guessed tag against the real HMAC.

Naive attempt: if (a[i] != b[i]) return 0; inside the loop. Why it breaks: the number of iterations is now a readable measurement of how many leading bytes matched. That’s the leak.

Fix 1 — never return early. Run the loop to n no matter what. But now what? You still have to record, at each position, whether the bytes matched, and an if inside the loop puts the branch straight back.

Fix 2 — record it arithmetically instead of with a branch. a[i] ^ b[i] is zero exactly when the bytes are equal, and OR-ing those XORs into an accumulator keeps any non-zero bit forever. No comparison, no jump, same work every iteration:

int ct_equal(const uint8_t *a, const uint8_t *b, size_t n) {
    uint8_t diff = 0;
    for (size_t i = 0; i < n; i++) {
        diff |= a[i] ^ b[i];
    }
    return diff == 0;
}

But there’s still a branch — the final diff == 0. That one is fine, and the reason is worth holding onto: it branches on a value that depends on the whole comparison, not on any particular position. An attacker who times the function learns only the bit they were always going to be told anyway — equal or not equal. Constant-time doesn’t mean “no branches ever”; it means no branch, no memory access, and no instruction whose duration depends on a secret.

But the lengths can still betray you. Give this function two buffers of the same length. What the real libraries do differs, and the difference is instructive: Go’s crypto/subtle.ConstantTimeCompare returns immediately on a length mismatch, Node’s crypto.timingSafeEqual throws unless the lengths match, and Python’s hmac.compare_digest documents that it can still leak the types and lengths of its arguments. None of them is buggy; they are all assuming the lengths are public. The rule that generalises is narrower than “never leak length”: don’t leak the length of something whose length is secret. For a fixed-size HMAC tag it isn’t.

Why the compiler keeps trying to ruin this

The genuinely hard part of constant-time code is not writing the loop. It’s keeping the optimizer from helpfully un-writing it. A sufficiently smart compiler can notice that once diff is non-zero it can never become zero again, and “optimize” the loop into an early exit. It can also lower diff |= ... into branchy code on some architectures.

This is why production constant-time primitives live in the standard library or in audited libraries (libsodium, BoringSSL, Go’s crypto/subtle), often with compiler barriers, volatile reads, or hand-written assembly. Rolling your own in a high-level language is usually fine for the algorithmic shape but offers no real guarantee that the binary the compiler emits is still constant-time. Use the stdlib function.

Show the seams

The mental model: when one of the inputs is a secret, the comparison function is part of your cryptography, not part of your control flow. Treat it accordingly.

You started with constant-time compare = always touch every byte + combine results without branching. What did this post add? — + and nothing else downstream may vary with the secret either: not the length check, not the compiler’s rewrite of your loop, not a table lookup indexed by a secret byte. The loop is the easy half; keeping the rest of the pipeline uniform is why this lives in the standard library instead of in your file.

Check yourself

Before you go — you replace == with a proper constant-time compare in your webhook handler, but you keep the early if (len(given) != len(expected)) return False in front of it. What can an attacker still learn, and does it matter?

Answer

They learn the length of the expected tag, because wrong-length guesses are rejected measurably faster than right-length ones. For a fixed-size HMAC tag that’s near-worthless — the length was public anyway, implied by the algorithm. For a variable-length secret like an API key or a password reset token, it’s a real leak: it collapses the search space before the attacker starts guessing content. For the webhook case specifically the clean answer is that the tag width is fixed by the algorithm, so reject anything that isn’t exactly that length and you’ve leaked nothing new. Where the secret’s length is genuinely variable, the usual move is to compare something fixed-width instead — a hash of each side — so length stops being an observable at all.

And one more — a signature verifier is fully constant-time in its comparison, but decrypts using an AES implementation that indexes a 256-entry lookup table with key-derived bytes. Is the system constant-time?

Answer

No. The instruction stream may be identical every run, but the addresses it touches are not: which table entries land in cache depends on secret bytes, and cache hits and misses have very different latencies. An attacker who can influence or observe the cache can recover key material without the code ever branching on a secret. This is precisely the failure mode AES-NI was introduced to remove — do the rounds in hardware and there is no secret-indexed table to probe. “No branches” is necessary, not sufficient; the real rule is that no observable — time, cache state, power — may depend on the secret.

Going deeper