[GH-ISSUE #1726] Authenticated free-tier users should be rate-limited by user ID, not IP address #1198

Closed
opened 2026-05-07 01:00:46 +02:00 by BreizhHardware · 5 comments

Originally created by @tflpd on GitHub (May 6, 2026).
Original GitHub issue: https://github.com/binwiederhier/ntfy/issues/1726

🐞 Describe the bug

Authenticated free-tier users are rate-limited by IP address rather than by their user identity. This causes a noisy neighbor problem where unrelated users sharing the same IP address share a single rate-limit bucket.

I run a Cloudflare Worker that sends ~1 notification/day to ntfy.sh. Despite my minimal usage, I consistently hit the

"daily message quota reached" error (HTTP 429, code 42908):
{
  "code": 42908,
  "http": 429,
  "error": "limit reached: daily message quota reached; increase your limits with a paid plan, see https://ntfy.sh/docs/publish/#limitations",
  "link": "https://ntfy.sh/docs/publish/#limitations"
}

This happens because:

  1. Cloudflare Workers share egress IPs across thousands of customers' workers
  2. My worker authenticates with Basic Auth, but since I'm on the free tier, my visitor ID is still "ip:<cloudflare_egress_ip>"
  3. Other ntfy.sh users whose requests happen to exit through the same Cloudflare IP are consuming the shared rate limit bucket
  4. By the time my 1 daily notification runs, the shared IP's quota is often exhausted
    The root cause is the visitorID() function in server/visitor.go, which only uses user-based tracking when u.Tier != nil.

An authenticated free-tier user gets visitor ID "ip:1.2.3.4" instead of "user:u_abc123", even though the server has positively identified who they are.

Proposed fix: Change the condition to u != nil so all authenticated users are tracked by user ID. This gives free users the same limits they have today, just tracked against their own identity rather than a shared IP. No additional resources are granted — only fair accounting.

Additionally, stats persistence in server.go:980 should be extended to free authenticated users so counters survive server restarts (currently EnqueueUserStats is only called when u.Tier != nil).

This same problem affects any serverless platform (AWS Lambda, Vercel, Fly.io), corporate NAT, VPNs, or university networks.

💻 Components impacted
ntfy server

🔮 Additional context

Abuse considerations: Account creation is already rate-limited to 3 per IP per day (DefaultVisitorAccountCreationLimitBurst = 3), and ntfy.sh requires email verification. An attacker could at most triple their limits from a single IP, which is less impactful than the current situation where a single noisy neighbor can block thousands of legitimate users sharing an IP on platforms like Cloudflare Workers.

Related issues:

  • #144 — Original shared-IP rate limiting problem with UnifiedPush/Matrix
  • #1106 — Still-open follow-up requesting IP allowlisting as a workaround
  • #609 / #633 — Subscriber-billed topics (same class of fix, scoped to UnifiedPush only)
  • #1173 — Rate limit header transparency

The subscriber-billed topics work in #633 already established the precedent that identity-based rate limiting is preferable to IP-based rate limiting. This proposal extends that principle to all authenticated users.

Originally created by @tflpd on GitHub (May 6, 2026). Original GitHub issue: https://github.com/binwiederhier/ntfy/issues/1726 :lady_beetle: **Describe the bug** Authenticated free-tier users are rate-limited by IP address rather than by their user identity. This causes a noisy neighbor problem where unrelated users sharing the same IP address share a single rate-limit bucket. I run a Cloudflare Worker that sends ~1 notification/day to ntfy.sh. Despite my minimal usage, I consistently hit the ``` "daily message quota reached" error (HTTP 429, code 42908): { "code": 42908, "http": 429, "error": "limit reached: daily message quota reached; increase your limits with a paid plan, see https://ntfy.sh/docs/publish/#limitations", "link": "https://ntfy.sh/docs/publish/#limitations" } ``` This happens because: 1. Cloudflare Workers share egress IPs across thousands of customers' workers 2. My worker authenticates with Basic Auth, but since I'm on the free tier, my visitor ID is still "ip:<cloudflare_egress_ip>" 3. Other ntfy.sh users whose requests happen to exit through the same Cloudflare IP are consuming the shared rate limit bucket 4. By the time my 1 daily notification runs, the shared IP's quota is often exhausted The root cause is the `visitorID()` function in [server/visitor.go](https://github.com/binwiederhier/ntfy/blob/main/server/visitor.go#L532-L543), which only uses user-based tracking when `u.Tier != nil`. An authenticated free-tier user gets visitor ID "ip:1.2.3.4" instead of "user:u_abc123", even though the server has positively identified who they are. **Proposed fix**: Change the condition to `u != nil` so all authenticated users are tracked by user ID. This gives free users the same limits they have today, just tracked against their own identity rather than a shared IP. No additional resources are granted — only fair accounting. Additionally, stats persistence in [server.go:980](https://github.com/binwiederhier/ntfy/blob/main/server/server.go#L980) should be extended to free authenticated users so counters survive server restarts (currently `EnqueueUserStats` is only called when `u.Tier != nil`). This same problem affects any serverless platform (AWS Lambda, Vercel, Fly.io), corporate NAT, VPNs, or university networks. :computer: **Components impacted** ntfy server :crystal_ball: **Additional context** Abuse considerations: Account creation is already rate-limited to 3 per IP per day (`DefaultVisitorAccountCreationLimitBurst` = 3), and `ntfy.sh` requires email verification. An attacker could at most triple their limits from a single IP, which is less impactful than the current situation where a single noisy neighbor can block thousands of legitimate users sharing an IP on platforms like Cloudflare Workers. Related issues: - #144 — Original shared-IP rate limiting problem with UnifiedPush/Matrix - #1106 — Still-open follow-up requesting IP allowlisting as a workaround - #609 / #633 — Subscriber-billed topics (same class of fix, scoped to UnifiedPush only) - #1173 — Rate limit header transparency The subscriber-billed topics work in #633 already established the precedent that identity-based rate limiting is preferable to IP-based rate limiting. This proposal extends that principle to all authenticated users.
BreizhHardware 2026-05-07 01:00:46 +02:00
  • closed this issue
  • added the
    🪲 bug
    label
Author
Owner

@tflpd commented on GitHub (May 6, 2026):

Happy to put up the PR and tests if y'all agree with the approach! 😊

<!-- gh-comment-id:4384189248 --> @tflpd commented on GitHub (May 6, 2026): Happy to put up the PR and tests if y'all agree with the approach! 😊
Author
Owner

@binwiederhier commented on GitHub (May 6, 2026):

This is not a bug. It's by design. Otherwise people could trivially circumvent the rate limiting.

<!-- gh-comment-id:4384221356 --> @binwiederhier commented on GitHub (May 6, 2026): This is not a bug. It's by design. Otherwise people could trivially circumvent the rate limiting.
Author
Owner

@tflpd commented on GitHub (May 6, 2026):

I understand the concern, but I'd argue IP-based tracking doesn't actually prevent abuse -- it just shifts the vector from "create accounts" to "rotate IPs" which requires no account, no email, and no identity at all.

Account creation is already limited to 3/IP/day and requires email verification, so the multiplication factor is capped and traceable.

Meanwhile, the current design causes real collateral damage for legitimate authenticated users on shared infrastructure (serverless platforms, corporate NAT, etc). User-based tracking would actually be harder to abuse because accounts are identifiable and bannable, unlike anonymous IP rotation.

<!-- gh-comment-id:4384292723 --> @tflpd commented on GitHub (May 6, 2026): I understand the concern, but I'd argue IP-based tracking doesn't actually prevent abuse -- it just shifts the vector from "create accounts" to "rotate IPs" which requires no account, no email, and no identity at all. Account creation is already limited to 3/IP/day and requires email verification, so the multiplication factor is capped and traceable. Meanwhile, the current design causes real collateral damage for legitimate authenticated users on shared infrastructure (serverless platforms, corporate NAT, etc). User-based tracking would actually be harder to abuse because accounts are identifiable and bannable, unlike anonymous IP rotation.
Author
Owner

@binwiederhier commented on GitHub (May 6, 2026):

I understand that you have a different perspective. You are free to fork the project and implement your own changes.

This has worked and is how I would like to do rate limiting for ntfy. Thanks for using ntfy.

<!-- gh-comment-id:4384303318 --> @binwiederhier commented on GitHub (May 6, 2026): I understand that you have a different perspective. You are free to fork the project and implement your own changes. This has worked and is how I would like to do rate limiting for ntfy. Thanks for using ntfy.
Author
Owner

@tflpd commented on GitHub (May 6, 2026):

Very fair, I respect that, thank you for the quick response!

Just out of curiosity, do you disagree on whether this is an issue for serverless platforms, or do you agree it's an issue but believe the current rate limiting approach is better for the overall user base?

I raised this thinking it fell in the same bucket as #144, so I was curious about the distinction.

<!-- gh-comment-id:4384335847 --> @tflpd commented on GitHub (May 6, 2026): Very fair, I respect that, thank you for the quick response! Just out of curiosity, do you disagree on whether this is an issue for serverless platforms, or do you agree it's an issue but believe the current rate limiting approach is better for the overall user base? I raised this thinking it fell in the same bucket as #144, so I was curious about the distinction.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/ntfy#1198
No description provided.