A support engineer once asked me why forty customers had received a renewal email twice. The answer took an hour to find and was completely mundane: the campaign list had been assembled by pasting one export under another, and forty addresses existed in both. Nobody had checked, because checking meant either eyeballing two thousand rows or writing a VLOOKUP that half the team didn't trust. So nobody checked, and the same forty people got told twice that their card was about to be charged.
That is the shape of this problem. Reconciling two lists is one of the most common things anyone does with data, and it's boring enough that people either skip it or do it badly. The instinct is usually to reach for a diff tool, paste both lists in, and squint at the coloured output - which fails immediately, because a diff answers a question you didn't ask. Or you go to a spreadsheet and start assembling MATCH/COUNTIF formulas, which works but takes ten minutes and produces an artifact you'll never reuse.
The operation you actually want has a name and it's older than any of the tools: set arithmetic. Intersection, difference, union. I build toolz.dev and put a browser-based list comparison tool there, but this guide is about the concepts underneath - why order should be ignored, what case folding quietly breaks, and how to pick between this and a diff.
TL;DR: To compare two lists, treat each as an unordered set and compute the intersection (items in both), the two differences (items only in A, items only in B), and the duplicates within each list. Ignore order entirely - a line diff is the wrong tool because it's positional, so reordering a list makes almost every row look changed. Fold case for identifiers like emails but preserve the original text in the output, trim whitespace before comparing, and do it in the browser since the lists people reconcile are usually customer data.
What questions does comparing two lists actually answer?
Once you see the operations named, the shapes become obvious. Given list A and list B:
- Intersection- what's in both? Which subscribers are also paying customers. Which of last month's SKUs are still in this month's catalogue.
- A minus B- what's only in A? Which users in the CRM never made it into billing. Which files exist locally but not on the server.
- B minus A- what's only in B? The same question in the other direction, and it's a different question. Missing-from-billing and missing-from-CRM are two distinct bugs with two distinct causes.
- Symmetric difference- what's in exactly one list? The union of both differences: everything that failed to match, regardless of direction. This is the "what's out of sync?" question.
- Union- everything from either list, de-duplicated. The merge, done correctly.
- Duplicates within a list- what's repeated inside A alone? This one isn't a comparison at all, but it's always the question you turn out to have needed, because it's what causes double-sends and double-billing.
That last one is worth separating out. Cross-list matching and within-list duplication are independent: an address can appear twice in A and also appear in B. Tools that only report cross-list results miss the failure that costs money.
Everything here maps directly onto operations you already know from SQL - INTERSECT, EXCEPT, UNION- and onto spreadsheet formulas. The value of a dedicated tool isn't that it does something you can't; it's that all six answers show up from one paste, instead of six different formulas.
Why is a diff tool the wrong choice for comparing lists?
This is the mistake I see most, and it's worth being precise about, because "compare two lists" and "diff two files" sound like synonyms.
A diff is positional. Diff algorithms compute the minimum edit script - the shortest sequence of insertions and deletions that turns one sequence into the other. That's the right model for source code and prose, where line 40 following line 39 is meaningful. Move a function and a diff correctly reports that you moved a function.
A list has no meaningful order. Row 300 in your CRM export has no relationship whatsoever to row 300 in your billing export. They're two bags of items that happen to be written down in whatever sequence the database returned.
Feed unordered data to a positional algorithm and you get noise. Take two lists with identical contents, sort one of them, and diff them:
List A List B
alice bob
bob alice
carol carol
A diff reports that alice was removed and re-added, or that bob moved - some churn proportional to how differently the two are sorted. The correct answer is nothing changed. Every item is in both lists. The sets are equal. A diff can't say that because it isn't asking about membership.
The comparison table, since the tools genuinely overlap in the minds of people looking for both:
| List Compare | Text Diff | |
|---|---|---|
| Model | Unordered set of items | Ordered sequence of lines |
| Order matters? | No - reorder freely, results identical | Yes - reordering shows as changes |
| Answers | Membership: in both, only A, only B, duplicated | Edits: what to insert/delete to transform A into B |
| Duplicate items | Reported explicitly as a group | Just more lines |
| Good for | Reconciling exports, email lists, IDs, SKUs, inventories | Source code, prose, config files, anything where position is meaning |
| Bad for | Comparing two versions of a document | Any list where sort order is arbitrary |
The rule: if you'd be equally happy with the list sorted differently, you want a set comparison. If reordering the lines would be a real change worth reporting, you want the Text Diff Checker. For structured data with nesting rather than flat lines, neither applies - that's what the JSON Diff is for, since it compares by key path rather than by line or by membership.
How should case sensitivity work?
This is the option people leave on default and then get quietly wrong, so it's worth thinking through once.
Case-insensitive matching is the right default for the data most people compare. Email addresses, usernames, domain names, product codes, country codes - these are conventionally case-insensitive in practice, and [email protected] and [email protected] are the same person in every system that matters.
There's a pedantic caveat here that's worth knowing because it's occasionally load-bearing: per RFC 5321, the domain part of an email address is case-insensitive, but the local part - everything before the @- is formally case-sensitive and left to the receiving mail server to interpret. So [email protected] and [email protected] could in principle be different mailboxes. In practice essentially every major provider treats them as identical, and if you're de-duplicating a mailing list you should absolutely fold case. But if you're debugging why one specific address bounces, that's the sort of detail that turns out to matter.
Case-sensitive matching is correct for anything where case carries information: Linux file paths, base64 strings, hashes, JWT tokens, API keys, Git SHAs, most programming identifiers. Folding case on a list of password hashes would merge distinct values and give you a confidently wrong answer.
The implementation detail that matters more than the option itself: fold case for matching, but show the original text. If you paste [email protected] and the tool tells you it's in both lists, it should hand back [email protected]- not [email protected]. Lowercasing the output silently corrupts your data on the way through, and since the usual next step is pasting the result somewhere else, that corruption travels. The tool keeps the first-seen form of each item and matches on a folded key behind the scenes, so what comes out is what you put in.
Whitespace deserves the same treatment and gets less thought. Copy a column out of a spreadsheet, or split a line like a, b, c on commas, and you get items carrying leading spaces. [email protected] and [email protected] are different strings and identical addresses. Trimming is on by default for that reason, and it's the option you'd notice missing within about thirty seconds of real use.
What separator should I use?
The default is one item per line, which is what you get pasting a spreadsheet column - the clipboard hands over newline-separated values, so a column of emails from Excel, Google Sheets, or a CSV export drops in with no reformatting.
The other separators cover data that arrives already inline. Comma for a single CSV row or a copied array. Semicolon for the Outlook and older-Windows convention for address lists. Space for shell output - ls, git diff --name-only piped through tr, anything space-delimited. Tab for a row pasted from a spreadsheet horizontally rather than vertically.
One thing to note: splitting on commas is not CSV parsing. A real CSV field can contain a comma inside quotes, and a naive split will tear "Smith, Jane" into two items. If you're pulling one column out of a genuine CSV file with quoted fields, run it through the CSV Viewer first - it implements the actual RFC 4180 quoting rules - then copy the column you want. For a flat list of emails or IDs with no embedded commas, splitting is fine and this doesn't come up.
Empty entries get dropped by default, because they're almost always artifacts: a trailing newline at the end of a paste, a blank row in a spreadsheet, a double comma. An empty string isn't an item in any list you actually care about. The option exists if you're specifically hunting for blank rows in an export, which is a real if uncommon thing to want.
How does the comparison scale?
The naive approach to comparing two lists is a nested loop: for every item in A, scan all of B. That's O(n×m), and it's fine for a hundred items and unusable for fifty thousand, where you're doing 2.5 billion string comparisons.
The right approach indexes each list into a hash map keyed by the comparison key - the folded, trimmed form of the item - with the value being the first-seen original. Building each index is one linear pass. Then every question becomes a constant-time lookup per item: is this key in B's map? The whole comparison is O(n+m), which means twenty thousand items on each side is forty thousand hash operations and completes faster than the browser can repaint.
The same index gives duplicates for free. Count occurrences per key while building it; any key with a count above one is duplicated within that list. No second pass, no extra structure.
In practice the ceiling isn't the comparison - it's the browser rendering a result group with fifty thousand rows into a textarea. The arithmetic finishes in milliseconds regardless. If you're routinely reconciling lists that large, you probably want this in a script rather than a tab, and the algorithm above is about ten lines in any language.
Sorting is worth a note. Results are sorted naturally by default, meaning numeric-aware: item2 before item10, not after it. Plain lexicographic sorting puts item10 first because 1 < 2 character by character, which is correct by the letter of string comparison and wrong by every human expectation when scanning IDs or versioned names. Turn sorting off and you get insertion order - items in the sequence they first appeared in A, then B - which is occasionally what you want when the original order encodes something like recency.
What does this look like in practice?
Four scenarios where I've actually used this, each mapping to a different result group.
Cleaning a mailing list before a send. Paste the new list and the previously-sent list. Only in A is who hasn't been contacted - that's your send list. In both is who'd get a duplicate. Duplicates in A is the forty people from the story at the top of this page. That check takes fifteen seconds and it's the one that would have saved the support engineer an hour.
Reconciling two systems. Export user emails from the CRM into A and from billing into B. Only in A is signed-up-but-never-billed; only in B is billed-but-missing-from-CRM. These are two different bugs. The first might be a broken webhook, the second might be a manual invoice someone raised outside the flow. A single "these lists differ" answer would obscure that entirely, which is exactly why both directions are reported separately.
Inventory and catalogue drift. Last month's SKU export against this month's. Only in A is discontinued, only in B is new, in both is carried over. Sorting matters here - the exports come out of different systems in different orders, and a diff would report the entire file as changed.
Deployment sanity checks. Files on staging versus files on production, from two ls outputs pasted with the space separator. Only in A is what hasn't shipped yet.
The pattern across all four: the useful answer is almost never "the lists are different." It's which items, in which direction - which is precisely what set operations give you and what a similarity score or a diff summary doesn't.
Are my lists uploaded anywhere?
No, and think for a second about what you'd paste into a tool like this.
It's a subscriber export. A list of customer emails. Employee IDs. License keys. Account numbers. The lists people reconcile are, by their nature, close to the most sensitive data an organisation holds - you don't reconcile lists of nothing, you reconcile lists of people. And "let me just paste these two thousand customer emails into a random website to check for overlap" is a sentence that should stop you cold, because in a lot of jurisdictions that's a processor relationship you just created without a contract.
There's no reason for this computation to touch a network. It's hash maps over strings - a few hundred lines of dependency-free TypeScript. The tool on toolz.dev runs entirely in your tab; the lists are JavaScript strings in your browser's memory and they never leave it. Nothing is uploaded, logged, or stored. Verify it the way you'd verify any such claim: open the network tab and hit Compare, or turn off your wifi and watch it keep working. I've written more on why this architecture matters for exactly this class of data in why browser-based tools beat server-side ones.
FAQ
How do I compare two lists to find what they have in common?
Paste one list into List A, the other into List B, and press Compare. The "In Both" group is the intersection - every item present in both lists. You can copy that group on its own, download it as a text file, or export every group at once with Copy Report. Order doesn't matter, so the lists don't need to be sorted the same way.
How do I find items that are in one list but not the other?
The "Only in A" and "Only in B" groups answer that, and they're deliberately separate. Only in A holds items missing from List B; Only in B holds items missing from List A. These are usually different problems with different causes - missing-from-billing and missing-from-CRM aren't the same bug - so collapsing them into one answer loses the information you need. The "Unique" group combines both if you want the symmetric difference.
Can it find duplicates inside a single list?
Yes. Duplicates in A and Duplicates in B list every distinct item appearing more than once within that list. This is independent of cross-list matching, so an item can be both duplicated in A and present in B. It's usually the check that matters most in practice, since within-list duplicates are what cause duplicate emails and double-billing.
Does capitalisation affect the comparison?
Only if you want it to. Case-sensitive matching is off by default, so [email protected] and [email protected] are treated as one item - and the output preserves whichever form you pasted rather than lowercasing your data. Turn it on for values where case carries meaning: Linux paths, base64 strings, hashes, API keys, Git SHAs.
What's the difference between this and a text diff tool?
A diff is positional: it compares line 1 to line 1 and computes the edits needed to turn one sequence into the other, so reordering a list makes nearly every line look changed. This tool ignores order entirely and only asks whether an item exists on each side. Use a diff for code and prose where position is meaning; use list compare for reconciling exports where the sort order is arbitrary.
Can I compare lists separated by commas instead of new lines?
Yes - switch the separator to comma, semicolon, space, or tab. Whitespace around each item is trimmed by default, so a, b, c splits into three clean items. One caveat: splitting on commas isn't real CSV parsing, so if your data has quoted fields containing commas, extract the column with a proper CSV tool first.
How many items can it handle?
The comparison indexes each list into a hash map and runs in linear time rather than using nested loops, so tens of thousands of items on each side complete in milliseconds. The practical ceiling is your browser rendering a very large result group into the page, not the comparison itself.
Are my lists uploaded anywhere?
No. All parsing and comparison happens as JavaScript in your browser - nothing is transmitted, logged, or stored. This matters here more than for most tools, because the lists people reconcile are usually customer emails, employee IDs, or license keys. Watch your network tab while comparing, or go offline and it keeps working.
Related tools: Text Diff Checker when order and position matter, JSON Diff for structured data, CSV Viewer for extracting a column from a real CSV, and Word Counter for quick counts. Further reading: why browser-based tools beat server-side ones and the web developer's toolkit.



