Show the reset time and the sync delay on the account page, document shared daily limits

Claude-Session: https://claude.ai/code/session_01DeXuhSjq5Gta5qPJgSceFe
This commit is contained in:
binwiederhier committed 2026-10-30 20:42:27 +00:00
1 parent c113e20677
commit c3ecea55c4
5 files changed
+46 -10

No files matched your search

+16 -3
View File
@@ -410,7 +410,7 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
</div>
## Database options
ntfy uses a database for storing messages ([message cache](#message-cache)), users and [access control](#access-control), and [web push](#web-push) subscriptions.
ntfy uses a database for storing messages ([message cache](#message-cache)), users and [access control](#access-control) (which includes the visitors' daily usage counts, see [message limits](#message-limits)), and [web push](#web-push) subscriptions.
You can choose between **SQLite** and **PostgreSQL** as the database backend.
### SQLite
@@ -1926,7 +1926,17 @@ For instance, if the request limit allows for 15,000 requests per day, and all o
to publish messages, then that is the daily message limit.
To limit the number of daily messages per visitor, you can set `visitor-message-daily-limit`. This defines the number
of messages a visitor can send in a day. This counter is reset every day at midnight (UTC).
of messages a visitor can send in a day. The day is a rolling 24-hour period per visitor (per account, or per IP address
for anonymous visitors), not the calendar day: each visitor's period starts at its own time of day, derived from the
visitor, so not every count resets at once. The account page shows when a visitor's counts reset next.
The daily counts (messages, e-mails and phone calls) are kept in the user database if `auth-file` (SQLite) or
`database-url` (PostgreSQL) is set, so they survive a restart. With PostgreSQL, all ntfy servers sharing the database
also share the counts: each server writes what it counted every 5 seconds and reads what the others counted, so a visitor
gets one daily allowance in total, not one per server (it may overshoot by a few seconds of traffic). An anonymous
visitor's message count is only written once it reaches 10% of its daily limit, so the many visitors that publish a
few messages never touch the database; a restart forgets those few. Without a user database, the counts are kept in
memory only and start over with every restart.
### Attachment limits
Aside from the global file size and total attachment cache limits (see [above](#attachments)), there are two relevant
@@ -1948,6 +1958,9 @@ are enabled):
* `visitor-email-limit-burst` is the initial bucket of emails each visitor has. This defaults to 16.
* `visitor-email-limit-replenish` is the rate at which the bucket is refilled (one email per x). Defaults to 1h.
Visitors without a tier can send at most 24h / `visitor-email-limit-replenish` e-mails per day (24 by default), a daily
limit that, like the message limit, survives restarts and is shared between servers if a user database is configured.
### Topic creation limits
To mitigate topic-enumeration / squatting attacks (where a single source pokes thousands of guessable
topic names to inflate the server's in-memory topic map), there is a per-visitor limit on how many *new*
@@ -2559,7 +2572,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily traffic limit per visitor, covering attachment downloads/uploads and messages replayed from the cache by poll requests. This is to protect your bandwidth costs from exploding. |
| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor |
| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Rate limiting: Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled |
| `visitor-message-daily-limit` | `NTFY_VISITOR_MESSAGE_DAILY_LIMIT` | *number* | - | Rate limiting: Allowed number of messages per day per visitor, reset every day at midnight (UTC). By default, this value is unset. |
| `visitor-message-daily-limit` | `NTFY_VISITOR_MESSAGE_DAILY_LIMIT` | *number* | - | Rate limiting: Allowed number of messages per day per visitor, reset every 24 hours at the visitor's own time of day. By default, this value is unset. |
| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP/CIDR list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
+12
View File
@@ -2288,6 +2288,18 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet
### ntfy server v2.36.0 (UNRELEASED)
**Features:**
* Daily limits (messages, e-mails, phone calls) are kept in the user database (`auth-file` for SQLite, or `database-url` for PostgreSQL) and survive restarts; with PostgreSQL, all servers share them, so a visitor gets one daily allowance in total, not one per server (see [message limits](config.md#message-limits))
**Behavior changes:**
* Daily limits now reset on a rolling 24-hour period per account or IP address instead of for everyone at midnight UTC: each visitor's period starts at its own time of day, which spreads the resets over the day. The account page and the `/v1/account` API (`stats.reset_at`) show when the visitor's counts reset
* Visitors without a tier have a daily e-mail limit again (24h / `visitor-email-limit-replenish`), now that it is shared between servers and survives restarts
* The daily reset job is gone; the `stats_*` columns of the user table are zeroed by the upgrade and no longer read or written
### ntfy iOS app v1.8.0 (UNRELEASED)
**Features:**
+6 -3
View File
@@ -398,9 +398,12 @@
# visitor-request-limit-replenish: "5s"
# visitor-request-limit-exempt-hosts: ""
# Rate limiting: Hard daily limit of messages per visitor and day. The limit is reset
# every day at midnight UTC. If the limit is not set (or set to zero), the request
# limit (see above) governs the upper limit.
# Rate limiting: Hard daily limit of messages per visitor and day. The day is a rolling 24-hour
# period per visitor (account or IP), starting at a time of day of the visitor's own, so not
# every count resets at once. If the limit is not set (or set to zero), the request
# limit (see above) governs the upper limit. The daily counts (messages, emails, calls) are
# kept in the user database ("auth-file", or "database-url"), so they survive a restart and,
# on PostgreSQL, are shared by all servers; without one, they are counted in memory only.
#
# visitor-message-daily-limit: 0
+2 -1
View File
@@ -267,7 +267,8 @@
"account_usage_title": "Usage",
"account_usage_of_limit": "of {{limit}}",
"account_usage_unlimited": "Unlimited",
"account_usage_limits_reset_daily": "Usage limits are reset daily at midnight (UTC)",
"account_usage_limits_reset_at": "Daily usage limits reset every 24 hours, next at {{time}}",
"account_usage_approximate": "Usage figures are updated every few seconds and may be slightly behind.",
"account_basics_tier_title": "Account type",
"account_basics_tier_description": "Your account's power level",
"account_basics_tier_admin": "Admin",
+10 -3
View File
@@ -840,12 +840,16 @@ const AddPhoneNumberDialog = (props) => {
const Stats = () => {
const { t, i18n } = useTranslation();
const { account } = useContext(AccountContext);
const { dateFormat, timeFormat } = usePrefCache();
if (!account) {
return <></>;
}
const normalize = (value, max) => Math.min((value / max) * 100, 100);
const resetDaily = t("account_usage_limits_reset_at", {
time: formatDateTime(account.stats.reset_at, dateFormat, timeFormat),
});
return (
<Card sx={{ p: 3 }} aria-label={t("account_usage_title")}>
@@ -881,7 +885,7 @@ const Stats = () => {
title={
<>
{t("account_usage_messages_title")}
<Tooltip title={t("account_usage_limits_reset_daily")}>
<Tooltip title={resetDaily}>
<span>
<InfoIcon />
</span>
@@ -911,7 +915,7 @@ const Stats = () => {
title={
<>
{t("account_usage_emails_title")}
<Tooltip title={t("account_usage_limits_reset_daily")}>
<Tooltip title={resetDaily}>
<span>
<InfoIcon />
</span>
@@ -942,7 +946,7 @@ const Stats = () => {
title={
<>
{t("account_usage_calls_title")}
<Tooltip title={t("account_usage_limits_reset_daily")}>
<Tooltip title={resetDaily}>
<span>
<InfoIcon />
</span>
@@ -1018,6 +1022,9 @@ const Stats = () => {
</Pref>
)}
</PrefGroup>
<Typography variant="body1" sx={{ pt: 3 }}>
{t("account_usage_approximate")}
</Typography>
{account.role === Role.USER && account.limits.basis === LimitBasis.IP && (
<Typography variant="body1" sx={{ pt: 3 }}>
{t("account_usage_basis_ip_description")}