# /polls/voters.json returning duplicate users across paged requests

**URL:** https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636
**Category:** Bug
**Tags:** polls, pr-welcome, rest-api
**Created:** [July 30, 2025, 11:06pm UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636 "2025-07-30T23:06:16Z")
**Posts on this page:** 11
**Page:** 1

<div class="post-metadata">

### Author: ![robmackenzie](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/robmackenzie/32/515453_2.png) [@robmackenzie](https://meta.discourse.org/u/robmackenzie)
#### Post date: [July 30, 2025, 11:06pm UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/1 "2025-07-30T23:06:16Z")

</div>

When calling the /polls/voters.json endpoint, using pagination, we’ve found that the first call returns 25 users as expected, but the 2nd page returns 26 users, one of which was returned in the first call. This is consistent whenever there are over 25 users.

The problem is here in the poll plugin for discourse:

> <https://github.com/discourse/discourse/blob/main/plugins/poll/lib/poll.rb#L223>

The offset is calculated assuming that postgress SQL is exclusive with a bound of the row using BETWEEN, when it is inclusive.

The first query actually returns 25, because the calculated rows actually start a 1, not 0.

My proposed fix is as simple as:

```plaintext
   params = {
      offset: offset + 1,
      offset_plus_limit: offset + limit,
      option_digest: opts[:option_id].presence,
    }

```

Or, a more elegant solution might be to use postgres LIMIT and OFFSET

```plaintext
    params = {
      limit: limit,
      offset: offset,
      option_digest: opts[:option_id].presence,
    }

```

```plaintext
          WHERE pv.poll_id IN (:poll_ids)
                /* where */
        ) v
        ORDER BY digest, CASE WHEN rank = 'Abstain' THEN 1 ELSE CAST(rank AS integer) END, username
		LIMIT :limit OFFSET :offset
      SQL

```

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [July 30, 2025, 11:32pm UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/2 "2025-07-30T23:32:49Z")

</div>

Nice catch, putting a #pr-welcome on this in case anyone wants to give fixing this a go.

We need to confirm we don’t break existing stuff in polls in case the front end incorrectly depends on wrong assumptions here.

---

<div class="post-metadata">

### Author: ![robmackenzie](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/robmackenzie/32/515453_2.png) [@robmackenzie](https://meta.discourse.org/u/robmackenzie)
#### Post date: [July 30, 2025, 11:48pm UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/3 "2025-07-30T23:48:55Z")

</div>

Thanks for quick reply!  
I’ll try to set up an env, didn’t wanna PR without testing. Anyone feel free to beat me to it. I’m not a ruby dev at all. I also don’t know about changing spec or tests.

Hilariously in the UI there is code that dumps the results into a set to bypass this bug.

> <https://github.com/discourse/discourse/blob/99abfb21f0ee896a8a38612b4a0946cffacc69ed/plugins/poll/assets/javascripts/discourse/components/poll.gjs#L500>

---

<div class="post-metadata">

### Author: ![robmackenzie](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/robmackenzie/32/515453_2.png) [@robmackenzie](https://meta.discourse.org/u/robmackenzie)
#### Post date: [July 31, 2025, 12:08am UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/4 "2025-07-31T00:08:41Z")

</div>

For anyone else viewing, we put in a similar UI fix for our raffle software here.

