thanos-io/thanos · warning

too many timeseries

Error message

too many timeseries

What it means

HTTP 413 returned by the OTLP handler when the number of time series in one request exceeds the tenant's per-request series limit enforced by requestLimiter.AllowSeries. This is a per-request cap, distinct from the total active-series limit, designed to bound memory needed to process a single write.

Solutions

  1. Reduce exporter batch size so each request carries fewer series.
  2. Increase the tenant's max series per request in the request limiter config.
  3. Split exports across multiple requests at the collector (e.g. batch/queued-retry processors with smaller batches).
  4. Aggregate/drop unneeded metrics in an OTTL processor before exporting.

Example fix

# before: one huge batch
batch:
  send_batch_size: 100000
# after
batch:
  send_batch_size: 5000
  send_batch_max_size: 5000
Defensive patterns

Strategy: validation

Validate before calling

if seriesCount > maxSeriesPerRequest { splitIntoBatches(metrics, maxSeriesPerRequest) }

Try / catch

if resp.StatusCode == 413 { reduceBatchSize(); retry() }

Prevention

When it happens

Trigger: A single OTLP export whose converted metric count exceeds the tenant's max series per request (request.series_limit / max_series).

Common situations: Exporter batching too many resource-metrics into one payload; many targets scraped into one export; per-request limit configured below the application's natural metric count.

Understand the failure class

Background: payload too large / request exceeds maximum size: why libraries cap bytes and how to fix oversize payloads — this error's family across 50 libraries.

Related errors


AI-assisted analysis of thanos-io/thanos@35b8b99117 (2026-09-07). Data as JSON: /api/errors/b9c5ecfb3cab7a3f. Report an issue: GitHub.

Appendix: source

Thrown at pkg/receive/handler_otlp.go:88

	if err != nil {
		level.Error(h.logger).Log("msg", "Error decoding remote write request", "err", err.Error())
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	metrics, _, err := h.convertToPrometheusFormat(ctx, req.Metrics())
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	totalSamples := 0
	for _, ts := range metrics {
		totalSamples += len(ts.Samples)
	}

	if !requestLimiter.AllowSeries(tenant, int64(len(metrics))) {
		http.Error(w, "too many timeseries", http.StatusRequestEntityTooLarge)
		return
	}

	if !requestLimiter.AllowSamples(tenant, int64(totalSamples)) {
		http.Error(w, "too many samples", http.StatusRequestEntityTooLarge)
		return
	}

	rep := uint64(0)
	// If the header is empty, we assume the request is not yet replicated.
	if replicaRaw := r.Header.Get(h.options.ReplicaHeader); replicaRaw != "" {
		if rep, err = strconv.ParseUint(replicaRaw, 10, 64); err != nil {
			http.Error(w, "could not parse replica header", http.StatusBadRequest)
			return
		}
	}

	//TODO: (nicolastakashi) Handle metadata in the future.

View on GitHub (pinned to 35b8b99117)