DEV Community

Cover image for Stop Joining Millions of Rows for Every Single Swipe
Doogal Simpson
Doogal Simpson

Posted on • Originally published at doogal.dev

Stop Joining Millions of Rows for Every Single Swipe

TL;DR: Dating apps avoid the architectural nightmare of joining millions of left-swipe records by using Bloom filters. By hashing user IDs into a bit array, they get a 100% guarantee that a '0' means a user is new, while accepting a rare false positive as a necessary trade-off for high-concurrency performance.

I’ve seen plenty of teams try to scale discovery feeds by throwing more hardware at SQL joins, and it is a losing game. If I have to check a user’s entire history of left swipes against a pool of millions of profiles every time they refresh their deck, the database isn't just going to be slow—it’s going to stop breathing. We aren't talking about a simple query; we are talking about a cross-reference that grows every time a user interacts with the app.

Scaling a discovery engine requires moving away from absolute row-level checks and toward probabilistic data structures. When I look at how top-tier apps handle the "have they seen this person?" problem, I don’t see massive JOIN statements. I see Bloom filters.

Why can't we just use a standard database join?

I can't use a standard join because the latency of checking billions of swipe records for every profile in a stack is unsustainable at scale. Even with perfect indexing, the I/O overhead of a massive NOT IN or LEFT JOIN on that volume will kill the request-response cycle and frustrate the user.

Think about the math. If I have 50 million users and each user has swiped on a few thousand people, my swipes table is a disaster waiting to happen. If I try to run a query to "show me 10 people this user hasn't swiped on," the engine has to scan or index-hop through a mountain of data. By the time the database returns a result, the user has already closed the app. I need a way to filter those candidates without actually touching the disk for every individual check.

How does a Bloom filter handle membership tests?

I use a Bloom filter to treat membership as a bitmask operation rather than a record lookup. It consists of a fixed-size array of bits, all starting at zero, which I flip to one based on the output of multiple hash functions.

When a user swipes left on someone, I don't just log the event; I run that profile’s ID through a set of hash functions. If I'm using two functions, they might return the integers 1 and 3. I go to my bit array, find those indices, and set them to one. That profile is now "recorded."

Later, when I'm deciding whether to show that same profile again, I run the ID through those same two hashes. If I get back 1 and 3, and both are already set to "1," I assume the user has seen them. But if I check a different profile and the hashes return 3 and 5, and index 5 is still a "0," I know for a fact—with 100% mathematical certainty—that this user has never seen this profile. I show it to them immediately.

The Engineering Trade-offs of Probabilistic Filtering

Factor Bloom Filter Approach
Storage Bit-level storage; constant footprint.
Speed Constant time (O(k)) lookups.
Accuracy 100% Negative accuracy; Probabilistic Positive.
Cost Negligible CPU/Memory overhead.

What happens when the filter gives a false positive?

I accept the false positive as a necessary trade-off: the app simply skips showing a specific profile because it incorrectly thinks the user has already swiped on it. In a discovery engine with millions of candidates, missing one potential match is an invisible error that saves the entire system from a performance collapse.

If I used a standard Hash Set to keep track of every swipe, the memory usage would balloon until I was spending a fortune on RAM just to keep the feed functional. With a Bloom filter, I keep the memory footprint predictable. If the filter tells me all the bits are "1," but it’s actually a collision and the user hasn't seen that person, I just move to the next candidate. The user doesn't care about the one person they didn't see; they care that the app didn't hang for five seconds while loading the next card.

FAQ

Can I remove a swipe from a Bloom filter?
No. In a standard Bloom filter, I can't flip a bit back to zero because I don't know which other IDs also hashed to that same bit. If I need to support "undoing" a swipe, I’d have to use a more complex structure like a Counting Bloom Filter or just rebuild the filter from the source of truth occasionally.

How do I decide the size of the bit array?
It’s a balance between memory and the error rate. If I make the array too small, it fills up with ones too quickly and starts giving me false positives for everyone. I calculate the size based on how many swipes I expect a user to perform over the lifetime of their account to keep the collision rate under a certain threshold.

Does this replace my primary database?
Absolutely not. I still store the actual swipe data in a persistent database like Postgres or Cassandra for long-term records and analytics. The Bloom filter is a performance layer I use to make real-time decisions in the discovery feed without hitting the heavy data store every single time.

Top comments (0)