Sign in →
1 min read

Go SDK

Zero-dependency Go SDK with http.Handler middleware for any Go web framework.

Updated 2026-11-11Suggest edits
Get the Go SDK
The Go SDK lives in the public repo github.com/aforoai/SDKs under aforo-metering-sdks/go.
View on GitHub Download ZIPgit clone https://github.com/aforoai/SDKs.git

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

terminal
go get github.com/aforoai/SDKs/aforo-metering-sdks/go/v2

The Go modules are tagged v2.0.0.

Direct Client + Middleware

main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "time"

    metering "github.com/aforoai/SDKs/aforo-metering-sdks/go/v2"
)

func main() {
    client, err := metering.NewClient(metering.Options{
        APIKey:  os.Getenv("AFORO_API_KEY"),
        BaseURL: "https://api.aforo.ai", // must be https (localhost excepted)
    })
    if err != nil {
        log.Fatal(err) // blank key, or a BaseURL that is not https
    }

    // Manual track when you know an event happened:
    if err := client.Track(metering.TrackEvent{
        CustomerID: "cust_acme_001",
        MetricName: "api_calls",
        Quantity:   1,
    }); err != nil {
        log.Printf("aforo: %v", err) // ErrInvalidEvent or ErrClientClosed
    }

    // Or meter every request — emits after the response is written:
    mw, err := metering.NewMiddleware(client, metering.MiddlewareOptions{
        MetricName: "api_calls", // must exist in your Aforo catalog
        CustomerResolver: func(r *http.Request) string {
            return customerFromSession(r) // your authenticated Aforo customer id
        },
    })
    if err != nil {
        log.Fatal(err) // no CustomerResolver and no TrustCustomerHeader
    }
    mux := http.NewServeMux()
    srv := &http.Server{Addr: ":8080", Handler: mw(mux)}
    go srv.ListenAndServe()

    // In your shutdown path, after the server stopped accepting requests:
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    srv.Shutdown(ctx)
    if err := client.Close(ctx); err != nil {
        log.Printf("aforo: %v", err) // wraps ErrShutdownIncomplete if events were left
    }
}

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-Id header, 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 to unknown, anonymous or the agent id.
  • When the resolver returns nothing for one request, the request runs normally and its event is dropped as invalid and counted.
  • A manual track() call is unchanged: you pass customerId yourself.

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.

Drop reasonWhen
overflowThe queue was full (usually during a long outage); the oldest event was evicted.
rejectedThe ingestor refused the batch with a non-retryable 4xx, refused individual events inside an accepted batch, or answered 413 to a single event. Unknown metric names land here.
invalidThe event failed a client-side check before it was queued (blank customer or metric, quantity <= 0, a field over the ingestor limit), or a customer resolver returned nothing.
shutdownStill queued when the shutdown timeout ran out.
closedtrack() was called after shutdown. It does not throw (Go returns ErrClientClosed).
retry_exhaustedKept for API compatibility only — an outage requeues instead.

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.

See also
Aforo Documentation — Aforo Docs