Authentication2026-04-19

TOTP vs HOTP: How Two-Factor Codes Actually Work

TOTP and HOTP are the two algorithms behind nearly every 2FA app. Here's how each one works, why TOTP won, and what to do when codes stop matching.

totphotp2famfaauthenticationotprfc-6238rfc-4226

TOTP vs HOTP: How Two-Factor Codes Actually Work

Open Google Authenticator, Authy, or 1Password and look at the six-digit code refreshing every thirty seconds. That code is TOTP. Its older sibling, HOTP, still powers hardware tokens and some banking devices. Both are defined by public RFCs and both are built on the same primitive — HMAC-SHA1 — yet they behave very differently in the real world.

Understanding the difference matters when you're implementing 2FA, debugging "invalid code" errors, or choosing between a mobile authenticator and a hardware token.

What HOTP Is

HOTP stands for HMAC-based One-Time Password, defined in RFC 4226 (2005). It was the first open standard for generating short, one-time numeric codes from a shared secret.

The algorithm is deliberately simple:

HOTP(K, C) = Truncate(HMAC-SHA1(K, C))

Where:

  • K is the shared secret (a random byte string, typically 20 bytes / 160 bits)
  • C is an 8-byte counter value
  • Truncate selects 31 bits of the HMAC output and converts them to a 6- to 8-digit decimal

The counter C starts at zero and increments by one every time a code is generated. Client and server must stay synchronized — every time you press the button on your hardware token, the counter ticks forward, and the server does the same when verifying.

What TOTP Is

TOTP stands for Time-based One-Time Password, defined in RFC 6238 (2011). It's a direct extension of HOTP that replaces the counter with a value derived from the current time:

T = floor((Current Unix Time − T0) / X)
TOTP(K) = HOTP(K, T)

Where:

  • T0 is the starting epoch (almost always 0, i.e., Unix epoch)
  • X is the time step in seconds (almost always 30)
  • T is the number of 30-second windows since the epoch

In other words, TOTP is HOTP with the counter replaced by "how many 30-second windows have elapsed since 1970." At any given moment, the authenticator app and the server independently compute T from their own clocks and derive the same code.

You can see this in action with the TOTP Generator — give it a Base32 secret and watch codes regenerate every 30 seconds. Under the hood, it's computing HMAC-SHA1 on the current window number.

Why TOTP Won

HOTP is still the underlying primitive, but TOTP dominates consumer 2FA for three practical reasons.

No Counter Drift

HOTP's counter has to stay synchronized. Every time a user presses the button on a hardware token without actually submitting the code, the client counter advances but the server's does not. After enough accidental presses, the codes no longer match and the device has to be re-synchronized.

TOTP eliminates this entirely. Both sides derive the counter from a clock, and clocks agree as long as they're roughly correct.

No Button, No Hardware

TOTP is the reason your phone can be a second factor. A mobile app with a secret in local storage can produce valid codes forever — no button press, no physical device, no counter state to manage.

Self-Healing on Missed Codes

If you generate a TOTP code, wait 45 seconds, then enter it, the server has already moved to the next window. TOTP servers almost universally accept codes from the previous window and the next window as well — trading off a small amount of security (a valid code is live for ~90 seconds) for massive usability gains.

HOTP cannot do this cleanly. The server can probe forward a few counters in case the user pressed the button extra times, but it can't probe backward — used counters are permanently burned.

The Shared Secret

Both algorithms start with a shared secret. In practice, this is almost always:

  1. Generated by the server as random bytes (at least 128 bits, preferably 160 bits)
  2. Encoded as Base32 (not Base64 — Base32 avoids characters that look alike)
  3. Transported to the authenticator app as a QR code containing an otpauth:// URI

