A few days ago, I opened the Forem mobile app and navigated over to the Community Hub (/community) π±.
Naturally, you expect the "Popular Community Members" section to feature the people driving great technical discussionsβauthors writing insightful deep-dives, answering tough engineering questions, and earning genuine community appreciation π¬β¨.
Instead, sitting proudly at the top of the leaderboard were accounts like:
- π€ Account A: 779 posts this month. Reactions: ~0.
- π€ Account B: 415 posts this month. Reactions: ~0.
Unless there is an engineer on Earth with 16 arms who writes a blog post every 40 minutes while sleeping π΄, we had a classic platform bug: Automated RSS syndication scrapers were being crowned the most "popular" developers in the community.

The RSS bot publishing 779 posts a month while the rest of us spend 3 days refining a single paragraph.
Here is the story of how that happened, why popularity metrics break when you measure volume instead of engagement, and how fixing it led to a wild deep-dive into PostgreSQL query planner costs with Ben Halpern ππ.
π΅οΈββοΈ The Crime Scene: What Did "Popular" Actually Mean?
If you don't explicitly define what "popular" means in code, PostgreSQL will define it for youβand usually in the most chaotic way possible π .
In CommunityController#index, the @top_authors query looked something like this:
# The old query (simplified)
@top_authors = User.where(id: recent_articles.select(:user_id))
.order("total_score DESC, recent_articles_count DESC")
.limit(8)
Look closely at what was missing:
- π« No filter on
published_from_feed: Forem has an official feature that lets creators syndicate their personal blogs via RSS feeds. But the query never checkedpublished_from_feed: false. If a feed bot syndicated 700 markdown drafts, every single one counted toward their author standing. - π Post Count as a Tie-Breaker: By ordering with
recent_articles_count DESC, sheer output volume trumped community feedback. - π Reactions Were Ignored: A creator who spent 20 hours crafting an engineering masterclass with 100 reactions was mathematically outranked by a script dumping weather logs.

(πΈ Before: Automated feed bots dominating the Popular Community Members section with hundreds of 0-reaction posts)
π οΈ Round 1: The "Simple" Fix
The first solution felt completely intuitive:
- π§Ή Exclude feed-imported posts:
.where(published_from_feed: false) - β€οΈ Rank primarily by real human engagement:
SUM(articles.public_reactions_count) DESC - π‘οΈ Exclude suspended/spam users:
.without_role(:suspended).without_role(:spam)
I opened PR #24324, added request specs, and pushed π.

Me pushing the PR thinking I solved open-source with 3 lines of ActiveRecord.
Then, @ben Sir's automated AI review agent (open hands) ran an EXPLAIN query on the production schema...
And the query planner cost completely exploded. π₯π€―
β οΈ The Database Trap: Rolify's Hidden Sequential Scans
Hereβs the baseline cost of the query on origin/main:
- π Baseline Planner Cost:
216,129.56(Bitmap index scan on articles + nested loop index scan onusers_pkey).
With my chained .without_role(:suspended).without_role(:spam), look at what happened:
- π¨ Planner cost jumped 4.4x to
953,586.49!
Why? π€
Forem uses the rolify gem for role management. Under the hood, without_role(:role_name) generates a NOT IN subquery:
WHERE users.id NOT IN (
SELECT users_roles.user_id FROM users_roles ...
)
When you chain .without_role(:suspended).without_role(:spam), ActiveRecord nests the first NOT IN subquery inside the second NOT IN subquery.
PostgreSQL looked at that nested monstrosity across nearly 1 million users and essentially threw its hands in the air:
- π It abandoned the fast index scan on
users_pkey. - π It triggered a Parallel Seq Scan on
usersacross 6 workers (~348,000 rows per worker). - π’ It ran an outer full-table Seq Scan across 988,000 rows.
- π£ To make matters worse,
/communitycalls<% if @top_authors.any? %>and<% @top_authors.each %>, executing that 950k-cost query twice per uncached render!

Waiting for PostgreSQL to run two full-table scans across 1,000,000 user rows.

