Rate Limiting
SkyState enforces two independent types of rate limiting:
- Monthly API request quota — applied to the public config read endpoint, counted per account per calendar month. Limits vary by subscription tier.
- Per-endpoint rate limiting — enforced by ASP.NET Core's built-in rate limiter on the public config route group.
Monthly API Request Quota
The public config endpoint (GET /api/public/{accountSlug}/{projectSlug}/config/{envSlug}) is metered. Each successful request increments a monthly counter for the account that owns the project.
Limits by tier
| Tier | Monthly requests |
|---|---|
free | 200 |
hobby | 2,000 |
pro | 20,000 |
Grace zone
The server enforces a 10% grace zone above the hard limit:
- 100%–110% of limit: requests are served but a warning header is added.
- Above 110% of limit: requests are blocked with
429 Too Many Requests.
Response headers
When a monthly limit is configured, these headers are included on successful 200 responses:
| Header | Type | Description |
|---|---|---|
X-RateLimit-Limit | integer | Monthly request limit for the account's tier |
X-RateLimit-Remaining | integer | Requests remaining before the limit is reached |
X-RateLimit-Reset | integer | Unix timestamp of when the counter resets (first second of next UTC calendar month) |
X-RateLimit-Warning | string | Present when usage is between 100% and 110% of limit |
X-RateLimit-Limit and X-RateLimit-Remaining are omitted when the account is on an unlimited plan.
X-RateLimit-Reset is always included on successful responses (regardless of whether a limit is configured).
429 Response body
When the monthly quota is exceeded (above 110%), the server returns:
HTTP/1.1 429 Too Many Requests
Retry-After: <seconds_until_month_reset>
Content-Type: application/json{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Monthly API request limit exceeded. Upgrade your plan for higher limits.",
"limit": 200,
"current": 221,
"resetAt": "2025-02-01T00:00:00.0000000+00:00",
"upgradeUrl": "/upgrade"
}| Field | Type | Description |
|---|---|---|
code | string | Always "RATE_LIMIT_EXCEEDED" |
message | string | Human-readable explanation |
limit | integer | Effective monthly limit |
current | integer | Current request count this month |
resetAt | ISO 8601 datetime | When the counter resets |
upgradeUrl | string | Path to the upgrade page |
The Retry-After header contains the number of seconds until the counter resets. Clients should not retry before this time.
Monthly Reset
Counters reset at the first second of each UTC calendar month. For example:
- If today is January 20, the next reset is February 1 at
00:00:00 UTC. - The
X-RateLimit-Resetheader returns this as a Unix timestamp. - The
resetAtfield in the 429 response body returns this as an ISO 8601 string.
Unknown or Error States
If the project slug cannot be found or the metering service encounters an infrastructure error:
- No rate limit headers are added to the response.
- The request is not blocked (fail-open behavior).
- The config lookup proceeds normally and returns
404if the project does not exist.
Viewing Your Usage
Use the billing status endpoint to check current monthly usage without triggering a metered request:
GET /api/admin/billing/statusThe apiRequests field in the response contains the current count, limit, and reset date:
{
"apiRequests": {
"count": 142,
"limit": 200,
"resetDate": "2025-02-01T00:00:00.000Z"
}
}See Billing for the full billing status endpoint documentation.
Caching to Reduce Request Count
The public config endpoint sets Cache-Control: public, max-age=<N> on successful responses. Clients (including browsers and CDNs) that respect this header will serve cached responses without making a new request to the server, which does not count against your monthly quota.
Cache durations vary by tier and environment:
| Tier | development | staging | production |
|---|---|---|---|
free | 10s | — | 900s (15 min) |
hobby | 10s | 10s | 300s (5 min) |
pro | 10s | 10s | 60s (1 min) |
On free tier, production responses are cached for 15 minutes. This means a busy production app with many users can serve thousands of page loads from a single API fetch.