Why we chose Argon2id and what it means for your API keys

Password hashing is one of those decisions that’s easy to get wrong and nearly impossible to fix after the fact. Here’s why Argon2id is the right choice and how we apply it to both passwords and API keys.

Shortifi.me Mar 19, 2026 4 min read

The stakes are higher than they appear

When you store a password or an API key, you’re making a promise to your users: even if your database is breached, their credentials can’t be trivially recovered. That promise lives or dies with your hashing algorithm.

Most engineers know to avoid MD5 and SHA-1 for password storage. Fewer realize that bcrypt, while better, has a known weakness at scale: its 72-byte input limit and relatively low memory cost make it increasingly tractable on modern GPU hardware. The OWASP Password Storage Cheat Sheet updated its primary recommendation to Argon2id in 2019. We built Shortifi with that recommendation from day one.

Abstract illustration of a key being transformed through layered hash rounds

What Argon2id is and why it matters

Argon2 won the Password Hashing Competition in 2015. It has three variants:

  • Argon2d — maximizes GPU resistance via data-dependent memory access, but vulnerable to side-channel attacks.
  • Argon2i — data-independent memory access, resistant to side-channels, but slightly weaker against GPU attacks.
  • Argon2id — hybrid: Argon2i for the first pass (side-channel resistance), Argon2d for subsequent passes (GPU resistance). This is the recommended default.

The key property that makes Argon2id superior to bcrypt is its configurable memory hardness. Where bcrypt is CPU-bound, Argon2id requires attackers to provision large amounts of RAM per hash attempt. A GPU farm that can test millions of bcrypt hashes per second is stymied when each attempt requires 64 MB of RAM — the parallelism collapses.

Our configuration

We use argon2-cffi with these parameters:

from argon2 import PasswordHasher

ph = PasswordHasher(
    time_cost=2,
    memory_cost=65536,  # 64 MB
    parallelism=2,
    hash_len=32,
    salt_len=16,
)

These are OWASP’s recommended minimum parameters for Argon2id as of 2024. We benchmark on deploy to ensure hash time stays under 500 ms on the target hardware — slow enough to deter offline attacks, fast enough to be imperceptible at login.

Passwords vs. API keys: different problems, same algorithm

Passwords and API keys are both secrets, but they have different characteristics:

Password API key
User-facing Yes Yes (once)
Machine-entered Rarely Always
Brute-force target High value High value
Needs prefix for identification? No Yes

How we handle API keys

API keys in Shortifi follow a prefix + secret structure:

sk_live_AbCdEfGhIjKlMnOpQrStUvWx

The sk_live_ prefix lets us identify the key type and allows secret scanning tools (like GitHub’s secret scanning) to detect accidental commits. The remaining characters are cryptographically random.

We never store the full key. After generation, we show it once and store only:

  1. The prefix (sk_live_) — plaintext, for lookup and display.
  2. An Argon2id hash of the full key — for verification.
  3. A hint of the last 4 characters — for user identification in the dashboard.

The lookup problem

Argon2id is intentionally slow. That’s fine for login (one hash per session). API keys are presented on every request, and hashing 64 MB per request at 500 ms each would make our API unusable.

We solve this with a two-tier lookup:

  1. Extract the key prefix and last 4 characters from the request header.
  2. Query PostgreSQL for the matching row using the prefix (a fast, indexed lookup).
  3. Verify the full key against the stored Argon2id hash.
  4. Cache the verified result in Redis for the session duration with a short TTL (5 minutes), keyed by a HMAC of the raw key.

Step 4 means that in steady state, active API clients hit the Redis cache on every request. The Argon2id verification only runs on the first request after a cold start or TTL expiry.

What this means for you as a user

If Shortifi’s database were ever exfiltrated, an attacker would have a list of Argon2id hashes. Cracking a single hash at 64 MB memory cost takes roughly 300 ms on a modern CPU. With a GPU cluster, each attempt requires 64 MB of VRAM. A 24 GB GPU can run ~375 parallel attempts at ~50 ms each — that’s 7,500 attempts per second.

For comparison: a GPU running bcrypt at cost factor 12 achieves ~20,000 attempts per second on the same hardware. Argon2id is already 2.5x harder; at higher memory parameters, the ratio widens further.

More importantly, each user has a unique random salt. An attacker can’t amortize a single hash attempt across multiple users — every crack is independent.

Staying current

We built parameter upgrade support into the PasswordHasher. argon2-cffi exposes a check_needs_rehash() method that returns True if a hash was created with parameters below the current configuration. On successful login, we transparently rehash and update the stored value. This means we can upgrade memory/time cost parameters over time without forcing password resets.

Run the work behind the click.

Start free. No credit card. One workspace included.

Start free