[https://github.com/vhs/raffle/issues/27](https://github.com/vhs/raffle/issues/27)

Cheers to @lukecyca from VHS for identifying the area of issue.

---

<div class="post-metadata">

### Author: ![Moin](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/moin/32/554653_2.png) [@Moin](https://meta.discourse.org/u/Moin)
#### Post date: [July 31, 2025, 5:26am UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/5 "2025-07-31T05:26:09Z")

</div>

There is also a related report about the loading of more voters in polls from January.

> [@Button to reveal more votes appends the same voters repeatedly](https://meta.discourse.org/t/button-to-reveal-more-votes-appends-the-same-voters-repeatedly/345090):
>
> Something similar to [Polls showing duplicate voters](https://meta.discourse.org/t/polls-showing-duplicate-voters/102838) is happening again. Repeatedly clicking the button to show more votes for a given question ends up showing the same one or two voters repeatedly: Seen in Discourse v3.3.3 on Firefox 134 on macOS and in Mobile Safari.

---

<div class="post-metadata">

### Author: ![merefield](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/merefield/32/176214_2.png) [@merefield](https://meta.discourse.org/u/merefield)
#### Post date: [July 31, 2025, 7:08am UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/7 "2025-07-31T07:08:59Z")

</div>

Great catch.

It’s even more funny than that. I submitted a couple of very large PRs a while back to modernise the front end and add Ranked Choice.

The huge scale of those PRs definitely contributed to me missing this and back end fixes were not really in scope (though in the process of doing those PRs I did actually resolve a lot of issues that weren’t strictly for those PRs to fix)

I admit to matching that front end precedent [that was set back in 2018](https://github.com/discourse/discourse/pull/6746) for ranked choice without really thinking about it 😅. (Which you guys just also followed, albeit knowingly 😆)

Amazing how workarounds can persist without the underlying issue being identified.

---

<div class="post-metadata">

### Author: ![clechasseur](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/clechasseur/32/517802_2.png) [@clechasseur](https://meta.discourse.org/u/clechasseur)
#### Post date: [August 17, 2025, 10:21pm UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/8 "2025-08-17T22:21:58Z")

</div>

So I thought I’d take a stab at this one as my first contribution, but it looks like it might be a bit hairier than I expected. 😉

I have a Draft PR up: [FIX: do not return duplicates from /polls/voters.json by clechasseur · Pull Request #1 · clechasseur/discourse · GitHub](https://github.com/clechasseur/discourse/pull/1)

In this PR, I first added tests that reproduce the problem, then I applied Rob’s simple fix and it made the tests pass.

I also tried the more elegant solution (the one I’d have preferred), but although it does prevent duplicate voters, it also changes which voter is returned in which page, including the first, which could potentially be considered a breaking change (depending on how the front end deals with it - I haven’t yet looked).

Taking a step back, however, I am left wondering what is the true meaning of the `limit` parameter when you call this endpoint - it doesn’t really limit the numbers of _total_ voters returned, merely the number of voters returned _for each poll option_. You can see this effect in the test I added for the multi-choice poll [here](https://github.com/clechasseur/discourse/blob/5ebb4ca9f647dbfa8251ebbe1d9b16fd967fd11c/plugins/poll/spec/lib/poll_spec.rb#L441-L452) - the first page is indeed limited to 2 voters per option, but in total, three different voters are returned (scattered across options). Switching to the elegant solution (i.e., using `LIMIT :limit OFFSET :offset`) results in the `limit` being applied to the total number of _votes_ and not voters. I’m not 100% sure it’s better or more intuitive.

Anyway, I am new to this so I may be overthinking this. The simple solution does remove duplicate voters and doesn’t wreak too much havok, so it might be the way to go. I’ll wait for input before submitting a PR to the parent repo.

–

As an aside, I think there is another bug in this part of the code. The query to load voters is ordered by digest, rank and username - but when ordering by rank, it uses this condition:

```sql
CASE WHEN rank = 'Abstain' THEN 1 ELSE CAST(rank AS integer) END

```

However, `’Abstain’` actually corresponds to rank 0, not 1 - rank 1 can also be returned as `’1’`. This potentially makes the ordering non-deterministic across queries, which means that depending on the number of voters and the `limit` value used, it could be possible to actually miss voters when doing paginated queries. In my new tests, I had to sort the returned voters to get around the non-deterministic nature. (Because it’s non-deterministic, I assume it’s not easy to reproduce in a test, but I can take a shot at it…)

---

<div class="post-metadata">

### Author: ![clechasseur](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/clechasseur/32/517802_2.png) [@clechasseur](https://meta.discourse.org/u/clechasseur)
#### Post date: [August 19, 2025, 1:12am UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/9 "2025-08-19T01:12:15Z")

</div>

> [@clechasseur](#):
>
> This potentially makes the ordering non-deterministic across queries, which means that depending on the number of voters and the `limit` value used, it could be possible to actually miss voters when doing paginated queries.

I followed the link above to a previous report, which includes a like to a [previous _previous_ report](https://meta.discourse.org/t/polls-showing-duplicate-voters/102838) which includes something that seems to point to this possibility as well:

> [@Polls showing duplicate voters](https://meta.discourse.org/t/polls-showing-duplicate-voters/102838/1):
>
> Interestingly enough, I switched my vote to another option and then switched back, and now I don’t show up at all.

That’s a long time ago however so the code might have been totally different back then. (I tried going back in time with Git but the history stopped in 2021 so I assume the code moved at some point.)

---

<div class="post-metadata">

### Author: ![clechasseur](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/clechasseur/32/517802_2.png) [@clechasseur](https://meta.discourse.org/u/clechasseur)
#### Post date: [August 20, 2025, 6:18am UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/10 "2025-08-20T06:18:26Z")

</div>

After some experimentation, I was unable to craft a test that caused the non-deterministic ordering to cause a problem with returned voters. I think this is because of the way the query is crafted and because of how the poll votes are created, but I am not 100% certain.

I’ve created a PR with my test and the fixes I propose: [FIX: Avoid returning duplicate voters from `/polls/voters.json` endpoint by clechasseur · Pull Request #34433 · discourse/discourse](https://github.com/discourse/discourse/pull/34433)

---

<div class="post-metadata">

### Author: ![nat](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/nat/32/235063_2.png) [@nat](https://meta.discourse.org/u/nat)
#### Post date: [September 30, 2025, 7:06am UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/12 "2025-09-30T07:06:14Z")

</div>

Thanks a ton for the fix and your patience for our review.

I’ve approved and merged it. 🥳

---

<div class="post-metadata">

### Author: ![nat](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/nat/32/235063_2.png) [@nat](https://meta.discourse.org/u/nat)
#### Post date: [October 4, 2025, 12:00am UTC](https://meta.discourse.org/t/polls-voters-json-returning-duplicate-users-across-paged-requests/376636/13 "2025-10-04T00:00:52Z")

</div>

This topic was automatically closed after 3 days. New replies are no longer allowed.
