DEV Community

Cover image for Popularity Depends on How You Define It: How 700 RSS Bot Posts Took Over DEV's Community Hub πŸ€–πŸ’₯
Dhruv Jani
Dhruv Jani Subscriber

Posted on Edited on AI-assisted

Popularity Depends on How You Define It: How 700 RSS Bot Posts Took Over DEV's Community Hub πŸ€–πŸ’₯

Led to a 4.4x PostgreSQL query regression

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.

Jim Carrey typing furiously
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)
Enter fullscreen mode Exit fullscreen mode

Look closely at what was missing:

  1. 🚫 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 checked published_from_feed: false. If a feed bot syndicated 700 markdown drafts, every single one counted toward their author standing.
  2. πŸ“Š Post Count as a Tie-Breaker: By ordering with recent_articles_count DESC, sheer output volume trumped community feedback.
  3. πŸ’” 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.

Screenshot before bug fix
(πŸ“Έ 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:

  1. 🧹 Exclude feed-imported posts: .where(published_from_feed: false)
  2. ❀️ Rank primarily by real human engagement: SUM(articles.public_reactions_count) DESC
  3. πŸ›‘οΈ Exclude suspended/spam users: .without_role(:suspended).without_role(:spam)

I opened PR #24324, added request specs, and pushed πŸš€.

Leonardo DiCaprio as Jordan Belfort nodding in approval
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 on users_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 ...
)
Enter fullscreen mode Exit fullscreen mode

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:

  1. πŸ›‘ It abandoned the fast index scan on users_pkey.
  2. πŸ”„ It triggered a Parallel Seq Scan on users across 6 workers (~348,000 rows per worker).
  3. 🐒 It ran an outer full-table Seq Scan across 988,000 rows.
  4. πŸ’£ To make matters worse, /community calls <% if @top_authors.any? %> and <% @top_authors.each %>, executing that 950k-cost query twice per uncached render!

Mr. Bean waiting and checking his watch
Waiting for PostgreSQL to run two full-table scans across 1,000,000 user rows.

Troy from Community walking in with pizza to find the room on fire
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
)
Enter fullscreen mode Exit fullscreen mode

🧠 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!)

Keanu Reeves Neo dodging bullets in The Matrix
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

Screenshot of community page after PR

(πŸ“Έ 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! πŸŽ‰πŸ€

Leonardo DiCaprio Great Gatsby raising a champagne toast

πŸ’‘ Key Lessons for Platform Builders:

  1. πŸ“’ Activity β‰  Engagement: If your algorithm rewards sheer publishing volume without weighting community response, bots will always game the system.
  2. ⚠️ Beware of without_role chains: Convenience helpers in ORMs can hide expensive nested NOT IN subqueries that degrade exponentially as your tables scale.
  3. πŸ”¬ 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)

Collapse
 
dj29 profile image
Dhruv Jani •

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!

Picked as gem
Collapse
 
xulingfeng profile image
xulingfeng •

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!πŸŽ‰

Picked as gem
Collapse
 
dj29 profile image
Dhruv Jani •

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)! πŸ˜„

Thread Thread
 
xulingfeng profile image
xulingfeng •

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 πŸ‘οΈπŸŽ‰πŸ˜†

Collapse
 
georgekobaidze profile image
Giorgi Kobaidze •

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!

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala •

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 πŸ˜ƒ

Collapse
 
dj29 profile image
Dhruv Jani •

Thanks, di πŸ˜„

Thread Thread
 
hemapriya_kanagala profile image
Hemapriya Kanagala •

Happy Diwali, bhai (@dj29) πŸ˜€

Thread Thread
 
dj29 profile image
Dhruv Jani •

Happy Diwali to you too, DiπŸ˜„

Collapse
 
francistrdev profile image
FrancisTRᴅᴇᴠ •

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

Picked as gem
Collapse
 
dj29 profile image
Dhruv Jani • • Edited

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.

So in short I removed those posts into popularity consideration which were RSS feed posts and were from flagged users.

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.πŸ˜…

Collapse
 
francistrdev profile image
FrancisTRᴅᴇᴠ •

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.

Thread Thread
 
dj29 profile image
Dhruv Jani •

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.

Thread Thread
 
francistrdev profile image
FrancisTRᴅᴇᴠ •

Sounds good! Also, I think you will be a great DEVenger. Don't you think?

cc: @itsugo @georgekobaidze @hemapriya_kanagala @codingwithjiro

Thread Thread
 
dj29 profile image
Dhruv Jani •

Sure!!! Will be an honor.πŸ˜„

Thread Thread
 
georgekobaidze profile image
Giorgi Kobaidze •

@francistrdev agreed!🫑

Thread Thread
 
dj29 profile image
Dhruv Jani •

Did it! Officially A DEVenger now.

Now 1 genuine question, now what? Like what do we do here?

Thread Thread
 
francistrdev profile image
FrancisTRᴅᴇᴠ •

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 :)

Thread Thread
 
dj29 profile image
Dhruv Jani •

Sure! Sounds good.
Thanks for the opportunity.
Have A Great Day!

Thread Thread
 
codingwithjiro profile image
Elmar Chavez •

Welcome to the team @dj29 πŸŽ‰

Thread Thread
 
dj29 profile image
Dhruv Jani •

Thanks!πŸ˜„

Thread Thread
 
hemapriya_kanagala profile image
Hemapriya Kanagala •

Welcome to the team, bhai πŸ˜ƒ

Collapse
 
dj29 profile image
Dhruv Jani •

πŸ€– 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!

Collapse
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ •

Dhruv, you’ve done a really wonderful job! Thank you so much for this. Have a Great night, and by the way, Happy Diwali! πŸͺ”βœ¨

Collapse
 
dj29 profile image
Dhruv Jani •

Thanks di.
For a moment I was like: I missed Diwali?πŸ˜…
But yeah, Have A Great Night!
And Happy Diwali! πŸͺ”βœ¨

Collapse
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ • • Edited

You missed Diwali?! Really?
If you work with your headphones on, that's good otherwise, Diwali firecrackers will definitely give you a mini jumpscare!

Thread Thread
 
dj29 profile image
Dhruv Jani •

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.

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ •

Is today Gyaras or Baras here? I mean, can I cook rice today? Tomorrow is Teras (Dhanteras), right?πŸ€”

Thread Thread
 
dj29 profile image
Dhruv Jani •

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.πŸ˜„

Thread Thread
 
technogamerz profile image
π“π‘πž π‹πšπ³π² 𝐆𝐒𝐫π₯ • • Edited

Thanks for the help, bro! :D

Collapse
 
gracelee087 profile image
Sohee Lee •

Good point! πŸ‘

Collapse
 
dj29 profile image
Dhruv Jani •

Thanks!
Have A Great Day!

Collapse
 
yug_vasava profile image
Yug Vasava •

Great bro! keep going.

Collapse
 
dj29 profile image
Dhruv Jani •

Thanks!