Rate Limits and Abuse Protection
Protect sign-in, uploads, post operations, and checkout with reusable rate limits. Define sensible defaults in your theme and let site admins adjust selected policies in Abuse and protection.
Define a policy
version 2;
// Allows normal catalog updates while limiting repeated product submissions.
// Creating and editing products share this allowance.
rateLimit product_write_attempts as "Product updates" {
limit: 30
window: "10m"
by: actor
adjustable: {
minLimit: 5
maxLimit: 100
minWindow: "1m"
maxWindow: "1h"
}
}
The label appears in the admin dashboard. Consecutive comments immediately above the definition become its description. Both // comments and /* block comments */ work; a blank line separates unrelated comments. Write these descriptions for site admins.
| Option | Meaning |
|---|---|
limit | Positive whole-number allowance, up to 100,000. |
window | Duration such as 30s, 10m, 1h, or 1d; from one second to one day. |
by: actor | Separate allowance per signed-in account; guests use their client IP. |
by: ip | Shared allowance for requests from the same client IP, including signed-in visitors. |
adjustable | Optional bounds for admin changes. Omit it to keep theme-defined values. All four bounds are required. |
scope | Optional automatic coverage for an operation group, described below. |
Allowances are separate for each site. A policy uses a fixed window starting with its first request; after that window ends, the next request starts a fresh allowance. Requests made just before and after a boundary can use both windows. Rate limits do not replace permissions or reservation rules.
Attach policies to post actions
Inside an existing post block, add:
rateLimit create, edit product_write_attempts;
rateLimit list product_list_attempts;
allow create: $admin.canCreate("products");
allow get: $post.current.data.visibility == "published" || $admin.canRead("$this");
allow edit: $admin.canEdit("products");
allow delete: $admin.canDelete("products");
Declare product_list_attempts as its own policy before using it. Supported post actions are create, get, list, edit, and delete. Reusing a policy name shares its allowance across those actions and post types. Different policy names have independent allowances.
Each explicitly requested post or list selector counts as an operation, regardless of how many rows it returns. A query containing three protected list selectors consumes three units. Reads and writes performed by FRL inside an endpoint or hook do not consume a second public-operation allowance.
Built-in account protection
Every site automatically has separate Sign-in attempts (sign_in) and Sign-up attempts (sign_up) policies. No theme declaration or profile binding is required. Both use the client IP, including when an account is already signed in. Unsuccessful attempts count too. For external sign-in providers, the sign-in allowance applies when the sign-in ticket is exchanged; fetching available providers and following redirects do not consume it. Creating a new account through a provider also checks the sign-up allowance.
| Policy | Default | Admin allowance range | Admin window range |
|---|---|---|---|
sign_in | 10 attempts per 10 minutes | 1–100 | 1 minute–1 day |
sign_up | 5 attempts per hour | 1–30 | 1 minute–1 day |
Site admins can adjust these policies in Abuse and protection. To customize their defaults, labels, descriptions, or adjustment ranges, declare the reserved name:
// Allow customers a few retries when signing in.
rateLimit sign_in as "Sign-in attempts" {
limit: 10
window: "10m"
adjustable: {
minLimit: 3
maxLimit: 30
minWindow: "1m"
maxWindow: "1h"
}
}
// Limit repeated account creation.
rateLimit sign_up {
limit: 5
window: "1h"
adjustable: { minLimit: 2 maxLimit: 10 minWindow: "10m" maxWindow: "1d" }
}
These customize the existing policies; they do not add duplicate checks. Omitted properties, including individual adjustment bounds, inherit the built-in defaults. The defaults must fit inside the adjustment bounds, and theme bounds must stay within the platform ranges above. These policies cannot be disabled, moved to another scope, or changed to by: actor. by: ip is optional and is the only accepted identity setting.
Do not bind sign_in or sign_up inside a profile, post, or endpoint: they already cover their account operations automatically. Other policies keep the usual explicit binding behavior.
A valid admin adjustment takes precedence over the theme defaults; theme defaults take precedence over the built-in defaults. If a theme update makes an existing admin adjustment invalid, the theme defaults apply. Restore defaults removes the admin adjustment and restores the current theme defaults, or the built-in defaults if the theme has no customization.
Add extra account policies
You can still attach additional named policies inside your profile block:
profile {
rateLimit signIn extra_sign_in_protection;
rateLimit create extra_registration_protection;
rateLimit resetPassword password_reset_attempts;
// Your existing schema and permissions go here.
}
Define each referenced policy. Every applicable policy must pass. Account actions support create, get, edit, delete, signIn, resetPassword, and verifyEmail. Only operations exposed by your site's account APIs can trigger them.
Protect endpoints and preserve safe retries
// Limits new checkout attempts while allowing an existing payment to reopen.
rateLimit checkout_attempts as "Checkout attempts" {
limit: 5
window: "10m"
by: actor
}
endpoint.post begin_checkout("/begin-checkout") {
must($actor.id, "Sign in to continue.", 401);
dedupe `${$actor.id}:${$request.body.attemptId}`;
rateLimit checkout_attempts;
// Your checkout actions go here.
return { ok: true };
}
Endpoint statements run in written order. A rateLimit statement checks and consumes its allowance when execution reaches it. There is no count option.
- Before
dedupe, it applies to every request reaching that line, including replays. - After
dedupe, it is skipped when dedupe returns a saved response, reports a conflict, or reports an in-progress request. - An earlier failed
must(...)orreturnprevents later checks from running. - Without dedupe, statements still run in written order.
You can use two policies: one before dedupe for repeated requests, another after it for new attempts. Reusing a named policy shares its allowance across endpoints. Each time its statement is reached consumes one unit, including if you deliberately reference it twice.
Put checks that must apply to every call, such as current authentication and authorization, before dedupe. Put payments, reservations, and writes after dedupe when retries must not repeat them. Statements before dedupe execute again even when a saved response exists.
An endpoint can have one top-level dedupe statement; it cannot appear inside a loop, conditional, atomic block, or helper function. A rateLimit statement can appear inside endpoint conditionals, loops, and atomic blocks: it consumes allowance each time execution reaches it. Skipped branches consume nothing. Rate-limit statements are not available in standalone helper functions or post hooks.
A later business failure or rollback does not refund rate-limit usage. A retry of a retryable failure reaches and consumes the applicable checks again. Dedupe controls replay; atomic groups database changes that must succeed together.
Cover groups of operations
// Limits repeated public API calls from the same connection.
rateLimit site_requests as "API requests" {
scope: site
limit: 300
window: "1m"
by: ip
}
| Scope | Coverage |
|---|---|
site | Public site API requests, custom endpoints, and upload API requests. Static pages, assets, and fb-admin requests are excluded. |
auth | Account operations, including sign-in and registration. |
postReads | Public post and list queries. |
postWrites | Public create, edit, and delete operations. |
endpoints | Custom theme endpoints. |
uploads | Upload requests, including profile photos and upload-session operations. |
Scoped policies apply automatically; do not also attach them to individual actions. A group policy consumes one unit per matching public call, while action bindings count the requested operations. Scoped policies run before endpoint statements, including on replay.
Respond to a limit
A blocked operation returns HTTP 429, a Retry-After header in seconds, and a body such as:
{
"ok": false,
"code": "rate_limited",
"error": "You’ve made several requests in a short time. Please wait a moment and try again.",
"httpStatus": 429,
"status": 429,
"retryAfter": 60
}
Show the message near the action and let the visitor try again after the wait. Do not continuously retry. A 429 is retryable with the same dedupe key and request details; it does not permanently save a failed checkout attempt. Keep that key when a response is uncertain too.
Rate limits are checked before the protected operation runs. Earlier, broader request limits can already have counted the request even if a more specific operation limit later blocks it.
Admin adjustments
Site owners and admins with admin-management access can open Abuse and protection to see descriptions, covered operations, effective limits, allowed counts, policy blocks, and the last blocked time.
Admins can change allowances and windows within the policy's adjustable bounds or restore defaults. Adjustments take effect on subsequent requests without resetting current usage. If a later theme version makes an adjustment invalid, its defaults apply. Multiple policies can block one request, so policy-block totals are not unique visitor or request counts.
Always-on platform safety limits remain active alongside theme policies: 1,000 public API requests per minute per client IP and 60 account operations per minute per client IP. Admin adjustments cannot disable them.
Reporting periods
The Abuse and protection page shows Today by default. Choose Last 7 days, Last 30 days, or All time to compare allowed and blocked policy attempts. The 7-day and 30-day views include today. Reporting days use UTC; daily history begins when daily reporting is enabled, while existing lifetime totals remain available under All time.
Reporting periods do not change your request allowances: a policy's window continues across midnight. One request can count toward several policies, so these totals describe policy checks, not unique visitors or requests.
Built-in declarations, including sign-in and sign-up protection, carry a gold ★ System badge in the dashboard. The badge identifies their origin and remains visible when a theme customizes their defaults or an admin adjusts their limits.
Declaration keys follow the FRL naming rules, including the reserved system namespace.