Go SDK
Zero-dependency Go SDK with http.Handler middleware for any Go web framework.
aforo-metering-sdks/go.Install & Quickstart#
Add Aforo metering to your Go service. A zero-dependency client with http.Handler and Chi middleware.
Zero dependencies beyond the standard library; Go 1.21+. From v2.0.0 the module path ends in /v2: github.com/aforoai/SDKs/aforo-metering-sdks/go/v2 (the protocol modules too, e.g. go-grpc/v2, which needs Go 1.26).
The Go modules are tagged v2.0.0.
Direct Client + Middleware
NewMiddleware refuses to build without CustomerResolver unless you set TrustCustomerHeader: true, which bills the customer named in X-Customer-Id — use it only behind a proxy that sets that header and removes the client's value. An empty resolver answer drops the event as invalid (counted in DroppedCount()); the request is never blocked. For Chi: r.Use(mw).
Who Is Billed#
Every integration that meters an incoming request — HTTP middleware, the gRPC / GraphQL / WebSocket / MQTT clients, the MCP SDKs and the MCP proxy — needs to know which customer to bill, and from 2.0.0 you tell it. Pass a customer resolver that reads identity your service already verified (a session, a JWT subject, an API-key lookup, an mTLS identity).
- Reading a value the caller of your API controls — the
X-Customer-Idheader, gRPC metadata, MCP_meta.customer_id, the MQTT username — needs an explicit opt-in:trustCustomerHeader(Node, Java),trust_customer_header(Python),TrustCustomerHeader(Go). Turn it on only when a proxy you control sets the value and strips any client copy. - With neither a resolver nor the opt-in, construction fails (Node throws, Python raises
ValueError, Go returns an error, Spring fails to start). There is no fallback tounknown,anonymousor the agent id. - When the resolver returns nothing for one request, the request runs normally and its event is dropped as
invalidand counted. - A manual
track()call is unchanged: you passcustomerIdyourself.
Delivery & Dropped Events#
All 2.0.0 SDKs send the same way. track() only queues (bounded, 10,000 events by default; on overflow the oldest is evicted). One background sender per client sends one request at a time: every 5 s, or as soon as 500 events are waiting (the batch size, capped at 1,000). Retries on network errors, timeouts, 408, 429 and 5xx use full-jitter backoff and honour Retry-After (seconds or an HTTP date, capped at 60 s).
An outage does not lose usage. After the last retry — or on 401 / 403, which usually means a wrong or revoked key and is logged as an error — the batch goes back to the front of the queue with the same idempotency keys and a breaker pauses sending (5 s, doubling to 5 min). New events keep queueing; the first successful request resumes sending, and the ingestor deduplicates the re-sent keys. Usage is lost only if the queue overflows first, so raise the queue size if you expect long outages. A 413 splits the batch in half.
Shutdown is bounded: shutdown() / close() keeps sending for at most the shutdown timeout (5 s by default) and reports what is still queued. The libraries install no signal handlers — call shutdown from your own SIGTERM path. Python also flushes at atexit, Node on beforeExit, Java in Spring's stop phase.
Every dropped event is counted, logged, and passed to the opt-in onDrop(events, reason) hook (on_drop in Python, OnDrop in Go). Events keep their idempotency keys, so re-tracking them after recovery does not bill twice.