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 |
| Per packet; A and AAAA in the same UDP datagram count as one |
REST API call (any endpoint except |
| 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 endpointsOwn usage (any tenant)
GET /api/v1/usage?from=2026-04-24T00&to=2026-04-24T23Authorization: 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-24T23Authorization: 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.