API authentication should be invisible—until it isn’t. For one SaaS team, a routine profiling exercise uncovered a hidden performance tax: every authenticated request to their API was spending nearly a full second in the authentication middleware before processing even began. The culprit? Using bcrypt to hash API keys, a decision made under the assumption that all credentials should be treated the same.
The hidden cost of slow hashing in API auth
When VesselAPI first launched, the engineering team hashed API keys using bcrypt at a cost factor of 10. The rationale was straightforward—bcrypt is the standard for password security, so it should work for API keys too. But what worked for user passwords became a bottleneck for API calls.
Bcrypt is intentionally slow. Its design goal is to make brute-force attacks computationally expensive, which is critical for passwords that users often reuse or choose poorly. On each API request, the system had to load every stored bcrypt hash from PostgreSQL and compare it against the incoming key until a match was found. With 11 active keys in production, this process consumed up to 1.1 seconds of CPU time per request—time that added no value beyond access control.
At the time, the impact wasn’t visible in production. The system processed fewer than 100,000 monthly requests, spread across hours and days. But the architectural ceiling was clear: bcrypt at cost factor 10 limited the system to under one authenticated request per second per CPU core. That’s not a tuning issue—it’s a scalability limit. Every new user or key added linear overhead, making future growth impossible without a redesign.
API keys aren’t passwords—and bcrypt isn’t the solution
The turning point came when the team questioned a fundamental assumption: should API keys really be treated like passwords?
API keys are not chosen by users. They’re not reused across services. They’re not found in rainbow tables. In VesselAPI’s case, each key was a 256-bit cryptographically random string—so random that brute-forcing one would require more attempts than there are atoms in the observable universe. At a billion guesses per second, recovering a single key would take longer than the expected lifespan of the solar system.
What API keys are vulnerable to is leakage: accidental commits to public repositories, exposure in logs, or theft from compromised clients. Bcrypt does nothing to mitigate these risks. Its deliberate slowness, which protects weak passwords, is wasted effort when the keys are already uncrackable.
The real need was speed and determinism. The team needed a way to verify keys quickly while still protecting them if the database was breached. The solution was twofold: replace bcrypt with a fast cryptographic hash and introduce a server-side secret to protect the stored values.
Introducing HMAC-SHA256 with a server-side pepper
Instead of bcrypt, VesselAPI switched to HMAC-SHA256, a fast, deterministic hash function designed for message authentication. Each API key is hashed using a 64-character server-side secret—called a pepper—stored in AWS Secrets Manager. The stored value in the database is the HMAC output, not the key itself.
This approach changes the security model. Even if an attacker steals the entire database, they gain nothing without the pepper. The pepper acts as a separate security boundary, preventing offline attacks. Meanwhile, HMAC-SHA256 computes in microseconds, not milliseconds—about 1,000 times faster than bcrypt at the same security level.
The code change was minimal but transformative. The old bcrypt-based flow loaded all stored hashes and compared them sequentially. The new flow computes the HMAC once and performs a single indexed lookup:
// Before: sequential bcrypt comparison for each stored hash
for _, stored := range allKeyHashes {
if err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(apiKey)); err == nil {
return stored, nil
}
}
// After: HMAC-SHA256 with indexed lookup
mac := hmac.New(sha256.New, []byte(pepper))
mac.Write([]byte(apiKey))
keyHMAC := hex.EncodeToString(mac.Sum(nil))
return db.VerifyApiKeyHMAC(keyHMAC) // indexed lookup in <1msThe immediate result: authentication overhead dropped from 946 milliseconds to about 811 milliseconds. Better, but still dominated by network latency to PostgreSQL. The real breakthrough came when the team eliminated database queries entirely on the hot path.
Eliminating database round trips with in-memory caching
With HMAC-SHA256 fast enough, the next bottleneck became clear: every request triggered multiple round trips to the database. The team set out to cache key metadata and user quota information in memory, reducing latency to microseconds.
The solution uses two sync.Map instances in the auth service:
keyCachemaps HMAC hashes to key metadata (user ID, permissions, etc.)userCachemaps user IDs to subscription status and quota limits
When both caches are warm, an authenticated request never touches the database. The flow becomes:
- Extract the Bearer token from the request header
- Compute HMAC-SHA256 of the key with the pepper (~1 microsecond)
- Look up the HMAC in
keyCache(tens to hundreds of nanoseconds) - Check user quota against
userCache(nanoseconds) - Update a local counter in memory (25 nanoseconds)
- Return the response
In steady state, this reduces authentication latency from over 900 milliseconds to under 1 millisecond—without any distributed caching infrastructure. The usage tracking is handled with atomic counters, ensuring thread safety and zero lock contention.
A scalable foundation for secure API growth
The migration from bcrypt to HMAC-SHA256 with caching wasn’t just a performance fix—it was a shift in how the team thinks about API security. API keys don’t need the same protections as passwords. They need speed, scalability, and a defense-in-depth model that accounts for real-world risks like leakage and insider threats.
For teams building APIs at scale, the lesson is clear: don’t assume that what works for passwords will work for keys. Choose your tools based on the actual threat model, not tradition. With the right cryptographic primitives and caching strategy, authentication can stop being a bottleneck and become an invisible layer of trust—just as it should be.
AI summary
API anahtarlarını doğrulamak için bcrypt kullanmanın performans kaybını öğrenin. HMAC-SHA256 ve cache kullanımıyla 1 saniyelik doğrulama süresini nasıl milisaniyelere indirebilirsiniz?