My simple bug fix accidentally generating a database denial-of-service vector.
β‘ Round 2: The Correlated NOT EXISTS Anti-Join
To fix the query regression without dropping role protection, we replaced the nested NOT IN calls with a correlated NOT EXISTS anti-join:
# app/controllers/community_controller.rb
.where.not(
UserRole.joins(:role)
.where("users_roles.user_id = users.id")
.where(roles: { name: %w[suspended spam], resource_type: nil, resource_id: nil })
.select(1)
.arel.exists
)
π§ Why this changes everything for PostgreSQL:
Instead of dumping nearly a million IDs into memory for a sluggish NOT IN array, PostgreSQL executes a clean Nested Loop Anti-Join using:
- π― An Index-Only Scan on
index_users_roles_on_user_id_and_role_id - π― An Index Scan on
roles_pkey
π The Scoreboard:
- π Old baseline on main:
216,129 - π₯ My initial attempt:
953,586(4.4x regression!) - β‘ With NOT EXISTS anti-join:
179,613(Faster than main!)

Zero sequential scans on the users table. Pure index scans.
π§ͺ Bonus: Slashing Test Suite Overhead
While profiling the specs in spec/requests/community_spec.rb, we noticed the test suite was creating 35 database records using 25.times and 10.times loops just to verify author ordering β±οΈ.
By tightening the fixtures down to just 3 targeted articles (e.g., Author A with 1 post & 50 reactions vs. Author B with 2 posts & 0 reactions), we tested the exact same ordering branches while cutting down factory churn across CI shards β‘ποΈ.
π The Aftermath
(πΈ After: Popular Community Members now showcases actual community writers based on verified engagement and public reactions)
The PR (#24324) was approved and merged into forem:main by @mikeydorje! ππ€
π‘ Key Lessons for Platform Builders:
- π’ Activity β Engagement: If your algorithm rewards sheer publishing volume without weighting community response, bots will always game the system.
- β οΈ Beware of
without_rolechains: Convenience helpers in ORMs can hide expensive nestedNOT INsubqueries that degrade exponentially as your tables scale. - π¬ Always check
EXPLAIN: An index scan vs. a sequential scan is the difference between a snappy 50ms render and taking down production during peak traffic.
Have you ever encountered a metric or leaderboard on your platform that got completely hijacked by automation? Let's chat in the comments! ππ¬


Top comments (34)
And lets hear from the popular guys on DEV:
Sir @francistrdev, Sir @georgekobaidze, Ma'am @sylwia-lask, my fav @xulingfeng, @hemapriya_kanagala di, @technogamerz di and our beloved and currently most popular: @heyitsjem, Did it help?
Have A Great Day Everybody!
Bro, this is amazing! There are tons of accounts spamming bulk posts nonstop. As someone with a compulsion to clear notification red dots and a likingβforβlikes habit, I was honestly getting overwhelmed. Congrats on fixing the issue and gaining great insights along the way!π
Yeah really!
I mean 720, 250, 350 these are like insane numbers!
Nobody can type this much! I really wanted to fix this issue to ensure atleast we don't give spotlight to bots.
And thanks! Have A Great Day(or Night)! π
Totally agree, those volumes are clearly impossible for human writers. Itβs such a meaningful fix to keep real creators visible! As a QA person, I can easily imagine how many edge cases you needed to cover for this ranking logic. Have a good one ποΈππ
This is absolutely fantastic! You're addressing the problem that has been driving me crazy. Like I don't even know what's real anymore.
I'm really tired of reading comments that are apparently written by bots. This platform is amazing, but it also has this problem that needs to be fixed as soon as possible.
Thanks for this!
Bhai, seriously, thanks a lot for fixing this! Seeing actual people who put real effort into writing finally get highlighted instead of random spam bots feels like a huge win π
Thanks, di π
Happy Diwali, bhai (@dj29) π
Happy Diwali to you too, Diπ
Hey Dhruv! I actually saw you contribute to this in real time! Good work on getting this in!
I do recommend to making it span over 6 months than having it in a single month. I am not sure if this is the use case, but there can be cases where a user makes a lot of posts and all of them are bot engagements. Something you might need to consider (not sure if you cover that already).
Otherwise, good work and thanks for the mention :D
Actually Sir @francistrdev, that's the thing. Bot posts are RSS feeds for which you don't need to go to dev.to to create post.
Also, keeping it in month helps cause over span of 6 months people can have a lot of posts and I've seen people post like 3 times a week. And we definitely want quality stats of posts for popularity.
But we can have something like popularity leaderboard for fun! But that'll probably ruin the fun of interactions and everyone will be posting, commenting for the sake of it.
I might be wrong but this is what I feel.
Now over to you.π
Ahh that makes sense! I don't think we need a leaderboard, though it's an idea.
Did you also have a use case where popularity is also based on the number of gems you have? I mention this because I notice that it does also count into your visibility as you become trustworthy.
That could help with the 6 month extension since it contributes to it and since it can't be rigged, according to @ben, I think it should work and would solve most cases. Unless the team chooses a Curator that turns out to be a bot, that's our fault lol.
Yeah we can do that, maybe give the week, 1 month, 6 month filter like we have for posts.
But for now, from my knowledge, it isn't taken into account. We can do that.
Sounds good! Also, I think you will be a great DEVenger. Don't you think?
cc: @itsugo @georgekobaidze @hemapriya_kanagala @codingwithjiro
Sure!!! Will be an honor.π
@francistrdev agreed!π«‘
Did it! Officially A DEVenger now.
Now 1 genuine question, now what? Like what do we do here?
Pretty much post whatever you believe is high quality and useful for everyone on DEV. There suppose to be a discord server, but still have no time for it yet lol.
You have my discord btw, so if anything (planning on posting or such), just lmk there and I can review before submitting :)
Sure! Sounds good.
Thanks for the opportunity.
Have A Great Day!
Welcome to the team @dj29 π
Thanks!π
Welcome to the team, bhai π
π€ 779 posts in a month. Almost zero reactions. And somehow, that's what popularity looked like.
What started as a random visit to DEV's Community Hub turned into a rabbit hole involving RSS bots, ActiveRecord, PostgreSQL query plans, and a rather unexpected 4.4x performance regression in my first attempted fix.
The best part? The final solution wasn't just about filtering bots. It was about understanding what the database was actually doing under the hood.
Huge thanks to @mikeydorje Sir who reviewed and helped get the fix merged into Forem! π€
Now I'm curious: Have you ever found a leaderboard, ranking system, or popularity metric being completely gamed by automation? I'd love to hear the story!
Dhruv, youβve done a really wonderful job! Thank you so much for this. Have a Great night, and by the way, Happy Diwali! πͺβ¨
Thanks di.
For a moment I was like: I missed Diwali?π
But yeah, Have A Great Night!
And Happy Diwali! πͺβ¨
You missed Diwali?! Really?
If you work with your headphones on, that's good otherwise, Diwali firecrackers will definitely give you a mini jumpscare!
Na I meant, "Diwali" is on Sunday. Since its a 5 day festival, today is what we call in Gujarat: ΰͺ ΰͺΰͺΏΰͺ―ΰͺΎΰͺ°ΰͺΈ
But the festival and vacations have started!
Have A Great Night, di.
Is today Gyaras or Baras here? I mean, can I cook rice today? Tomorrow is Teras (Dhanteras), right?π€
Here, we're celebrating Gyaras and I think(can be wrong) We cook rice but not khichdi during Diwali.
Tomorrow is dhanteras probably, cause its like 2 tithi are together on some day. Probably Baras and dhanteras tomorrow or Gyaras and baras today. But today is definitely Gyaras and tomorrow is dhanteras, Baras comes in between but doesn't stay for a whole day! and Chaudash on satursay and finally Diwali on Sunday.
Hope that helps, Di.π
Thanks for the help, bro! :D
Good point! π
Thanks!
Have A Great Day!
Great bro! keep going.
Thanks!