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:
rpsLimiton thetenantstable 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:Collectorstruct;IncrementDNS,IncrementAPI,flush,Start,Stop.internal/storage (rqlite.go + storage_*.go):usage_recordsschema,UpsertUsagePeriod,ListUsage,UsageRecord.internal/dns/server.go:resolveFromDBreturnstenantID;handleDNSRequestcallsIncrementDNSonce per packet.internal/api/server.go:authMiddlewarecallsIncrementAPIafter successful auth.internal/api/handlers.go:getOwnUsage,getTenantUsage.