DEV Community

gofortool
gofortool

Posted on

The Luhn algorithm in 20 lines: how credit card validators work

Type a random 16-digit number into any payment form and it gets rejected instantly - before the page talks to any bank, before any network request at all. How does the form know?

It's not magic and it's not a database lookup. It's a checksum from 1954 called the Luhn algorithm, and you can implement it in about 20 lines. Let's build a credit card validator from scratch and then look at what it can - and more importantly can't - tell you.

The idea: one digit that checks all the others

The last digit of every card number is not part of your account number. It's a check digit, computed from all the digits before it. When you type your card number, the validator recomputes the checksum and compares. One typo, and the math stops adding up.

Hans Peter Luhn, an IBM researcher, designed it to catch the two most common human errors:

  • Single-digit typos - typing 4 instead of 7 (caught 100% of the time)
  • Adjacent transpositions - typing 54 instead of 45 (caught almost always; the single exception is swapping 0990)

That's it. It was never meant to be secure - it was meant to catch fat fingers. Keep that in mind for later.

How it works, by hand

Take a number, say 4539 1488 0343 6467. Working from the right:

  1. Double every second digit (positions 2, 4, 6… from the right).
  2. If doubling gives a two-digit number (e.g. 8 × 2 = 16), add its digits together (1 + 6 = 7). Shortcut: just subtract 9.
  3. Sum everything - doubled and untouched digits alike.
  4. If the total is divisible by 10, the number is Luhn-valid.

Why subtract 9? Because for any doubled digit d ≥ 10, its digit sum is d - 9. 16 → 7, 18 → 9. Same result, no string juggling.

The 20 lines

function luhnCheck(cardNumber) {
  const digits = cardNumber.replace(/[\s-]/g, ""); // allow spaces & dashes

  if (!/^\d{12,19}$/.test(digits)) return false;   // card numbers are 12–19 digits

  let sum = 0;
  let shouldDouble = false;                        // last digit is never doubled

  for (let i = digits.length - 1; i >= 0; i--) {
    let d = digits.charCodeAt(i) - 48;             // faster than parseInt

    if (shouldDouble) {
      d *= 2;
      if (d > 9) d -= 9;                           // the digit-sum shortcut
    }

    sum += d;
    shouldDouble = !shouldDouble;
  }

  return sum % 10 === 0;
}
Enter fullscreen mode Exit fullscreen mode

Test it:

luhnCheck("4539 1488 0343 6467"); // true  (valid test number)
luhnCheck("4539 1488 0343 6468"); // false (one digit off)
luhnCheck("4111 1111 1111 1111"); // true  (the classic Visa test number)
Enter fullscreen mode Exit fullscreen mode

Time complexity is O(n) with a single pass and zero allocations beyond the cleaned string. This runs comfortably on every keystroke.

Bonus: identifying the card network

The first digits (the IIN/BIN - Issuer Identification Number) tell you the network, which is how forms show the Visa/Mastercard logo as you type:

function cardNetwork(digits) {
  if (/^4/.test(digits))                    return "Visa";
  if (/^5[1-5]/.test(digits) ||
      /^2(2[2-9]|[3-6]|7[01]|720)/.test(digits)) return "Mastercard";
  if (/^3[47]/.test(digits))                return "Amex";       // 15 digits!
  if (/^6(011|5)/.test(digits))             return "Discover";
  if (/^3(0[0-5]|[68])/.test(digits))       return "Diners Club";
  if (/^35(2[89]|[3-8])/.test(digits))      return "JCB";
  return "Unknown";
}
Enter fullscreen mode Exit fullscreen mode

Two details people get wrong: Amex is 15 digits, not 16 (and groups as 4-6-5, not 4-4-4-4), and Mastercard added the 2221–2720 range in 2017 - a lot of old regex on Stack Overflow still rejects those cards.

What Luhn does NOT tell you (read this before shipping)

This is the part that matters if you're building anything real:

  1. Luhn-valid ≠ real card. The algorithm validates format, not existence. 4111 1111 1111 1111 passes Luhn and belongs to nobody. Passing Luhn means "no typo detected," nothing more.
  2. It provides zero security. Anyone can generate millions of Luhn-valid numbers in a loop. It's a typo detector, not fraud prevention. Actual verification happens at authorization time, through your payment processor.
  3. Never log or store what users type into a card field - even "invalid" attempts. Invalid attempts are often almost-right numbers, one keystroke from a real card. Under PCI-DSS, if raw card numbers touch your servers, you inherit a compliance burden you almost certainly don't want. Client-side validation + a tokenizing payment provider (Stripe, Adyen, etc.) keeps card numbers off your infrastructure entirely.
  4. Not everything numeric uses Luhn - but a surprising amount does: IMEI numbers on phones, Canadian Social Insurance Numbers, and some national ID schemes all use the same checksum.

Try it live

I built a free, browser-only validator that runs this exact algorithm and shows the check-digit math step by step - nothing you type ever leaves your browser: Credit Card Validator (Luhn Check Tool). Useful for testing your own implementation against known-good test numbers.

The takeaway

The Luhn algorithm is a great first "real-world algorithm" to learn: it's short, it's everywhere, and it teaches a pattern - checksums as cheap error detection - that shows up all over computing, from ISBNs to network packets.

What's the oldest algorithm still running in your production code? I'd bet money something in your stack predates the moon landing. 👇

Top comments (0)