The Luhn checksum baked into every credit card number catches keystroke errors, not fraud — here is the math behind it, why card network prefixes matter, and where naive validators break.
A customer mistypes one digit filling out a checkout form. On a well-built form, the field turns red before they even hit submit — no server round-trip, no failed charge attempt, just an instant "check this number." That instant feedback isn't magic and it isn't fraud detection. It's a 70-year-old checksum called the Luhn algorithm, and it's baked into the last digit of every card number you've ever typed.
Most engineers know Luhn exists. Far fewer could explain what it actually verifies, which is the source of two common mistakes: treating a Luhn pass as proof a card is real and chargeable (it isn't), and reinventing a broken version of it with a regex that only checks digit count (which catches nothing).
The algorithm — also called the "modulus 10" or "mod 10" algorithm — was patented by IBM engineer Hans Peter Luhn in 1954, originally for validating identification numbers typed into machines. It's a checksum: the last digit of a card number isn't part of the account identifier at all, it's a check digit computed from every digit before it. If a human mistypes or transposes a digit anywhere in the number, the checksum almost certainly breaks, and the error gets caught immediately instead of surfacing three steps later as a failed authorization.
The algorithm, applied right to left starting from the check digit:
Take the widely-used test number 4111 1111 1111 1111 (Visa's standard test card, safe to publish since it's not a real account). Sixteen digits, numbered here from the right so it's clear which ones get doubled:
| Position (from right) | Digit | Doubled? | Value used |
|---|---|---|---|
| 16 (leftmost) | 4 | yes | 8 |
| 15 | 1 | no | 1 |
| 14 | 1 | yes | 2 |
| 13 | 1 | no | 1 |
| 12 | 1 | yes | 2 |
| 11 | 1 | no | 1 |
| 10 | 1 | yes | 2 |
| 9 | 1 | no | 1 |
| 8 | 1 | yes | 2 |
| 7 | 1 | no | 1 |
| 6 | 1 | yes | 2 |
| 5 | 1 | no | 1 |
| 4 | 1 | yes | 2 |
| 3 | 1 | no | 1 |
| 2 | 1 | yes | 2 |
| 1 (rightmost, check digit) | 1 | no | 1 |
Sum the "value used" column: 8+1+2+1+2+1+2+1+2+1+2+1+2+1+2+1 = 30. 30 is divisible by 10, so the number passes. Change any single digit — say the leading 4 to a 5 — and the sum shifts by a non-multiple of 10, so the check fails. That's the entire mechanism: no lookup table, no network call, just arithmetic anyone can run on paper in under a minute.
In code, the same logic is a dozen lines:
function luhnCheck(digits) {
let sum = 0
let shouldDouble = false
for (let i = digits.length - 1; i >= 0; i--) {
let d = parseInt(digits[i], 10)
if (shouldDouble) {
d *= 2
if (d > 9) d -= 9
}
sum += d
shouldDouble = !shouldDouble
}
return sum % 10 === 0
}
Walking right to left with a shouldDouble flag that flips each iteration is simpler than indexing from the left and doing parity math — it also naturally handles numbers of any length, which matters because valid card numbers aren't all the same length.
A Luhn-valid number tells you the digits are internally consistent. It doesn't tell you what kind of card it is, and issuers don't assign digits randomly — the leading digits (the Issuer Identification Number, or IIN, historically called a BIN) identify the network, and each network has a fixed set of valid lengths:
| Network | Leading digits | Length |
|---|---|---|
| Visa | 4 | 13, 16, or 19 |
| Mastercard | 51–55, or 2221–2720 | 16 |
| American Express | 34 or 37 | 15 |
| Discover | 6011, 644–649, 65, or 622126–622925 | 16 or 19 |
| Diners Club | 300–305, 36, or 38 | 14 |
| JCB | 3528–3589 | 16 |
| UnionPay | 62 | 16–19 |
A real validator checks all three signals together — Luhn checksum, length, and prefix — because each one alone is a weak filter. A 16-digit number starting with 4 that fails Luhn is a typo. A 16-digit number that passes Luhn but starts with a prefix no network has issued is a fabricated number, not a real card that happens to have a rare failure mode. Production systems (and the credit card validator on this site) run all three checks together and report the network alongside the pass/fail result, because "valid-looking Visa number" and "valid-looking number, network unknown" are different findings.
That Mastercard range is a good example of why hardcoding prefixes is riskier than it looks: Mastercard ran out of room in the 51–55 block as card issuance grew, so the network added 2221–2720 as a second valid range in 2016. Any validator written before that update — including plenty of regexes still copy-pasted into forms today — silently rejects a fully legitimate, newer Mastercard number. IIN ranges are allocated by the card networks and occasionally expanded; a validator that hasn't been touched in years is a validator that's quietly wrong about someone's real card.
This is the part that trips people up: passing the Luhn check means the number is arithmetically well-formed, not that it belongs to an open account, has available credit, or was issued to the person typing it. Luhn was designed to catch keystroke errors — transposed digits, a mistyped key, a digit dropped when copying a number by hand — not to detect fraud. A generated number that happens to satisfy the checksum and prefix rules will sail through client-side validation every time; it still gets declined at the payment processor, because that's a completely different check against the issuing bank's live account data.
This cuts both ways in how teams misuse it. Some treat a Luhn pass as reassurance the card is legitimate and skip stricter server-side validation — a mistake, since Luhn is a UX nicety, not a security control. Others distrust it entirely and skip it, sending every malformed number straight to the payment gateway, which wastes an API call and a decline on an error a client-side check would have caught in milliseconds. The correct scope is narrow and specific: use Luhn (plus prefix/length rules) to give instant feedback on typos before submission, and let the actual authorization network be the source of truth on whether the card is real and chargeable.
The other common implementation mistake is regex-only validation — matching ^4\d{15}$ and calling it done. That confirms length and a leading digit, nothing else; it will happily accept 4111111111111112, which fails Luhn and is not a valid number. Strip non-digit formatting characters (spaces, hyphens) first, then run the actual checksum — which is exactly what a Luhn-based credit card validator checks for you, along with network detection from the prefix table above.
Takeaway: Luhn is a 30-second arithmetic check for typos, not a fraud filter — build it to catch keystrokes before submission, and let the payment network handle whether the card is actually real.