Skip to content

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):

sql
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:

csharp
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:

  1. Resolves the accountSlug/projectSlug to an account_id
  2. Loads the account to determine the subscription tier
  3. Increments the counter (always, including for unlimited accounts)
  4. 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:

json
"MeteringSettings": {
  "WarningThresholdMultiplier": 1.0,
  "BlockThresholdMultiplier": 1.1
}
  • Block threshold (default 1.1): requests are blocked when count > limit * 1.1. This provides a small buffer of 10% over the tier limit before hard enforcement kicks in. When blocked, the metering service returns MeterResult.OverLimit and the endpoint returns HTTP 429.
  • Warning threshold (default 1.0): at or above the limit, the billing status response includes the resource in the OverLimit array. 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 OverLimit array in the billing status response includes "api_requests" when count >= 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:

  • production environment: 1,000 requests per minute per project+environment pair
  • development and staging environments: 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:

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

Built with VitePress