Usage Metering

Usage Metering

Nexus GSLB tracks per-tenant DNS query and API call counts and stores them in hourly buckets, providing the data foundation for consumption-based billing and capacity planning.

What is metered

Event Counter Granularity
DNS query received for a DB-managed domain dns_queries Per packet; A and AAAA in the same UDP datagram count as one
REST API call (any endpoint except /api/v1/health) api_calls Per HTTP request

Both counters are attributed to the tenant identified by the Bearer token on the request. Static load-balancer queries (not backed by the DB) are not metered, since they have no associated tenant.

Storage

Usage is persisted in the usage_records SQLite table:

CREATE TABLE usage_records (
    tenant_id   TEXT    NOT NULL,
    period      TEXT    NOT NULL,  -- "2006-01-02T15" UTC hourly bucket
    dns_queries INTEGER NOT NULL DEFAULT 0,
    api_calls   INTEGER NOT NULL DEFAULT 0,
    PRIMARY KEY (tenant_id, period)
);

Writes are additive upserts (ON CONFLICT DO UPDATE SET dns_queries = dns_queries + excluded.dns_queries), so concurrent flushes from multiple nodes accumulate correctly without losing counts. If the daemon crashes, at most one flush interval (default 60 s) of counts are lost.

Architecture

DNS queries      → resolveFromDB → Collector.IncrementDNS(tenantID)
API calls        → authMiddleware → Collector.IncrementAPI(tenantID)
                                        ↓ every 60 s
                               UpsertUsagePeriod → usage_records

The usage.Collector accumulates increments in an in-memory map protected by a mutex. Every 60 seconds (configurable at construction time), the map is atomically swapped to zero and flushed to the database. The flush also runs on Stop() so no counts are lost during graceful shutdown.

API endpoints

Own usage (any tenant)

GET /api/v1/usage?from=2026-04-24T00&to=2026-04-24T23
Authorization: Bearer <token>

Returns the caller's usage for the specified period range. Both from and to are hourly period strings ("YYYY-MM-DDTHH" in UTC). If omitted, defaults to the current UTC day.

Response (200):

[
  { "tenantId": "abc123", "period": "2026-04-24T14", "dnsQueries": 12450, "apiCalls": 34 },
  { "tenantId": "abc123", "period": "2026-04-24T15", "dnsQueries": 9801, "apiCalls": 22 }
]

An empty array is returned when no records exist for the period.

Any tenant's usage (admin only)

GET /api/v1/tenants/{id}/usage?from=2026-04-24T00&to=2026-04-24T23
Authorization: Bearer <admin-key>

Returns the named tenant's usage. Returns 403 Forbidden if called with a non-admin token.

Querying multi-day ranges

Period strings sort lexicographically, so range queries work across day and month boundaries:

GET /api/v1/usage?from=2026-04-01T00&to=2026-04-30T23

This returns up to 720 hourly rows (30 days × 24 hours). Group or sum client-side for daily or monthly totals.

Limitations

  • No hard enforcement: rpsLimit on the tenants table is stored but not yet plumbed into the per-tenant DNS rate limiter. Usage metering records consumption; enforcement is a future feature.
  • No real-time streaming: counts are flushed every 60 s; very short-lived traffic spikes may appear aggregated with surrounding traffic.
  • Single-node flush: in a multi-node cluster, each node flushes its own counts independently. The DB accumulates counts from all nodes correctly, but the in-flight window (≤ 60 s) is per-node.
  • Static LB queries unmetered: DNS queries resolved entirely by the static load balancer (no DB lookup) have no tenant attribution and are not counted.

Code references

  • internal/usage/collector.go: Collector struct; IncrementDNS, IncrementAPI, flush, Start, Stop.
  • internal/storage (rqlite.go + storage_*.go): usage_records schema, UpsertUsagePeriod, ListUsage, UsageRecord.
  • internal/dns/server.go: resolveFromDB returns tenantID; handleDNSRequest calls IncrementDNS once per packet.
  • internal/api/server.go: authMiddleware calls IncrementAPI after successful auth.
  • internal/api/handlers.go: getOwnUsage, getTenantUsage.

Was this article helpful?
© 2026