Skip to content

Rate Limiting

SkyState enforces two independent types of rate limiting:

  1. Monthly API request quota — applied to the public config read endpoint, counted per account per calendar month. Limits vary by subscription tier.
  2. 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

TierMonthly requests
free200
hobby2,000
pro20,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:

HeaderTypeDescription
X-RateLimit-LimitintegerMonthly request limit for the account's tier
X-RateLimit-RemainingintegerRequests remaining before the limit is reached
X-RateLimit-ResetintegerUnix timestamp of when the counter resets (first second of next UTC calendar month)
X-RateLimit-WarningstringPresent 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
HTTP/1.1 429 Too Many Requests
Retry-After: <seconds_until_month_reset>
Content-Type: application/json
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"
}
FieldTypeDescription
codestringAlways "RATE_LIMIT_EXCEEDED"
messagestringHuman-readable explanation
limitintegerEffective monthly limit
currentintegerCurrent request count this month
resetAtISO 8601 datetimeWhen the counter resets
upgradeUrlstringPath 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-Reset header returns this as a Unix timestamp.
  • The resetAt field 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 404 if 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/status

The apiRequests field in the response contains the current count, limit, and reset date:

json
{
  "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:

Tierdevelopmentstagingproduction
free10s900s (15 min)
hobby10s10s300s (5 min)
pro10s10s60s (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.

Built with VitePress