Limits and guardrails

Rate limits, the cardinality guardrail, and how masking works in Explore.

Limits and guardrails

Three limits affect a real app sending data to upzero: how much you can send, how many distinct label combinations a metric can have before reading it back gets expensive, and how upzero groups similar log and trace messages for you automatically.

Rate limiting

Each ingestion token is rate-limited on bytes per second and records per second. The threshold is not something you or anyone else sets; it is derived from your organization's plan tier and enforced per token, so one noisy service cannot crowd out another token's throughput. There is no --max-bytes-per-second flag on up0 tokens create and no way to raise a single token's limit independent of the plan.

An over-limit batch is rejected, not silently dropped or queued:

✗ Rate limit exceeded for this token. Check current usage: up0 tokens list --output json

Your OTel exporter surfaces this the same way it surfaces any other rejected batch, typically as an export failure logged by the SDK. If you hit it during normal traffic rather than a test burst, the fix is fewer or smaller batches at the source, or a plan with a higher tier, not a flag on the token.

The cardinality guardrail

This one is different from the rate limit above: it applies when you or the console read your own metrics back, not when you send them. A metric whose label set has too many distinct combinations (a user id or request id baked into a label, for example) makes a query over it cost unbounded memory even when the final answer would have been small. upzero aborts that query server-side rather than let it run:

400 INVALID_QUERY_PARAMS

The fix is at the emitter, not on the query. Retrying or narrowing the query does not help if the metric itself carries a high-cardinality label. Move anything with unbounded distinct values, a user id, a request id, a raw URL path, an order number, onto a log or trace attribute instead, where high cardinality is expected and fine. Keep metric labels to values with a small, known set: environment, region, HTTP method, status code class, and similar.

This guardrail is not yet enforced at ingest time; today it only stops a read that would already be expensive. A high-cardinality metric is accepted when sent and only becomes a problem the first time someone queries it.

Masking and event patterns

Explore's Event Patterns mode groups similar log and trace records together so you see the shape of your data instead of scrolling every row. To do that, it replaces the variable parts of a log's body or a trace's span_name with a token before grouping: a UUID becomes <uuid>, a hex value or pointer becomes <hex>, an IPv4 or IPv6 address becomes <ip>, and a plain number becomes <num>. So

Processed order 4821 for a1b2c3d4-e5f6-7890-abcd-ef0123456789 in 250ms

and every record with the same shape but different order id, uuid and duration group into one pattern row with an exact count.

You do not need to do anything for this to work; it happens automatically when you switch Explore into Event Patterns mode, and the count for each pattern is exact over the whole time range, not an estimate from a sample page. The full, unmasked message is still stored and still visible on the individual record, both in the results table and via up0 tail. Masking only affects how patterns are grouped and labelled, never what is stored or what a single record shows.

On this page