Timing attacks are a class of side-channel attacks where an attacker learns sensitive information by measuring how long an operation takes. The code might be logically correct and secure on paper, but tiny differences in execution time can leak secrets one bit at a time.

This sounds abstract until you realize how often we compare secrets in backend code: API keys, passwords, HMAC signatures, webhook secrets, session tokens. If an attacker can repeatedly hit your endpoint and precisely measure response times, even microsecond-level differences can be enough to recover a secret.

The core idea is simple. If your code exits early when it finds a mismatch, comparisons that share a longer prefix with the real secret will take slightly longer. Over many requests, an attacker can statistically infer the correct value.

A classic example is naive string comparison.

Naive Comparison and Why It’s Dangerous

Suppose you have an HTTP endpoint that validates an API key.

func isValidAPIKey(input string) bool {
return input == "super-secret-api-key"
}

At first glance this looks harmless. But string comparison in Go is not constant time. Internally, it compares byte by byte and stops as soon as it finds a mismatch.

That means these comparisons take different amounts of time:

“Xxxxxxxxxxxxxxxxx” vs “super-secret-api-key”
“sXxxxxxxxxxxxxxxx” vs “super-secret-api-key”
“suXxxxxxxxxxxxxxx” vs “super-secret-api-key”

Each extra matching character pushes the mismatch further, slightly increasing execution time.

Now imagine this function behind an HTTP handler:

func handler(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
if !isValidAPIKey(key) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
w.Write([]byte("ok"))
}

An attacker can send thousands of requests, vary one character at a time, and measure response durations. Over time, they can reconstruct the API key without ever seeing it directly.

This is not theoretical. Timing attacks have been used in the real world against HMAC validation, OAuth tokens, and webhook signature checks.

Where Timing Attacks Commonly Appear

You’ll most often see timing attack risks in:

Password comparisons
API key validation
HMAC or signature verification (Stripe, GitHub, Slack webhooks)
Session or CSRF token checks
Any logic that compares attacker-controlled input to a secret

The mistake is almost always the same: using a normal equality comparison where a constant-time comparison is required.

Constant-Time Comparison

A constant-time comparison ensures that the execution time depends only on the length of the inputs, not on their contents. Even if the first byte is wrong, the function still checks all remaining bytes.

Go’s standard library gives you exactly what you need in the crypto/subtle package.

import "crypto/subtle"

For byte slices, you should use subtle.ConstantTimeCompare.

func isValidAPIKey(input string) bool {
expected := []byte("super-secret-api-key")
given := []byte(input)

if len(expected) != len(given) {
return false
}
return subtle.ConstantTimeCompare(expected, given) == 1
}

Now the comparison always scans every byte. An attacker no longer gets useful timing differences from partial matches.

Note the length check. ConstantTimeCompare returns 0 if lengths differ, but you should still explicitly handle it to avoid confusion and make the intent clear.

HMAC Verification: An Example

Webhook verification is one of the most common places people accidentally introduce timing attacks.

Imagine a service that receives webhook payloads signed with HMAC-SHA256.

Naive implementation:

func verifySignature(body []byte, signature string, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return signature == expected
}

This looks correct cryptographically, but the final comparison is vulnerable.

The correct approach:

func verifySignature(body []byte, signature string, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := mac.Sum(nil)

given, err := hex.DecodeString(signature)
if err != nil {
return false
}

if len(expected) != len(given) {
return false
}

return subtle.ConstantTimeCompare(expected, given) == 1
}

Here, even if the attacker guesses most of the signature correctly, they gain no timing advantage.

Timing Attacks vs Network Noise

A common argument is: “This is over HTTP, network latency will drown out timing differences anyway.”

That used to be true. It’s no longer a safe assumption.

Attackers average results over thousands or millions of requests. They control their own network conditions. In cloud environments and data centers, latency jitter is often small enough that statistical analysis still works.

If a fix costs you nothing and removes an entire class of vulnerabilities, there’s no reason not to apply it.

General Rules

  • If you are comparing anything secret, do not use ==.
  • If the value is attacker-controlled and the other side is secret, assume timing attacks are possible.
  • Use crypto/subtle for comparisons, not homegrown tricks.
  • Prefer comparing byte slices, not strings, for cryptographic material.

This applies even if the code “only runs internally”. Internal systems get exposed over time, especially in microservice architectures.