Get started
API analytics
Measure your own HTTP API: requests, status codes, latency percentiles and errors for every endpoint, plus the clients and customers calling it. Add the middleware to your server, and the numbers show up on the site's API tab.
What it measures
| Metric | Details |
|---|---|
| Requests | Totals per endpoint, status code, client and consumer, compared with the previous period |
| Status mix | 2xx, 3xx, 4xx and 5xx over time, with 4xx and 5xx rates |
| Latency | p50, p95 and p99 per endpoint, estimated from log-scale buckets to within about 4% |
| Errors | 4xx and 5xx grouped per endpoint and status, with rate, last seen and recent samples |
| Clients | The calling library or browser from User-Agent, such as curl, python-requests, okhttp, chrome or googlebot |
| Consumers | An optional id you attach to each request, such as an account id |
Endpoints are grouped by method and route template, such as GET /users/:id, so every user id lands in the same row. See Route templates.
Create an API key
A key belongs to one site and can only send that site's API data.
For a standalone API, go to /app, click Add site, choose API and enter the API's hostname, such as api.example.com. API sites can create keys right away, without a tracking script or verification. To add API analytics to a website you already track, use that site instead; websites must be verified before they can create keys. API-only sites covers the differences.
- Open the site's settings and go to the API tab. New API sites open there.
- Click Create key. The label is optional.
- Copy the key right away: it is shown once. Keys look like
tt_followed by 48 hex characters.
A site can have up to 10 active keys. A revoked key stops working within a minute. To create and revoke keys over HTTP, see Keys.
Install the middleware
npm install totallyticsgo get github.com/bitgate/totallytics-goSet the key as TOTALLYTICS_API_KEY: a secret on Cloudflare Workers (npx wrangler secret put TOTALLYTICS_API_KEY), or an environment variable on Node.js, Bun, Deno (with --allow-env) and Go. Without a key, the middleware does nothing. Register it before your routes so it sees every request.
import { Hono } from "hono";
import { totallytics } from "totallytics/hono";
const app = new Hono();
app.use("*", totallytics());
app.get("/users/:id", (c) => c.json({ id: c.req.param("id") }));
export default app;import { withTotallytics } from "totallytics/workers";
export default withTotallytics({
async fetch(request, env, ctx) {
return new Response("hello");
},
} satisfies ExportedHandler<Env>);import express from "express";
import { totallytics, totallyticsErrors } from "totallytics/express";
const app = express();
app.use(totallytics());
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id });
});
app.use(totallyticsErrors());
app.listen(3000);package main
import (
"net/http"
"github.com/bitgate/totallytics-go"
)
func main() {
tt := totallytics.New(totallytics.Options{})
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.PathValue("id")))
})
http.ListenAndServe(":8080", tt.Middleware(mux))
}| Framework | Route reported |
|---|---|
| Hono | The matched pattern, such as /users/:id, including sub-apps and basePath. When no route answers, the wildcard that ran, such as /* |
| Cloudflare Workers | The raw path, which Totallytics turns into a template. Other handlers, such as scheduled and queue, pass through untouched |
| Express | req.baseUrl + req.route.path, or the raw path when no route matched |
| Go | The matched ServeMux pattern, such as /users/:id for GET /users/{id}, or the raw path for subtree patterns like /static/ and unmatched requests |
On Express, totallyticsErrors() is optional: placed before your own error handler, it attaches err.message to error samples and passes the error on. Requests aborted before headers were sent are recorded as 499.
The package has no dependencies and runs on Node.js 18+, Bun, Deno and Workers. Requests are aggregated in memory and sent in small background batches. The middleware never throws into your code and never delays a response.
- Node.js, Bun and Deno send every 10 seconds, plus a best-effort flush on
beforeExitandSIGTERM. On serverless runtimes that freeze between requests, callawait middleware.flush()yourself. - Workers send once per isolate, 5 seconds after the first request, kept alive with
waitUntil. Full batches go out right away. - Failed sends are retried with the identical batch, up to 3 attempts in total, and Totallytics counts it once.
Durations run from the middleware until your handler returns a response; streamed bodies are not included. Deployed Workers only advance their clock on I/O, so purely CPU-bound handlers report about 0 ms.
Options
| Option | Default | Description |
|---|---|---|
apiKey |
TOTALLYTICS_API_KEY |
String, or a function: (c) on Hono, (env) on Workers, () on Express |
consumer |
none | Returns an opaque id for the caller; see Consumers |
route |
detected | Returns your own route template |
ignore |
none | Returns true to skip a request, such as a health check |
errorSamples |
true |
Send individual 4xx and 5xx requests, up to 50 5xx and 20 4xx per flush |
flushIntervalMs |
10000 |
Send interval on Node.js, Bun and Deno |
flushDelayMs |
5000 |
Delay before the Workers send, up to 20000 |
maxBatchRows |
1000 |
Metric rows per request, up to 5000 |
endpoint |
https://totallytics.com/api/ingest |
Ingest URL |
debug |
false |
Log diagnostics with console.warn |
consumer, route and ignore receive (c) on Hono, (request, env) on Workers and (req, res) on Express.
app.use(
"*",
totallytics({
consumer: (c) => c.get("account")?.id,
ignore: (c) => c.req.path === "/health",
}),
);Go
The Go module needs Go 1.22 or newer and has no dependencies. On Go 1.22, tt.Middleware has to wrap the *http.ServeMux itself; from Go 1.23 it can sit further out, as long as the middleware in between passes the request on unchanged. chi, gin and echo have their own modules, github.com/bitgate/totallytics-go/chi, /gin and /echo, which report the router's route pattern: register totallyticschi.Middleware(tt), totallyticsgin.Middleware(tt) or totallyticsecho.Middleware(tt) before your routes. Panics are recorded as 500 and re-panicked, so your recovery middleware still handles them.
totallytics.Options takes APIKey, Endpoint, Consumer, Route, Ignore, MaxBatchRows and FlushInterval, which work like the options above, plus a Logger for diagnostics. Batches go out every 10 seconds. Call tt.Shutdown(ctx) after server.Shutdown so the last batch is sent, and tt.Flush(ctx) before a serverless invocation returns. The Go README has a quickstart for each router.
Other frameworks
On other Node.js frameworks, record each finished request with the core client:
import { Totallytics } from "totallytics";
const tt = new Totallytics();
tt.record({ method, path, route, status, durationMs, userAgent, consumer });From other languages, send batches to POST /api/ingest yourself.
Route templates
Totallytics groups requests by route template, not by raw path. With raw paths, /users/1 and /users/2 become separate endpoints: thousands of rows with a few requests each, and no useful percentiles.
Hono and Express report the route pattern that matched. The Workers adapter sends the raw path, and Totallytics templates it: numeric, UUID, long hex and random-token segments become :id, dates become :date and email addresses :email. Query strings are dropped, paths deeper than 12 segments end in *, and templates are cut at 256 characters.
| Sent | Stored as |
|---|---|
/users/123/orders |
/users/:id/orders |
/files/3f2a9c1e-8b4d-4c1a-9e2f-7a6b5c4d3e2f |
/files/:id |
/reports/2026-09-28 |
/reports/:date |
/search?q=shoes |
/search |
/wp-login.php, answered with 404 |
/* |
Scanners and typos hit paths that don't exist all day. A 404 whose route still has no parameter or wildcard after templating is stored as /*, so that noise stays in one row instead of flooding your endpoint list. A 404 on a real template, such as /users/:id, keeps its route, and error samples keep the raw path.
Pass route to send your own template, for example to split up a catch-all handler.
Consumers
Pass consumer to see who calls your API and who runs into errors: requests, 5xx rate and p95 latency per consumer. Return an opaque id, such as an internal account id, never an email address or API key. Ids are cut at 128 characters. Requests without one show up as Unidentified. In Go, set Options.Consumer or call totallytics.SetConsumer(r, id) from your auth middleware.
What is never sent
The middleware never sends request or response bodies, headers other than User-Agent, query strings or IP addresses. Error samples do contain the raw path and the error message. If either can hold personal data, set errorSamples: false in JavaScript; the Go SDK always sends them.
The API tab
Open a site and choose API. Only the site owner sees this tab, also on public sites. API sites open on it.
- Summary: requests, 5xx rate, p50 and p95 latency, each compared with the previous period of the same length.
- Requests chart, stacked by 2xx, 3xx, 4xx and 5xx, and a Latency chart with p50 and p95. Ranges up to 4 days use hourly points, longer ranges daily ones.
- Endpoints, Status codes, Clients and Consumers tables. Endpoints show requests, 5xx rate, p50, p95 and p99.
Click any name in these tables to filter the whole tab to it: summary, charts, the other tables and errors. Filters from different tables combine; clicking another row in the same table swaps the filter, and clicking the active row removes it. Active filters show as chips that you remove one by one or with Clear all. They are stored in the URL, so a filtered view survives a reload.
The Errors panel groups every 4xx and 5xx response by status and endpoint, with its count, its rate (the share of that endpoint's requests that returned this status) and when it was last seen. Expand a row for the latest samples: time, raw path, client, consumer, duration and error message. Filter to this error narrows the whole tab to that status and endpoint.
To get an email when an API starts failing, slows down or goes quiet, set up API alerts.
Retention and limits
| Data | Kept for |
|---|---|
| Per-minute rows | 35 days |
| Hourly rollups | 25 months |
| Error samples | 14 days |
Stats only count requests timestamped after the site was registered. Range edges more than 34 days back come from the hourly rollups and round to whole hours.
| Limit | Value |
|---|---|
| Active keys per site | 10 |
| Ingest request body | 4 MiB |
| Rows per ingest request | 5,000 metric rows and 200 error samples |
| Row timestamps | Up to 7 days old or 5 minutes ahead |
| Route template | 256 characters, 12 segments |
| Consumer id | 128 characters |
| Filters per stats request | 12, each value up to 256 characters |