The Luhn algorithm is a checksum that catches typing mistakes in a number. You double every second digit from the right, subtract 9 from any result above 9, sum everything, and check whether the total is divisible by 10. If it is, the number is well-formed. If it is not, someone mistyped a digit.
That is the whole thing. It is not encryption, it does not prove a card exists, and it holds no secret — which is exactly why it works everywhere, and why understanding its limits matters as much as understanding the arithmetic.
A worked example, step by step
Take the number 4539 1488 0343 6467. It is a synthetic Visa-format number of the kind
our generator produces: correctly structured, issued by
nobody.
Verifying a complete number
When a number already includes its check digit, the last digit takes part in the sum like any other. Doubling starts one position to its left and alternates from there.
Number: 4 5 3 9 1 4 8 8 0 3 4 3 6 4 6 7
Position
(from right): 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
Double every second digit from the right (even positions):
8 5 6 9 2 4 16 8 0 3 8 3 12 4 12 7
Subtract 9 from any value above 9:
8 5 6 9 2 4 7 8 0 3 8 3 3 4 3 7
Sum: 8+5+6+9+2+4+7+8+0+3+8+3+3+4+3+7 = 80
80 mod 10 = 0 → the number is well-formed
Two details in that table are worth pausing on. First, doubling applies to the even positions counted from the right, which means the check digit itself is never doubled. Second, subtracting 9 from a doubled value above 9 gives the same answer as adding its two digits: 16 becomes 7 either way, and 12 becomes 3. Implementations use the subtraction because it is one operation instead of two, not because it is a different rule.
Calculating the check digit
Generating is the same arithmetic run backwards. You have the payload — a network prefix plus an account identifier — and you need the digit that will make the total come out even at ten. Because the check digit occupies position 1, doubling now starts at the rightmost payload digit rather than skipping it.
Using the first fifteen digits of the example, 453914880343646:
Payload: 4 5 3 9 1 4 8 8 0 3 4 3 6 4 6
Position
(from right): 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
Double every second digit, starting at the rightmost (odd positions):
8 5 6 9 2 4 16 8 0 3 8 3 12 4 12
Subtract 9 from any value above 9:
8 5 6 9 2 4 7 8 0 3 8 3 3 4 3
Sum: 8+5+6+9+2+4+7+8+0+3+8+3+3+4+3 = 73
Check digit = (10 − (73 mod 10)) mod 10 = (10 − 3) mod 10 = 7
Seven is exactly the digit the original number ends in, and the two sums are consistent with each other: 73 + 7 = 80, the total from the verification above.
Notice which positions got doubled. In the verification the check digit sat at position 1 and was skipped; here the payload is one digit shorter, so doubling starts immediately at the rightmost digit. The same physical digits end up doubled in both runs — but only because the routine flipped its starting parity to compensate for the missing digit. Feed a payload that already contains a check digit into the generation routine and you get a plausible-looking digit that is simply wrong. This is the most common bug in hand-written Luhn code, and it is invisible if you only ever test with one card length.
The outer mod 10 in step four exists for one case: when the sum is already a multiple of
ten, 10 − 0 gives 10, which is not a digit. Wrapping it back to 0 handles that cleanly.
Why doubling every second digit?
A checksum is only as good as the errors it detects, and the design of Luhn is a direct response to the two mistakes humans actually make when copying digits: getting one digit wrong, and swapping two neighbours.
Single-digit substitutions are always caught. Changing any digit changes its contribution to the sum. For an undoubled position the contribution moves by the same amount as the digit. For a doubled position it moves by twice that, or by twice minus 9 after the reduction. In neither case can the change be a multiple of 10 unless the digit did not change at all, so the total stops being divisible by ten and the check fails.
Adjacent transpositions are almost always caught. This is what the doubling buys. If
every digit contributed equally, 12 and 21 would produce identical sums and the swap
would slip through. Alternate doubling makes each position’s weight depend on where it
sits, so swapping two neighbours changes the total — except in one case. Swapping 0 and
9 produces the same sum in both orders, because doubling 9 and reducing gives 9, and
doubling 0 gives 0. 09 ↔ 90 is the documented blind spot, and it is the only one.
What it cannot catch. Two errors that cancel each other out — one digit up by 3 and another down by 3 in an undoubled position, say — leave the total unchanged. And, critically:
One in ten random 16-digit strings passes a Luhn check.
That figure follows directly from the design. The check digit has ten possible values and exactly one of them is correct, so a random final digit is right a tenth of the time. It is the mathematical reason Luhn cannot function as a fraud control, and it is worth keeping in mind whenever a site tells you a generated number is “valid”.
What Luhn validation does not tell you
| Luhn says | Luhn does not say |
|---|---|
| The digits are internally consistent | The card exists |
| No single digit was mistyped | The card is active |
| Most adjacent transpositions are absent | The card has funds |
| The number is well-formed | The number belongs to anyone |
The gap between those columns is where most of the confusion about test card numbers lives. A payment form that accepts a number has confirmed its shape. An issuing bank that approves an authorisation has confirmed an account, a balance, and its own risk rules. Nothing on this site can do the second thing, and the FAQ goes into why in more detail — but the arithmetic above is the reason. Every number our validator marks as passing is passing a format test, and the tool says so.
Where Luhn is used besides card numbers
Payment cards are the best-known application, not the only one. The algorithm shows up wherever a number is read aloud, typed from a form, or copied from a label:
- IMEI — the 15-digit identifier burned into every GSM handset
- Canadian Social Insurance Number (SIN)
- Israeli identity number (Teudat Zehut)
- South African identity number
- US National Provider Identifier (NPI), used for healthcare providers
- National Drug Codes and various product and shipment numbers
The pattern is consistent: none of these uses Luhn as a security measure. Each uses it to stop a call-centre operator or a data-entry clerk from silently corrupting a record.
It is worth noting what these numbers have in common beyond the checksum. All of them are identifiers that get transcribed by hand at some point in their life — read off a screen, dictated over a phone, copied from a sticker onto a form. Numbers that only ever move between machines do not need Luhn, because machines do not transpose digits. The algorithm is a piece of human-factors engineering that happens to be expressed in arithmetic.
Implementation
Two functions cover everything. luhnValid verifies a complete number; luhnCheckDigit
computes the missing final digit for a payload. Note the difference in how double is
initialised — that single line is the parity distinction the worked example above
illustrates.
/**
* Validate a number against the Luhn checksum.
* @param {string} input - digits, optionally with spaces or dashes
* @returns {boolean}
*/
function luhnValid(input) {
const digits = String(input).replace(/\D/g, '');
if (digits.length === 0) return false;
let sum = 0;
let double = false;
for (let i = digits.length - 1; i >= 0; i--) {
let d = digits.charCodeAt(i) - 48;
if (double) {
d *= 2;
if (d > 9) d -= 9;
}
sum += d;
double = !double;
}
return sum % 10 === 0;
}
/**
* Compute the check digit for a payload that does not include one.
* @param {string} payload - digits without the final check digit
* @returns {number} the digit that completes the number
*/
function luhnCheckDigit(payload) {
const digits = String(payload).replace(/\D/g, '');
let sum = 0;
let double = true;
for (let i = digits.length - 1; i >= 0; i--) {
let d = digits.charCodeAt(i) - 48;
if (double) {
d *= 2;
if (d > 9) d -= 9;
}
sum += d;
double = !double;
}
return (10 - (sum % 10)) % 10;
}
Both iterate from the end of the string and read digits with charCodeAt, which avoids
allocating a substring per digit and sidesteps the parseInt trap described below.
Ports of these two functions to Python, PHP, Java, C#, Go, and SQL are collected in
the code examples guide.
Seven common implementation mistakes
- Starting from the wrong end. Luhn works right to left. An implementation that doubles from the left produces correct answers on even-length numbers and wrong ones on odd-length numbers. Tested only against 16-digit Visa and Mastercard numbers, it looks perfect — then fails on every 15-digit American Express card in production.
- Extracting digits with
parseInt.parseInt(str, 10)parses the whole string from the given position onward, not one character. UseNumber(str[i]),charCodeAt, or a character-level parse. - Not stripping separators. Users type
4539 1488 0343 6467, and paste it with non-breaking spaces and dashes. Strip everything that is not a digit before you start. - Treating an empty string as valid. With no digits,
sumis 0 and0 % 10 === 0istrue, so an empty input passes. Check the length first — this bug survives code review remarkably often because the arithmetic is correct. - Skipping length validation entirely.
18passes the Luhn check. So do thousands of other short numbers. Luhn tells you nothing about whether the input is the right size for a card; pair it with the per-network length rules in the length reference. - Forgetting the UnionPay exception. Some UnionPay ranges do not satisfy the Luhn checksum at all. Code that hard-rejects a failed check will decline legitimate cards.
- Using floating-point arithmetic. Parsing a 19-digit number into a JavaScript
NumberexceedsNumber.MAX_SAFE_INTEGERand silently loses precision. Keep card numbers as strings from input to storage — always.
Mistakes 1 and 4 are the ones that reach production, because both pass a test suite built from 16-digit examples.
Where to run the check in your application
Run it client-side for immediate feedback: flagging a typo before submission saves a round trip and is the entire reason the algorithm exists. Then run it again on the server, because anything the browser validates can be bypassed.
But be careful how you act on a failure. Warn, do not block. A new BIN range, an unusual length, or a UnionPay card can break your assumptions, and losing a legitimate customer costs far more than one extra authorisation call. Show a message next to the field, keep the submit button live, and let the gateway make the final call.
Keep the wording human, too. “Please check your card number” is useful. “Luhn validation failed” tells the customer nothing they can act on and leaks your implementation into the user interface. If you need worked examples to test the message against, the test card number reference has numbers for every network, and the BIN generator covers prefix-level cases.
Where the algorithm came from
Hans Peter Luhn, a researcher at IBM, filed for a patent on a “computer for verifying numbers” in 1954. US Patent 2,950,048 — Computer for Verifying Numbers1 was granted in 1960 and has long since expired, which is why the method is free for anyone to implement — a large part of why it became ubiquitous.
The formula was later standardised as ANSI X4.13 and is specified for payment card identifiers in ISO/IEC 7812-1:2017 — Identification cards: Identification of issuers2, the standard that also defines the issuer identification number and the 12-to-19-digit length range covered in the card number structure guide. For the wider history and the formal proof of its error-detection properties, the Wikipedia entry on the Luhn algorithm is a good starting point.
Seventy years on, an algorithm designed for punched cards and telephone operators still runs in every checkout form on the internet — because the problem it solves, people mistyping digits, has not changed at all.