Metering
SkyState meters API requests to the public config endpoint. Each call to fetch remote config is counted against the account's monthly allowance.
What is Metered
Only requests to the public config endpoint are metered:
GET /{accountSlug}/{projectSlug}/config/{environmentSlug}Requests to the admin API (dashboard operations, project management, billing) are not metered.
Monthly Counters
Counts are stored in the api_request_counter table, keyed by (account_id, counter_year, counter_month):
create table if not exists api_request_counter
(
account_id uuid not null references "account" (account_id) on delete cascade,
counter_year integer not null,
counter_month integer not null,
request_count integer not null default 0,
primary key (account_id, counter_year, counter_month)
);Reset Timing
The counter resets on the first day of each calendar month at 00:00:00 UTC. A new row is automatically created for the new month on the first metered request. The reset date is computed as:
new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(1)This reset date is returned in the billing status response (ApiRequests.ResetDate) so the dashboard can show users when their counter will reset.
Counter Increment
MeteringService.MeterAsync is called on each public config request. The service:
- Resolves the
accountSlug/projectSlugto anaccount_id - Loads the account to determine the subscription tier
- Increments the counter (always, including for unlimited accounts)
- Compares the new count against the tier limit
The counter is always incremented, even if the request is subsequently blocked. This ensures counts are accurate and cannot be gamed by retrying at the boundary.
Enforcement Thresholds
Two thresholds are configured under MeteringSettings in appsettings.json:
"MeteringSettings": {
"WarningThresholdMultiplier": 1.0,
"BlockThresholdMultiplier": 1.1
}- Block threshold (default
1.1): requests are blocked whencount > limit * 1.1. This provides a small buffer of 10% over the tier limit before hard enforcement kicks in. When blocked, the metering service returnsMeterResult.OverLimitand the endpoint returns HTTP 429. - Warning threshold (default
1.0): at or above the limit, the billing status response includes the resource in theOverLimitarray. The dashboard uses this to display an over-limit banner.
Over-Limit Behavior
When a request is blocked by the metering service:
- The public config endpoint returns HTTP
429 Too Many Requests - The
OverLimitarray in the billing status response includes"api_requests"whencount >= limit - The dashboard displays an over-limit banner with an upgrade prompt
Per-Minute Rate Limiting
In addition to monthly metering, a fixed-window per-minute rate limiter is applied at the middleware layer:
productionenvironment: 1,000 requests per minute per project+environment pairdevelopmentandstagingenvironments: 60 requests per minute per project+environment pair
Rate-limited requests return HTTP 429 with a Retry-After: 60 header.
Billing Status Response
The GET /admin/billing/status endpoint returns current usage for display in the dashboard:
{
"tier": "hobby",
"projects": {
"count": 4,
"limit": 7
},
"apiRequests": {
"count": 1450,
"limit": 2000,
"resetDate": "2026-04-01T00:00:00Z"
},
"overLimit": [],
"currentPeriodEnd": "2026-03-31T00:00:00Z",
"lastStripeError": null,
"scheduledTier": null,
"scheduledChangeDate": null
}The overLimit array lists resource keys ("projects", "api_requests") that are at or over limit.