The standard URI format (from Google's key-uri-format spec) looks like:

otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=SHA1&digits=6&period=30

Fields after secret are hints. Most authenticator apps ignore them and assume SHA-1, 6 digits, and a 30-second period — the defaults that RFC 6238 recommends.

Digits, Period, and Algorithm

The standard allows for customization, but defaults are near-universal:

Parameter Default Allowed Notes
Digits 6 6, 7, or 8 More digits = more entropy but worse UX
Period 30s Any Some banking apps use 60s; most apps ignore anything else
Algorithm SHA-1 SHA-1, SHA-256, SHA-512 RFC 6238 permits stronger hashes, but many apps only support SHA-1

A common mistake: issuing secrets with algorithm=SHA256 or algorithm=SHA512 and finding that some older authenticator apps silently fall back to SHA-1, producing codes that never match. If you're building a 2FA system, test against multiple authenticator apps before committing to a non-default algorithm.

Why SHA-1 Is Still Used (Even Though It's Broken)

SHA-1 is cryptographically broken for collision resistance. That matters for certificate signing, where an attacker might craft a colliding document. It does not meaningfully weaken HMAC-SHA1 as used by HOTP/TOTP, because:

  1. HMAC's security does not rely on the hash being collision-resistant — only on the compression function being a PRF.
  2. The attacker is never shown the HMAC output in full; TOTP truncates it to 31 bits, then to 6 decimal digits.

So while "HMAC-SHA256 TOTP" exists and is marginally better, SHA-1 TOTP remains secure in practice. The bottleneck is secret entropy and secret storage, not the hash function.

TOTP Versus SMS, Push, and Passkeys

TOTP isn't the only second factor. Here's how it compares:

Method Phishing-resistant? Offline? Shared secret? Notes
SMS No No N/A SIM-swap attacks, SS7 interception
TOTP No Yes Yes Works offline, secret can be phished
Push No (but harder) No N/A Relies on push notification delivery
Hardware HOTP (YubiKey OTP) No Yes Yes Physical device required
WebAuthn / Passkeys Yes Yes No (asymmetric) Cryptographically bound to origin

Passkeys are strictly better than TOTP for phishing resistance, but TOTP remains the best "works everywhere" fallback. Most mature systems offer both.

When TOTP Codes Stop Matching

Nearly every TOTP debugging session comes down to one of these:

  1. Clock skew. The user's phone is several minutes off actual time. Fix: enable "automatic time" on the device. Most servers accept ±1 window, so skew over 30 seconds starts to fail.
  2. Secret copied incorrectly. QR code scanned wrong; secret typed with lowercase l versus uppercase I. Fix: re-enroll from scratch.
  3. Wrong account in the authenticator app. User pastes code from their personal Google account instead of the work one. Fix: check the issuer label.
  4. Server and client disagree on period or digits. Server expects 6 digits; app produces 8. Fix: match the otpauth:// URI defaults.
  5. Base32 padding. Some libraries require = padding in Base32 secrets; others don't. Fix: test with both padded and unpadded forms.

When to Choose HOTP

HOTP isn't obsolete — it's the right choice for:

  • Hardware tokens without a clock. Battery-less cards, RSA-SecurID-style devices, and banking dongles that increment a counter on button press.
  • Systems with no reliable time source. Offline, air-gapped systems where TOTP can't work.
  • One-shot emergency codes. When you want "this specific counter value must be used once, ever," HOTP's strict monotonicity is useful.

For everything else — web logins, API 2FA, VPN MFA — TOTP is the default.

Quick Reference

  • HOTP (RFC 4226): counter-based, used by hardware tokens, requires synchronization.
  • TOTP (RFC 6238): time-based, used by mobile authenticator apps, self-heals on drift.
  • Both use HMAC-SHA1 with a shared secret. Both truncate to 6 digits by default.
  • TOTP secrets travel as Base32-encoded otpauth:// URIs, usually delivered via QR code.
  • Default period is 30 seconds; servers typically accept ±1 window.
  • Clock skew is the #1 cause of "invalid code" errors.
  • Test your TOTP implementation live using the TOTP Generator.