Repository: VictoriaMetrics/VictoriaMetrics
Pull request #3015 (2022-08-24 03:42:36)

[vmagent] make opentsdb insert url support multitenant
Let opentsdb http insert request support tenantId:projectId specified in the url like:

`http://vmagent:4242/insert/1000/opentsdb/api/put`

Without the change, the request always writes data into tenant 0 instead of 1000.






Patch:
diff --git a/README.md b/README.md
index 60336bb68..89df4b248 100644
--- a/README.md
+++ b/README.md
@@ -1213,6 +1213,8 @@ By default VictoriaMetrics is tuned for an optimal resource usage under typical
 - `-search.maxConcurrentRequests` limits the number of concurrent requests VictoriaMetrics can process. Bigger number of concurrent requests usually means bigger memory usage. For example, if a single query needs 100 MiB of additional memory during its execution, then 100 concurrent queries may need `100 * 100 MiB = 10 GiB` of additional memory. So it is better to limit the number of concurrent queries, while suspending additional incoming queries if the concurrency limit is reached. VictoriaMetrics provides `-search.maxQueueDuration` command-line flag for limiting the max wait time for suspended queries.
 - `-search.maxSamplesPerSeries` limits the number of raw samples the query can process per each time series. VictoriaMetrics sequentially processes raw samples per each found time series during the query. It unpacks raw samples on the selected time range per each time series into memory and then applies the given [rollup function](https://docs.victoriametrics.com/MetricsQL.html#rollup-functions). The `-search.maxSamplesPerSeries` command-line flag allows limiting memory usage in the case when the query is executed on a time range, which contains hundreds of millions of raw samples per each located time series.
 - `-search.maxSamplesPerQuery` limits the number of raw samples a single query can process. This allows limiting CPU usage for heavy queries.
+- `-search.maxPointsPerTimeseries` limits the number of calculated points, which can be returned per each matching time series from [range query](https://docs.victoriametrics.com/keyConcepts.html#range-query).
+- `-search.maxPointsSubqueryPerTimeseries` limits the number of calculated points, which can be generated per each matching time series during [subquery](https://docs.victoriametrics.com/MetricsQL.html#subqueries) evaluation.
 - `-search.maxSeries` limits the number of time series, which may be returned from [/api/v1/series](https://prometheus.io/docs/prometheus/latest/querying/api/#finding-series-by-label-matchers). This endpoint is used mostly by Grafana for auto-completion of metric names, label names and label values. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxSeries` to quite low value in order limit CPU and memory usage.
 - `-search.maxTagKeys` limits the number of items, which may be returned from [/api/v1/labels](https://prometheus.io/docs/prometheus/latest/querying/api/#getting-label-names). This endpoint is used mostly by Grafana for auto-completion of label names. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxTagKeys` to quite low value in order to limit CPU and memory usage.
 - `-search.maxTagValues` limits the number of items, which may be returned from [/api/v1/label/.../values](https://prometheus.io/docs/prometheus/latest/querying/api/#querying-label-values). This endpoint is used mostly by Grafana for auto-completion of label values. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxTagValues` to quite low value in order to limit CPU and memory usage.
@@ -1547,8 +1549,27 @@ Both limits can be set simultaneously. If any of these limits is reached, then i
 The exceeded limits can be [monitored](#monitoring) with the following metrics:
 
 * `vm_hourly_series_limit_rows_dropped_total` - the number of metrics dropped due to exceeded hourly limit on the number of unique time series.
+
+* `vm_hourly_series_limit_max_series` - the hourly series limit set via `-storage.maxHourlySeries` command-line flag.
+
+* `vm_hourly_series_limit_current_series` - the current number of unique series during the last hour.
+  The following query can be useful for alerting when the number of unique series during the last hour exceeds 90% of the `-storage.maxHourlySeries`:
+
+  ```metricsql
+  vm_hourly_series_limit_current_series / vm_hourly_series_limit_max_series > 0.9
+  ```
+
 * `vm_daily_series_limit_rows_dropped_total` - the number of metrics dropped due to exceeded daily limit on the number of unique time series.
 
+* `vm_daily_series_limit_max_series` - the daily series limit set via `-storage.maxDailySeries` command-line flag.
+
+* `vm_daily_series_limit_current_series` - the current number of unique series during the last day.
+  The following query can be useful for alerting when the number of unique series during the last day exceeds 90% of the `-storage.maxDailySeries`:
+
+  ```metricsql
+  vm_daily_series_limit_current_series / vm_daily_series_limit_max_series > 0.9
+  ```
+
 These limits are approximate, so VictoriaMetrics can underflow/overflow the limit by a small percentage (usually less than 1%).
 
 See also more advanced [cardinality limiter in vmagent](https://docs.victoriametrics.com/vmagent.html#cardinality-limiter).
@@ -1803,6 +1824,7 @@ curl http://0.0.0.0:8428/debug/pprof/profile > cpu.pprof
 The command for collecting CPU profile waits for 30 seconds before returning.
 
 The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
 
 ## Integrations
 
@@ -2153,7 +2175,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
   -search.maxLookback duration
      Synonym to -search.lookback-delta from Prometheus. The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. See also '-search.maxStalenessInterval' flag, which has the same meaining due to historical reasons
   -search.maxPointsPerTimeseries int
-     The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
+     The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as VMUI or Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
+  -search.maxPointsSubqueryPerTimeseries int
+     The maximum number of points per series, which can be generated by subquery. See https://valyala.medium.com/prometheus-subqueries-in-victoriametrics-9b1492b720b3 (default 100000)
   -search.maxQueryDuration duration
      The maximum duration for query execution (default 30s)
   -search.maxQueryLen size
@@ -2225,9 +2249,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      Overrides max size for storage/tsid cache. See https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#cache-tuning
      Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
   -storage.maxDailySeries int
-     The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See also -storage.maxHourlySeries
+     The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See https://docs.victoriametrics.com/#cardinality-limiter . See also -storage.maxHourlySeries
   -storage.maxHourlySeries int
-     The maximum number of unique series can be added to the storage during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See also -storage.maxDailySeries
+     The maximum number of unique series can be added to the storage during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See https://docs.victoriametrics.com/#cardinality-limiter . See also -storage.maxDailySeries
   -storage.minFreeDiskSpaceBytes size
      The minimum free disk space at -storageDataPath after which the storage stops accepting new data
      Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 10000000)
diff --git a/app/vmagent/README.md b/app/vmagent/README.md
index dd746a87d..b7123d957 100644
--- a/app/vmagent/README.md
+++ b/app/vmagent/README.md
@@ -893,6 +893,7 @@ curl http://0.0.0.0:8429/debug/pprof/profile > cpu.pprof
 The command for collecting CPU profile waits for 30 seconds before returning.
 
 The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
 
 ## Advanced usage
 
diff --git a/app/vmagent/opentsdbhttp/request_handler.go b/app/vmagent/opentsdbhttp/request_handler.go
index 7d2d409eb..e49f0274f 100644
--- a/app/vmagent/opentsdbhttp/request_handler.go
+++ b/app/vmagent/opentsdbhttp/request_handler.go
@@ -1,10 +1,15 @@
 package opentsdbhttp
 
 import (
+	"errors"
+	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
 	parserCommon "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
 	parser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentsdbhttp"
@@ -20,18 +25,28 @@ var (
 // InsertHandler processes HTTP OpenTSDB put requests.
 // See http://opentsdb.net/docs/build/html/api_http/put.html
 func InsertHandler(req *http.Request) error {
+	path := strings.Replace(req.URL.Path, "//", "/", -1)
+	p, err := httpserver.ParsePath(path)
+	if err != nil {
+		// Cannot parse multitenant path. Skip it - probably it will be parsed later.
+		return err
+	}
+	if p.Prefix != "insert" {
+		return errors.New(fmt.Sprintf(`unsupported multitenant prefix: %q; expected "insert"`, p.Prefix))
+	}
+	at, err := auth.NewToken(p.AuthToken)
 	extraLabels, err := parserCommon.GetExtraLabels(req)
 	if err != nil {
 		return err
 	}
 	return writeconcurrencylimiter.Do(func() error {
 		return parser.ParseStream(req, func(rows []parser.Row) error {
-			return insertRows(rows, extraLabels)
+			return insertRows(at, rows, extraLabels)
 		})
 	})
 }
 
-func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error {
+func insertRows(at *auth.Token, rows []parser.Row, extraLabels []prompbmarshal.Label) error {
 	ctx := common.GetPushCtx()
 	defer common.PutPushCtx(ctx)
 
@@ -65,7 +80,7 @@ func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error {
 	ctx.WriteRequest.Timeseries = tssDst
 	ctx.Labels = labels
 	ctx.Samples = samples
-	remotewrite.Push(nil, &ctx.WriteRequest)
+	remotewrite.Push(at, &ctx.WriteRequest)
 	rowsInserted.Add(len(rows))
 	rowsPerInsert.Update(float64(len(rows)))
 	return nil
diff --git a/app/vmalert/README.md b/app/vmalert/README.md
index bd28a4cf5..c9cbb39bb 100644
--- a/app/vmalert/README.md
+++ b/app/vmalert/README.md
@@ -630,6 +630,35 @@ Use the official [Grafana dashboard](https://grafana.com/grafana/dashboards/1495
 If you have suggestions for improvements or have found a bug - please open an issue on github or add
 a review to the dashboard.
 
+## Profiling
+
+`vmalert` provides handlers for collecting the following [Go profiles](https://blog.golang.org/profiling-go-programs):
+
+* Memory profile. It can be collected with the following command (replace `0.0.0.0` with hostname if needed):
+
+<div class="with-copy" markdown="1">
+
+```console
+curl http://0.0.0.0:8880/debug/pprof/heap > mem.pprof
+```
+
+</div>
+
+* CPU profile. It can be collected with the following command (replace `0.0.0.0` with hostname if needed):
+
+<div class="with-copy" markdown="1">
+
+```console
+curl http://0.0.0.0:8880/debug/pprof/profile > cpu.pprof
+```
+
+</div>
+
+The command for collecting CPU profile waits for 30 seconds before returning.
+
+The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
+
 ## Configuration
 
 ### Flags
diff --git a/app/vmauth/README.md b/app/vmauth/README.md
index 94683509b..379b43dd1 100644
--- a/app/vmauth/README.md
+++ b/app/vmauth/README.md
@@ -217,6 +217,7 @@ curl http://0.0.0.0:8427/debug/pprof/profile > cpu.pprof
 The command for collecting CPU profile waits for 30 seconds before returning.
 
 The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
 
 ## Advanced usage
 
diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go
index e85a094c4..d62bd0448 100644
--- a/app/vmselect/prometheus/prometheus.go
+++ b/app/vmselect/prometheus/prometheus.go
@@ -44,11 +44,14 @@ var (
 	maxStepForPointsAdjustment = flag.Duration("search.maxStepForPointsAdjustment", time.Minute, "The maximum step when /api/v1/query_range handler adjusts "+
 		"points with timestamps closer than -search.latencyOffset to the current time. The adjustment is needed because such points may contain incomplete data")
 
-	maxUniqueTimeseries = flag.Int("search.maxUniqueTimeseries", 300e3, "The maximum number of unique time series, which can be selected during /api/v1/query and /api/v1/query_range queries. This option allows limiting memory usage")
-	maxFederateSeries   = flag.Int("search.maxFederateSeries", 1e6, "The maximum number of time series, which can be returned from /federate. This option allows limiting memory usage")
-	maxExportSeries     = flag.Int("search.maxExportSeries", 10e6, "The maximum number of time series, which can be returned from /api/v1/export* APIs. This option allows limiting memory usage")
-	maxTSDBStatusSeries = flag.Int("search.maxTSDBStatusSeries", 10e6, "The maximum number of time series, which can be processed during the call to /api/v1/status/tsdb. This option allows limiting memory usage")
-	maxSeriesLimit      = flag.Int("search.maxSeries", 30e3, "The maximum number of time series, which can be returned from /api/v1/series. This option allows limiting memory usage")
+	maxUniqueTimeseries    = flag.Int("search.maxUniqueTimeseries", 300e3, "The maximum number of unique time series, which can be selected during /api/v1/query and /api/v1/query_range queries. This option allows limiting memory usage")
+	maxFederateSeries      = flag.Int("search.maxFederateSeries", 1e6, "The maximum number of time series, which can be returned from /federate. This option allows limiting memory usage")
+	maxExportSeries        = flag.Int("search.maxExportSeries", 10e6, "The maximum number of time series, which can be returned from /api/v1/export* APIs. This option allows limiting memory usage")
+	maxTSDBStatusSeries    = flag.Int("search.maxTSDBStatusSeries", 10e6, "The maximum number of time series, which can be processed during the call to /api/v1/status/tsdb. This option allows limiting memory usage")
+	maxSeriesLimit         = flag.Int("search.maxSeries", 30e3, "The maximum number of time series, which can be returned from /api/v1/series. This option allows limiting memory usage")
+	maxPointsPerTimeseries = flag.Int("search.maxPointsPerTimeseries", 30e3, "The maximum points per a single timeseries returned from /api/v1/query_range. "+
+		"This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points "+
+		"returned to graphing UI such as VMUI or Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph")
 )
 
 // Default step used if not set.
@@ -733,6 +736,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
 		Start:               start,
 		End:                 start,
 		Step:                step,
+		MaxPointsPerSeries:  *maxPointsPerTimeseries,
 		MaxSeries:           *maxUniqueTimeseries,
 		QuotedRemoteAddr:    httpserver.GetQuotedRemoteAddr(r),
 		Deadline:            deadline,
@@ -818,7 +822,7 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
 	if start > end {
 		end = start + defaultStep
 	}
-	if err := promql.ValidateMaxPointsPerTimeseries(start, end, step); err != nil {
+	if err := promql.ValidateMaxPointsPerSeries(start, end, step, *maxPointsPerTimeseries); err != nil {
 		return err
 	}
 	if mayCache {
@@ -829,6 +833,7 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
 		Start:               start,
 		End:                 end,
 		Step:                step,
+		MaxPointsPerSeries:  *maxPointsPerTimeseries,
 		MaxSeries:           *maxUniqueTimeseries,
 		QuotedRemoteAddr:    httpserver.GetQuotedRemoteAddr(r),
 		Deadline:            deadline,
diff --git a/app/vmselect/promql/eval.go b/app/vmselect/promql/eval.go
index 7f3a5808a..63f45cd55 100644
--- a/app/vmselect/promql/eval.go
+++ b/app/vmselect/promql/eval.go
@@ -24,10 +24,9 @@ import (
 )
 
 var (
-	disableCache           = flag.Bool("search.disableCache", false, "Whether to disable response caching. This may be useful during data backfilling")
-	maxPointsPerTimeseries = flag.Int("search.maxPointsPerTimeseries", 30e3, "The maximum points per a single timeseries returned from /api/v1/query_range. "+
-		"This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points "+
-		"returned to graphing UI such as Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph")
+	disableCache                   = flag.Bool("search.disableCache", false, "Whether to disable response caching. This may be useful during data backfilling")
+	maxPointsSubqueryPerTimeseries = flag.Int("search.maxPointsSubqueryPerTimeseries", 100e3, "The maximum number of points per series, which can be generated by subquery. "+
+		"See https://valyala.medium.com/prometheus-subqueries-in-victoriametrics-9b1492b720b3")
 	noStaleMarkers = flag.Bool("search.noStaleMarkers", false, "Set this flag to true if the database doesn't contain Prometheus stale markers, so there is no need in spending additional CPU time on its handling. Staleness markers may exist only in data obtained from Prometheus scrape targets")
 )
 
@@ -36,15 +35,15 @@ var (
 // big time ranges.
 const minTimeseriesPointsForTimeRounding = 50
 
-// ValidateMaxPointsPerTimeseries checks the maximum number of points that
-// may be returned per each time series.
-//
-// The number mustn't exceed -search.maxPointsPerTimeseries.
-func ValidateMaxPointsPerTimeseries(start, end, step int64) error {
+// ValidateMaxPointsPerSeries validates that the number of points for the given start, end and step do not exceed maxPoints.
+func ValidateMaxPointsPerSeries(start, end, step int64, maxPoints int) error {
+	if step == 0 {
+		return fmt.Errorf("step can't be equal to zero")
+	}
 	points := (end-start)/step + 1
-	if uint64(points) > uint64(*maxPointsPerTimeseries) {
-		return fmt.Errorf(`too many points for the given step=%d, start=%d and end=%d: %d; cannot exceed -search.maxPointsPerTimeseries=%d`,
-			step, start, end, uint64(points), *maxPointsPerTimeseries)
+	if points > int64(maxPoints) {
+		return fmt.Errorf("too many points for the given start=%d, end=%d and step=%d: %d; the maximum number of points is %d (see -search.maxPoints* command-line flags)",
+			start, end, step, points, maxPoints)
 	}
 	return nil
 }
@@ -99,6 +98,9 @@ type EvalConfig struct {
 	// Zero means 'no limit'
 	MaxSeries int
 
+	// MaxPointsPerSeries is the limit on the number of points, which can be generated per each returned time series.
+	MaxPointsPerSeries int
+
 	// QuotedRemoteAddr contains quoted remote address.
 	QuotedRemoteAddr string
 
@@ -127,6 +129,7 @@ func copyEvalConfig(src *EvalConfig) *EvalConfig {
 	ec.End = src.End
 	ec.Step = src.Step
 	ec.MaxSeries = src.MaxSeries
+	ec.MaxPointsPerSeries = src.MaxPointsPerSeries
 	ec.Deadline = src.Deadline
 	ec.MayCache = src.MayCache
 	ec.LookbackDelta = src.LookbackDelta
@@ -174,10 +177,10 @@ func (ec *EvalConfig) getSharedTimestamps() []int64 {
 }
 
 func (ec *EvalConfig) timestampsInit() {
-	ec.timestamps = getTimestamps(ec.Start, ec.End, ec.Step)
+	ec.timestamps = getTimestamps(ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries)
 }
 
-func getTimestamps(start, end, step int64) []int64 {
+func getTimestamps(start, end, step int64, maxPointsPerSeries int) []int64 {
 	// Sanity checks.
 	if step <= 0 {
 		logger.Panicf("BUG: Step must be bigger than 0; got %d", step)
@@ -185,7 +188,7 @@ func getTimestamps(start, end, step int64) []int64 {
 	if start > end {
 		logger.Panicf("BUG: Start cannot exceed End; got %d vs %d", start, end)
 	}
-	if err := ValidateMaxPointsPerTimeseries(start, end, step); err != nil {
+	if err := ValidateMaxPointsPerSeries(start, end, step, maxPointsPerSeries); err != nil {
 		logger.Panicf("BUG: %s; this must be validated before the call to getTimestamps", err)
 	}
 
@@ -806,7 +809,8 @@ func evalRollupFuncWithSubquery(qt *querytracer.Tracer, ec *EvalConfig, funcName
 	ecSQ.Start -= window + maxSilenceInterval + step
 	ecSQ.End += step
 	ecSQ.Step = step
-	if err := ValidateMaxPointsPerTimeseries(ecSQ.Start, ecSQ.End, ecSQ.Step); err != nil {
+	ecSQ.MaxPointsPerSeries = *maxPointsSubqueryPerTimeseries
+	if err := ValidateMaxPointsPerSeries(ecSQ.Start, ecSQ.End, ecSQ.Step, ecSQ.MaxPointsPerSeries); err != nil {
 		return nil, err
 	}
 	// unconditionally align start and end args to step for subquery as Prometheus does.
@@ -818,8 +822,8 @@ func evalRollupFuncWithSubquery(qt *querytracer.Tracer, ec *EvalConfig, funcName
 	if len(tssSQ) == 0 {
 		return nil, nil
 	}
-	sharedTimestamps := getTimestamps(ec.Start, ec.End, ec.Step)
-	preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, window, ec.LookbackDelta, sharedTimestamps)
+	sharedTimestamps := getTimestamps(ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries)
+	preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps)
 	if err != nil {
 		return nil, err
 	}
@@ -956,8 +960,8 @@ func evalRollupFuncWithMetricExpr(qt *querytracer.Tracer, ec *EvalConfig, funcNa
 
 	// Obtain rollup configs before fetching data from db,
 	// so type errors can be caught earlier.
-	sharedTimestamps := getTimestamps(start, ec.End, ec.Step)
-	preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, start, ec.End, ec.Step, window, ec.LookbackDelta, sharedTimestamps)
+	sharedTimestamps := getTimestamps(start, ec.End, ec.Step, ec.MaxPointsPerSeries)
+	preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps)
 	if err != nil {
 		return nil, err
 	}
diff --git a/app/vmselect/promql/rollup.go b/app/vmselect/promql/rollup.go
index 953d0f973..57f0cd62c 100644
--- a/app/vmselect/promql/rollup.go
+++ b/app/vmselect/promql/rollup.go
@@ -248,7 +248,7 @@ func getRollupAggrFuncNames(expr metricsql.Expr) ([]string, error) {
 	return aggrFuncNames, nil
 }
 
-func getRollupConfigs(name string, rf rollupFunc, expr metricsql.Expr, start, end, step, window int64, lookbackDelta int64, sharedTimestamps []int64) (
+func getRollupConfigs(name string, rf rollupFunc, expr metricsql.Expr, start, end, step int64, maxPointsPerSeries int, window, lookbackDelta int64, sharedTimestamps []int64) (
 	func(values []float64, timestamps []int64), []*rollupConfig, error) {
 	preFunc := func(values []float64, timestamps []int64) {}
 	if rollupFuncsRemoveCounterResets[name] {
@@ -258,12 +258,15 @@ func getRollupConfigs(name string, rf rollupFunc, expr metricsql.Expr, start, en
 	}
 	newRollupConfig := func(rf rollupFunc, tagValue string) *rollupConfig {
 		return &rollupConfig{
-			TagValue:        tagValue,
-			Func:            rf,
-			Start:           start,
-			End:             end,
-			Step:            step,
-			Window:          window,
+			TagValue: tagValue,
+			Func:     rf,
+			Start:    start,
+			End:      end,
+			Step:     step,
+			Window:   window,
+
+			MaxPointsPerSeries: maxPointsPerSeries,
+
 			MayAdjustWindow: rollupFuncsCanAdjustWindow[name],
 			LookbackDelta:   lookbackDelta,
 			Timestamps:      sharedTimestamps,
@@ -400,6 +403,9 @@ type rollupConfig struct {
 	Step   int64
 	Window int64
 
+	// The maximum number of points, which can be generated per each series.
+	MaxPointsPerSeries int
+
 	// Whether window may be adjusted to 2 x interval between data points.
 	// This is needed for functions which have dt in the denominator
 	// such as rate, deriv, etc.
@@ -416,6 +422,10 @@ type rollupConfig struct {
 	isDefaultRollup bool
 }
 
+func (rc *rollupConfig) getTimestamps() []int64 {
+	return getTimestamps(rc.Start, rc.End, rc.Step, rc.MaxPointsPerSeries)
+}
+
 func (rc *rollupConfig) String() string {
 	start := storage.TimestampToHumanReadableFormat(rc.Start)
 	end := storage.TimestampToHumanReadableFormat(rc.End)
@@ -513,7 +523,7 @@ func (rc *rollupConfig) doInternal(dstValues []float64, tsm *timeseriesMap, valu
 	if rc.Window < 0 {
 		logger.Panicf("BUG: Window must be non-negative; got %d", rc.Window)
 	}
-	if err := ValidateMaxPointsPerTimeseries(rc.Start, rc.End, rc.Step); err != nil {
+	if err := ValidateMaxPointsPerSeries(rc.Start, rc.End, rc.Step, rc.MaxPointsPerSeries); err != nil {
 		logger.Panicf("BUG: %s; this must be validated before the call to rollupConfig.Do", err)
 	}
 
diff --git a/app/vmstorage/main.go b/app/vmstorage/main.go
index 5747dd01d..ef207ab0c 100644
--- a/app/vmstorage/main.go
+++ b/app/vmstorage/main.go
@@ -49,9 +49,11 @@ var (
 		"When set, then /api/v1/query_range would return '503 Service Unavailable' error for queries with 'from' value outside -retentionPeriod. "+
 		"This may be useful when multiple data sources with distinct retentions are hidden behind query-tee")
 	maxHourlySeries = flag.Int("storage.maxHourlySeries", 0, "The maximum number of unique series can be added to the storage during the last hour. "+
-		"Excess series are logged and dropped. This can be useful for limiting series cardinality. See also -storage.maxDailySeries")
+		"Excess series are logged and dropped. This can be useful for limiting series cardinality. See https://docs.victoriametrics.com/#cardinality-limiter . "+
+		"See also -storage.maxDailySeries")
 	maxDailySeries = flag.Int("storage.maxDailySeries", 0, "The maximum number of unique series can be added to the storage during the last 24 hours. "+
-		"Excess series are logged and dropped. This can be useful for limiting series churn rate. See also -storage.maxHourlySeries")
+		"Excess series are logged and dropped. This can be useful for limiting series churn rate. See https://docs.victoriametrics.com/#cardinality-limiter . "+
+		"See also -storage.maxHourlySeries")
 
 	minFreeDiskSpaceBytes = flagutil.NewBytes("storage.minFreeDiskSpaceBytes", 10e6, "The minimum free disk space at -storageDataPath after which the storage stops accepting new data")
 
@@ -626,12 +628,29 @@ func registerStorageMetrics(strg *storage.Storage) {
 		return float64(m().SlowMetricNameLoads)
 	})
 
-	metrics.NewGauge(`vm_hourly_series_limit_rows_dropped_total`, func() float64 {
-		return float64(m().HourlySeriesLimitRowsDropped)
-	})
-	metrics.NewGauge(`vm_daily_series_limit_rows_dropped_total`, func() float64 {
-		return float64(m().DailySeriesLimitRowsDropped)
-	})
+	if *maxHourlySeries > 0 {
+		metrics.NewGauge(`vm_hourly_series_limit_current_series`, func() float64 {
+			return float64(m().HourlySeriesLimitCurrentSeries)
+		})
+		metrics.NewGauge(`vm_hourly_series_limit_max_series`, func() float64 {
+			return float64(m().HourlySeriesLimitMaxSeries)
+		})
+		metrics.NewGauge(`vm_hourly_series_limit_rows_dropped_total`, func() float64 {
+			return float64(m().HourlySeriesLimitRowsDropped)
+		})
+	}
+
+	if *maxDailySeries > 0 {
+		metrics.NewGauge(`vm_daily_series_limit_current_series`, func() float64 {
+			return float64(m().DailySeriesLimitCurrentSeries)
+		})
+		metrics.NewGauge(`vm_daily_series_limit_max_series`, func() float64 {
+			return float64(m().DailySeriesLimitMaxSeries)
+		})
+		metrics.NewGauge(`vm_daily_series_limit_rows_dropped_total`, func() float64 {
+			return float64(m().DailySeriesLimitRowsDropped)
+		})
+	}
 
 	metrics.NewGauge(`vm_timestamps_blocks_merged_total`, func() float64 {
 		return float64(m().TimestampsBlocksMerged)
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index b4fa60c18..72acbb9fe 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -22,6 +22,8 @@ The following tip changes can be tested by building VictoriaMetrics components f
 * SECURITY: [vmalert](https://docs.victoriametrics.com/vmalert.html): do not expose `-remoteWrite.url`, `-remoteRead.url` and `-datasource.url` command-line flag values in logs and at `http://vmalert:8880/flags` page by default, since they may contain sensitive data such as auth keys. This aligns `vmalert` behaviour with [vmagent](https://docs.victoriametrics.com/vmagent.html), which doesn't expose `-remoteWrite.url` command-line flag value in logs and at `http://vmagent:8429/flags` page by default. Specify `-remoteWrite.showURL`, `-remoteRead.showURL` and `-datasource.showURL` command-line flags for showing values for the corresponding `-*.url` flags in logs. Thanks to @mble for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2965).
 
 * FEATURE: return shorter error messages to Grafana and to other clients requesting [/api/v1/query](https://docs.victoriametrics.com/keyConcepts.html#instant-query) and [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query) endpoints. This should simplify reading these errors by humans. The long error message with full context is still written to logs.
+* FEATURE: add the ability to fine-tune the number of points, which can be generated per each matching time series during [subquery](https://docs.victoriametrics.com/MetricsQL.html#subqueries) evaluation. This can be done with the `-search.maxPointsSubqueryPerTimeseries` command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2922).
+* FEATURE: [monitoring](https://docs.victoriametrics.com/#monitoring): expose `vm_hourly_series_limit_max_series`, `vm_hourly_series_limit_current_series`, `vm_daily_series_limit_max_series` and `vm_daily_series_limit_current_series` metrics when `-search.maxHourlySeries` or `-search.maxDailySeries` limits are set. This allows alerting when the number of unique series reaches the configured limits. See [these docs](https://docs.victoriametrics.com/#cardinality-limiter) for details.
 * FEATURE: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): reduce the amounts of logging at `vmstorage` when `vmselect` connects/disconnects to `vmstorage`.
 * FEATURE: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): improve performance for heavy queries on systems with many CPU cores.
 * FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add ability to use {% raw %}`{{label_name}}`{% endraw %} placeholders in the `replacement` option of relabeling rules. This simplifies constructing label values from multiple existing label values. See [these docs](https://docs.victoriametrics.com/vmagent.html#relabeling-enhancements) for details.
@@ -33,7 +35,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
 * FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add a legend in the top right corner for shortcut keys. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2813).
 * FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add `toTime()` template function in the same way as Prometheus 2.38 [does](https://github.com/prometheus/prometheus/pull/10993). See [these docs](https://prometheus.io/docs/prometheus/latest/configuration/template_reference/#numbers).
 * FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add `$alertID` and `$groupID` template variables. These variables may be used for templating annotations or `-external.alert.source` command-line flag. See the full list of supported variables [here](https://docs.victoriametrics.com/vmalert.html#templating).
-* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add `$activeAt` template variable. See more details [here](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2999). See the full list of supported variables [here](https://docs.victoriametrics.com/vmalert.html#templating). Thanks to @laixintao.
+* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add `$activeAt` template variable. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2999). See the full list of supported variables [here](https://docs.victoriametrics.com/vmalert.html#templating). Thanks to @laixintao for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3000).
 * FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): point alert source to [vmalert's UI](https://docs.victoriametrics.com/vmalert.html#web) at `/vmalert/alert?...` instead of JSON handler at `/vmalert/api/v1/alert?...`. This improves user experience. The old behavior can be achieved by setting {% raw %}`-external.alert.source=vmalert/api/v1/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}`{% endraw %} command-line flag.
 
 * BUGFIX: prevent from excess CPU usage when the storage enters [read-only mode](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#readonly-mode).
diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md
index 4b5d05f15..e2af269c6 100644
--- a/docs/Cluster-VictoriaMetrics.md
+++ b/docs/Cluster-VictoriaMetrics.md
@@ -193,6 +193,17 @@ or [an alternative dashboard for VictoriaMetrics cluster](https://grafana.com/gr
 
 It is recommended setting up alerts in [vmalert](https://docs.victoriametrics.com/vmalert.html) or in Prometheus from [this config](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/cluster/deployment/docker/alerts.yml).
 
+## Cardinality limiter
+
+`vmstorage` nodes can be configured with limits on the number of unique time series across all the tenants with the following command-line flags:
+
+- `-storage.maxHourlySeries` is the limit on the number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series) during the last hour.
+- `-storage.maxDailySeries` is the limit on the number of unique time series during the day. This limit can be used for limiting daily [time series churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate).
+
+Note that these limits are set and applied individually per each `vmstorage` node in the cluster. So, if the cluster has `N` `vmstorage` nodes, then the cluster-level limits will be `N` times bigger than the per-`vmstorage` limits.
+
+See more details about cardinality limiter in [these docs](https://docs.victoriametrics.com/#cardinality-limiter).
+
 ## Troubleshooting
 
 See [trobuleshooting docs](https://docs.victoriametrics.com/Troubleshooting.html).
@@ -302,45 +313,47 @@ with new configs.
 
 There are the following cluster update / upgrade approaches exist:
 
-* `No downtime` strategy. Gracefully restart every node in the cluster one-by-one with the updated config / upgraded binary.
+### No downtime strategy
+
+Gracefully restart every node in the cluster one-by-one with the updated config / upgraded binary.
 
-  It is recommended restarting the nodes in the following order:
+It is recommended restarting the nodes in the following order:
 
-  1. Restart `vmstorage` nodes.
-  2. Restart `vminsert` nodes.
-  3. Restart `vmselect` nodes.
+1. Restart `vmstorage` nodes.
+2. Restart `vminsert` nodes.
+3. Restart `vmselect` nodes.
 
-  This strategy allows upgrading the cluster without downtime if the following conditions are met:
+This strategy allows upgrading the cluster without downtime if the following conditions are met:
 
-  - The cluster has at least a pair of nodes of each type - `vminsert`, `vmselect` and `vmstorage`,
-    so it can continue accept new data and serve incoming requests when a single node is temporary unavailable
-    during its restart. See [cluster availability docs](#cluster-availability) for details.
-  - The cluster has enough compute resources (CPU, RAM, network bandwidth, disk IO) for processing
-    the current workload when a single node of any type (`vminsert`, `vmselect` or `vmstorage`)
-    is temporarily unavailable during its restart.
-  - The updated config / upgraded binary is compatible with the remaining components in the cluster.
-    See the [CHANGELOG](https://docs.victoriametrics.com/CHANGELOG.html) for compatibility notes between different releases.
+- The cluster has at least a pair of nodes of each type - `vminsert`, `vmselect` and `vmstorage`,
+  so it can continue accept new data and serve incoming requests when a single node is temporary unavailable
+  during its restart. See [cluster availability docs](#cluster-availability) for details.
+- The cluster has enough compute resources (CPU, RAM, network bandwidth, disk IO) for processing
+  the current workload when a single node of any type (`vminsert`, `vmselect` or `vmstorage`)
+  is temporarily unavailable during its restart.
+- The updated config / upgraded binary is compatible with the remaining components in the cluster.
+  See the [CHANGELOG](https://docs.victoriametrics.com/CHANGELOG.html) for compatibility notes between different releases.
 
   If at least a single condition isn't met, then the rolling restart may result in cluster unavailability
   during the config update / version upgrade. In this case the following strategy is recommended.
 
-* `Minimum downtime` strategy:
+### Minimum downtime strategy
 
-  1. Gracefully stop all the `vminsert` and `vmselect` nodes in parallel.
-  2. Gracefully restart all the `vmstorage` nodes in parallel.
-  3. Start all the `vminsert` and `vmselect` nodes in parallel.
+1. Gracefully stop all the `vminsert` and `vmselect` nodes in parallel.
+2. Gracefully restart all the `vmstorage` nodes in parallel.
+3. Start all the `vminsert` and `vmselect` nodes in parallel.
 
-  The cluster is unavailable for data ingestion and querying when performing the steps above.
-  The downtime is minimized by restarting cluster nodes in parallel at every step above.
-  The `minimum downtime` strategy has the following benefits comparing to `no downtime` startegy:
+The cluster is unavailable for data ingestion and querying when performing the steps above.
+The downtime is minimized by restarting cluster nodes in parallel at every step above.
+The `minimum downtime` strategy has the following benefits comparing to `no downtime` startegy:
 
-  - It allows performing config update / version upgrade with minimum disruption
-    when the previous config / version is incompatible with the new config / version.
-  - It allows perorming config update / version upgrade with minimum disruption
-    when the cluster has no enough compute resources (CPU, RAM, disk IO, network bandwidth)
-    for rolling upgrade.
-  - It allows minimizing the duration of config update / version ugprade for clusters with big number of nodes
-    of for clusters with big `vmstorage` nodes, which may take long time for graceful restart.
+- It allows performing config update / version upgrade with minimum disruption
+  when the previous config / version is incompatible with the new config / version.
+- It allows perorming config update / version upgrade with minimum disruption
+  when the cluster has no enough compute resources (CPU, RAM, disk IO, network bandwidth)
+  for rolling upgrade.
+- It allows minimizing the duration of config update / version ugprade for clusters with big number of nodes
+  of for clusters with big `vmstorage` nodes, which may take long time for graceful restart.
 
 ## Cluster availability
 
@@ -429,11 +442,13 @@ By default cluster components of VictoriaMetrics are tuned for an optimal resour
 - `-search.maxConcurrentRequests` at `vmselect` limits the number of concurrent requests a single `vmselect` node can process. Bigger number of concurrent requests usually means bigger memory usage at both `vmselect` and `vmstorage`. For example, if a single query needs 100 MiB of additional memory during its execution, then 100 concurrent queries may need `100 * 100 MiB = 10 GiB` of additional memory. So it is better to limit the number of concurrent queries, while suspending additional incoming queries if the concurrency limit is reached. `vmselect` provides `-search.maxQueueDuration` command-line flag for limiting the max wait time for suspended queries.
 - `-search.maxSamplesPerSeries` at `vmselect` limits the number of raw samples the query can process per each time series. `vmselect` sequentially processes raw samples per each found time series during the query. It unpacks raw samples on the selected time range per each time series into memory and then applies the given [rollup function](https://docs.victoriametrics.com/MetricsQL.html#rollup-functions). The `-search.maxSamplesPerSeries` command-line flag allows limiting memory usage at `vmselect` in the case when the query is executed on a time range, which contains hundreds of millions of raw samples per each located time series.
 - `-search.maxSamplesPerQuery` at `vmselect` limits the number of raw samples a single query can process. This allows limiting CPU usage at `vmselect` for heavy queries.
+- `-search.maxPointsPerTimeseries` limits the number of calculated points, which can be returned per each matching time series from [range query](https://docs.victoriametrics.com/keyConcepts.html#range-query).
+- `-search.maxPointsSubqueryPerTimeseries` limits the number of calculated points, which can be generated per each matching time series during [subquery](https://docs.victoriametrics.com/MetricsQL.html#subqueries) evaluation.
 - `-search.maxSeries` at `vmselect` limits the number of time series, which may be returned from [/api/v1/series](https://prometheus.io/docs/prometheus/latest/querying/api/#finding-series-by-label-matchers). This endpoint is used mostly by Grafana for auto-completion of metric names, label names and label values. Queries to this endpoint may take big amounts of CPU time and memory at `vmstorage` and `vmselect` when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxSeries` to quite low value in order limit CPU and memory usage.
 - `-search.maxTagKeys` at `vmstorage` limits the number of items, which may be returned from [/api/v1/labels](https://prometheus.io/docs/prometheus/latest/querying/api/#getting-label-names). This endpoint is used mostly by Grafana for auto-completion of label names. Queries to this endpoint may take big amounts of CPU time and memory at `vmstorage` and `vmselect` when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxTagKeys` to quite low value in order to limit CPU and memory usage.
 - `-search.maxTagValues` at `vmstorage` limits the number of items, which may be returned from [/api/v1/label/.../values](https://prometheus.io/docs/prometheus/latest/querying/api/#querying-label-values). This endpoint is used mostly by Grafana for auto-completion of label values. Queries to this endpoint may take big amounts of CPU time and memory at `vmstorage` and `vmselect` when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxTagValues` to quite low value in order to limit CPU and memory usage.
-- `-storage.maxDailySeries` at `vmstorage` can be used for limiting the number of time series seen per day.
-- `-storage.maxHourlySeries` at `vmstorage` can be used for limiting the number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series).
+- `-storage.maxDailySeries` at `vmstorage` can be used for limiting the number of time series seen per day aka [time series churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). See [cardinality limiter docs](#cardinality-limiter).
+- `-storage.maxHourlySeries` at `vmstorage` can be used for limiting the number of [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series). See [cardinality limiter docs](#cardinality-limiter).
 
 See also [capacity planning docs](#capacity-planning) and [cardinality limiter in vmagent](https://docs.victoriametrics.com/vmagent.html#cardinality-limiter).
 
@@ -552,6 +567,8 @@ Example command for collecting memory profile from `vminsert` (replace `0.0.0.0`
 curl http://0.0.0.0:8480/debug/pprof/heap > mem.pprof
 ```
 
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
+
 </div>
 
 ## vmalert
@@ -901,7 +918,9 @@ Below is the output for `/path/to/vmselect -help`:
   -search.maxLookback duration
      Synonym to -search.lookback-delta from Prometheus. The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. See also '-search.maxStalenessInterval' flag, which has the same meaining due to historical reasons
   -search.maxPointsPerTimeseries int
-     The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
+     The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as VMUI or Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
+  -search.maxPointsSubqueryPerTimeseries int
+     The maximum number of points per series, which can be generated by subquery. See https://valyala.medium.com/prometheus-subqueries-in-victoriametrics-9b1492b720b3 (default 100000)
   -search.maxQueryDuration duration
      The maximum duration for query execution (default 30s)
   -search.maxQueryLen size
@@ -1099,9 +1118,9 @@ Below is the output for `/path/to/vmstorage -help`:
      Overrides max size for storage/tsid cache. See https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#cache-tuning
      Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
   -storage.maxDailySeries int
-     The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See also -storage.maxHourlySeries
+     The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See https://docs.victoriametrics.com/#cardinality-limiter . See also -storage.maxHourlySeries
   -storage.maxHourlySeries int
-     The maximum number of unique series can be added to the storage during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See also -storage.maxDailySeries
+     The maximum number of unique series can be added to the storage during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See https://docs.victoriametrics.com/#cardinality-limiter . See also -storage.maxDailySeries
   -storage.minFreeDiskSpaceBytes size
      The minimum free disk space at -storageDataPath after which the storage stops accepting new data
      Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 10000000)
diff --git a/docs/README.md b/docs/README.md
index 60336bb68..89df4b248 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1213,6 +1213,8 @@ By default VictoriaMetrics is tuned for an optimal resource usage under typical
 - `-search.maxConcurrentRequests` limits the number of concurrent requests VictoriaMetrics can process. Bigger number of concurrent requests usually means bigger memory usage. For example, if a single query needs 100 MiB of additional memory during its execution, then 100 concurrent queries may need `100 * 100 MiB = 10 GiB` of additional memory. So it is better to limit the number of concurrent queries, while suspending additional incoming queries if the concurrency limit is reached. VictoriaMetrics provides `-search.maxQueueDuration` command-line flag for limiting the max wait time for suspended queries.
 - `-search.maxSamplesPerSeries` limits the number of raw samples the query can process per each time series. VictoriaMetrics sequentially processes raw samples per each found time series during the query. It unpacks raw samples on the selected time range per each time series into memory and then applies the given [rollup function](https://docs.victoriametrics.com/MetricsQL.html#rollup-functions). The `-search.maxSamplesPerSeries` command-line flag allows limiting memory usage in the case when the query is executed on a time range, which contains hundreds of millions of raw samples per each located time series.
 - `-search.maxSamplesPerQuery` limits the number of raw samples a single query can process. This allows limiting CPU usage for heavy queries.
+- `-search.maxPointsPerTimeseries` limits the number of calculated points, which can be returned per each matching time series from [range query](https://docs.victoriametrics.com/keyConcepts.html#range-query).
+- `-search.maxPointsSubqueryPerTimeseries` limits the number of calculated points, which can be generated per each matching time series during [subquery](https://docs.victoriametrics.com/MetricsQL.html#subqueries) evaluation.
 - `-search.maxSeries` limits the number of time series, which may be returned from [/api/v1/series](https://prometheus.io/docs/prometheus/latest/querying/api/#finding-series-by-label-matchers). This endpoint is used mostly by Grafana for auto-completion of metric names, label names and label values. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxSeries` to quite low value in order limit CPU and memory usage.
 - `-search.maxTagKeys` limits the number of items, which may be returned from [/api/v1/labels](https://prometheus.io/docs/prometheus/latest/querying/api/#getting-label-names). This endpoint is used mostly by Grafana for auto-completion of label names. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxTagKeys` to quite low value in order to limit CPU and memory usage.
 - `-search.maxTagValues` limits the number of items, which may be returned from [/api/v1/label/.../values](https://prometheus.io/docs/prometheus/latest/querying/api/#querying-label-values). This endpoint is used mostly by Grafana for auto-completion of label values. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxTagValues` to quite low value in order to limit CPU and memory usage.
@@ -1547,8 +1549,27 @@ Both limits can be set simultaneously. If any of these limits is reached, then i
 The exceeded limits can be [monitored](#monitoring) with the following metrics:
 
 * `vm_hourly_series_limit_rows_dropped_total` - the number of metrics dropped due to exceeded hourly limit on the number of unique time series.
+
+* `vm_hourly_series_limit_max_series` - the hourly series limit set via `-storage.maxHourlySeries` command-line flag.
+
+* `vm_hourly_series_limit_current_series` - the current number of unique series during the last hour.
+  The following query can be useful for alerting when the number of unique series during the last hour exceeds 90% of the `-storage.maxHourlySeries`:
+
+  ```metricsql
+  vm_hourly_series_limit_current_series / vm_hourly_series_limit_max_series > 0.9
+  ```
+
 * `vm_daily_series_limit_rows_dropped_total` - the number of metrics dropped due to exceeded daily limit on the number of unique time series.
 
+* `vm_daily_series_limit_max_series` - the daily series limit set via `-storage.maxDailySeries` command-line flag.
+
+* `vm_daily_series_limit_current_series` - the current number of unique series during the last day.
+  The following query can be useful for alerting when the number of unique series during the last day exceeds 90% of the `-storage.maxDailySeries`:
+
+  ```metricsql
+  vm_daily_series_limit_current_series / vm_daily_series_limit_max_series > 0.9
+  ```
+
 These limits are approximate, so VictoriaMetrics can underflow/overflow the limit by a small percentage (usually less than 1%).
 
 See also more advanced [cardinality limiter in vmagent](https://docs.victoriametrics.com/vmagent.html#cardinality-limiter).
@@ -1803,6 +1824,7 @@ curl http://0.0.0.0:8428/debug/pprof/profile > cpu.pprof
 The command for collecting CPU profile waits for 30 seconds before returning.
 
 The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
 
 ## Integrations
 
@@ -2153,7 +2175,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
   -search.maxLookback duration
      Synonym to -search.lookback-delta from Prometheus. The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. See also '-search.maxStalenessInterval' flag, which has the same meaining due to historical reasons
   -search.maxPointsPerTimeseries int
-     The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
+     The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as VMUI or Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
+  -search.maxPointsSubqueryPerTimeseries int
+     The maximum number of points per series, which can be generated by subquery. See https://valyala.medium.com/prometheus-subqueries-in-victoriametrics-9b1492b720b3 (default 100000)
   -search.maxQueryDuration duration
      The maximum duration for query execution (default 30s)
   -search.maxQueryLen size
@@ -2225,9 +2249,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      Overrides max size for storage/tsid cache. See https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#cache-tuning
      Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
   -storage.maxDailySeries int
-     The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See also -storage.maxHourlySeries
+     The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See https://docs.victoriametrics.com/#cardinality-limiter . See also -storage.maxHourlySeries
   -storage.maxHourlySeries int
-     The maximum number of unique series can be added to the storage during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See also -storage.maxDailySeries
+     The maximum number of unique series can be added to the storage during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See https://docs.victoriametrics.com/#cardinality-limiter . See also -storage.maxDailySeries
   -storage.minFreeDiskSpaceBytes size
      The minimum free disk space at -storageDataPath after which the storage stops accepting new data
      Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 10000000)
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index 23299ccec..ce8e1137b 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -1217,6 +1217,8 @@ By default VictoriaMetrics is tuned for an optimal resource usage under typical
 - `-search.maxConcurrentRequests` limits the number of concurrent requests VictoriaMetrics can process. Bigger number of concurrent requests usually means bigger memory usage. For example, if a single query needs 100 MiB of additional memory during its execution, then 100 concurrent queries may need `100 * 100 MiB = 10 GiB` of additional memory. So it is better to limit the number of concurrent queries, while suspending additional incoming queries if the concurrency limit is reached. VictoriaMetrics provides `-search.maxQueueDuration` command-line flag for limiting the max wait time for suspended queries.
 - `-search.maxSamplesPerSeries` limits the number of raw samples the query can process per each time series. VictoriaMetrics sequentially processes raw samples per each found time series during the query. It unpacks raw samples on the selected time range per each time series into memory and then applies the given [rollup function](https://docs.victoriametrics.com/MetricsQL.html#rollup-functions). The `-search.maxSamplesPerSeries` command-line flag allows limiting memory usage in the case when the query is executed on a time range, which contains hundreds of millions of raw samples per each located time series.
 - `-search.maxSamplesPerQuery` limits the number of raw samples a single query can process. This allows limiting CPU usage for heavy queries.
+- `-search.maxPointsPerTimeseries` limits the number of calculated points, which can be returned per each matching time series from [range query](https://docs.victoriametrics.com/keyConcepts.html#range-query).
+- `-search.maxPointsSubqueryPerTimeseries` limits the number of calculated points, which can be generated per each matching time series during [subquery](https://docs.victoriametrics.com/MetricsQL.html#subqueries) evaluation.
 - `-search.maxSeries` limits the number of time series, which may be returned from [/api/v1/series](https://prometheus.io/docs/prometheus/latest/querying/api/#finding-series-by-label-matchers). This endpoint is used mostly by Grafana for auto-completion of metric names, label names and label values. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxSeries` to quite low value in order limit CPU and memory usage.
 - `-search.maxTagKeys` limits the number of items, which may be returned from [/api/v1/labels](https://prometheus.io/docs/prometheus/latest/querying/api/#getting-label-names). This endpoint is used mostly by Grafana for auto-completion of label names. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxTagKeys` to quite low value in order to limit CPU and memory usage.
 - `-search.maxTagValues` limits the number of items, which may be returned from [/api/v1/label/.../values](https://prometheus.io/docs/prometheus/latest/querying/api/#querying-label-values). This endpoint is used mostly by Grafana for auto-completion of label values. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate). In this case it might be useful to set the `-search.maxTagValues` to quite low value in order to limit CPU and memory usage.
@@ -1551,8 +1553,27 @@ Both limits can be set simultaneously. If any of these limits is reached, then i
 The exceeded limits can be [monitored](#monitoring) with the following metrics:
 
 * `vm_hourly_series_limit_rows_dropped_total` - the number of metrics dropped due to exceeded hourly limit on the number of unique time series.
+
+* `vm_hourly_series_limit_max_series` - the hourly series limit set via `-storage.maxHourlySeries` command-line flag.
+
+* `vm_hourly_series_limit_current_series` - the current number of unique series during the last hour.
+  The following query can be useful for alerting when the number of unique series during the last hour exceeds 90% of the `-storage.maxHourlySeries`:
+
+  ```metricsql
+  vm_hourly_series_limit_current_series / vm_hourly_series_limit_max_series > 0.9
+  ```
+
 * `vm_daily_series_limit_rows_dropped_total` - the number of metrics dropped due to exceeded daily limit on the number of unique time series.
 
+* `vm_daily_series_limit_max_series` - the daily series limit set via `-storage.maxDailySeries` command-line flag.
+
+* `vm_daily_series_limit_current_series` - the current number of unique series during the last day.
+  The following query can be useful for alerting when the number of unique series during the last day exceeds 90% of the `-storage.maxDailySeries`:
+
+  ```metricsql
+  vm_daily_series_limit_current_series / vm_daily_series_limit_max_series > 0.9
+  ```
+
 These limits are approximate, so VictoriaMetrics can underflow/overflow the limit by a small percentage (usually less than 1%).
 
 See also more advanced [cardinality limiter in vmagent](https://docs.victoriametrics.com/vmagent.html#cardinality-limiter).
@@ -1807,6 +1828,7 @@ curl http://0.0.0.0:8428/debug/pprof/profile > cpu.pprof
 The command for collecting CPU profile waits for 30 seconds before returning.
 
 The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
 
 ## Integrations
 
@@ -2157,7 +2179,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
   -search.maxLookback duration
      Synonym to -search.lookback-delta from Prometheus. The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. See also '-search.maxStalenessInterval' flag, which has the same meaining due to historical reasons
   -search.maxPointsPerTimeseries int
-     The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
+     The maximum points per a single timeseries returned from /api/v1/query_range. This option doesn't limit the number of scanned raw samples in the database. The main purpose of this option is to limit the number of per-series points returned to graphing UI such as VMUI or Grafana. There is no sense in setting this limit to values bigger than the horizontal resolution of the graph (default 30000)
+  -search.maxPointsSubqueryPerTimeseries int
+     The maximum number of points per series, which can be generated by subquery. See https://valyala.medium.com/prometheus-subqueries-in-victoriametrics-9b1492b720b3 (default 100000)
   -search.maxQueryDuration duration
      The maximum duration for query execution (default 30s)
   -search.maxQueryLen size
@@ -2229,9 +2253,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      Overrides max size for storage/tsid cache. See https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#cache-tuning
      Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0)
   -storage.maxDailySeries int
-     The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See also -storage.maxHourlySeries
+     The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See https://docs.victoriametrics.com/#cardinality-limiter . See also -storage.maxHourlySeries
   -storage.maxHourlySeries int
-     The maximum number of unique series can be added to the storage during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See also -storage.maxDailySeries
+     The maximum number of unique series can be added to the storage during the last hour. Excess series are logged and dropped. This can be useful for limiting series cardinality. See https://docs.victoriametrics.com/#cardinality-limiter . See also -storage.maxDailySeries
   -storage.minFreeDiskSpaceBytes size
      The minimum free disk space at -storageDataPath after which the storage stops accepting new data
      Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 10000000)
diff --git a/docs/_config.yml b/docs/_config.yml
index 4b1c8fcf4..bb8598883 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -10,7 +10,7 @@ readme_index:
   with_frontmatter: true
 
 google:
-  gtag: UA-129683199-1
+  gtag: G-N9SVT8S3HK
 
 plugins:
   - github-pages
diff --git a/docs/vmagent.md b/docs/vmagent.md
index 7c708fb93..00aed3f58 100644
--- a/docs/vmagent.md
+++ b/docs/vmagent.md
@@ -897,6 +897,7 @@ curl http://0.0.0.0:8429/debug/pprof/profile > cpu.pprof
 The command for collecting CPU profile waits for 30 seconds before returning.
 
 The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
 
 ## Advanced usage
 
diff --git a/docs/vmalert.md b/docs/vmalert.md
index c008e07b7..48df5d9c1 100644
--- a/docs/vmalert.md
+++ b/docs/vmalert.md
@@ -634,6 +634,35 @@ Use the official [Grafana dashboard](https://grafana.com/grafana/dashboards/1495
 If you have suggestions for improvements or have found a bug - please open an issue on github or add
 a review to the dashboard.
 
+## Profiling
+
+`vmalert` provides handlers for collecting the following [Go profiles](https://blog.golang.org/profiling-go-programs):
+
+* Memory profile. It can be collected with the following command (replace `0.0.0.0` with hostname if needed):
+
+<div class="with-copy" markdown="1">
+
+```console
+curl http://0.0.0.0:8880/debug/pprof/heap > mem.pprof
+```
+
+</div>
+
+* CPU profile. It can be collected with the following command (replace `0.0.0.0` with hostname if needed):
+
+<div class="with-copy" markdown="1">
+
+```console
+curl http://0.0.0.0:8880/debug/pprof/profile > cpu.pprof
+```
+
+</div>
+
+The command for collecting CPU profile waits for 30 seconds before returning.
+
+The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
+
 ## Configuration
 
 ### Flags
diff --git a/docs/vmauth.md b/docs/vmauth.md
index 0aab5f153..63f1de9d3 100644
--- a/docs/vmauth.md
+++ b/docs/vmauth.md
@@ -221,6 +221,7 @@ curl http://0.0.0.0:8427/debug/pprof/profile > cpu.pprof
 The command for collecting CPU profile waits for 30 seconds before returning.
 
 The collected profiles may be analyzed with [go tool pprof](https://github.com/google/pprof).
+It is safe sharing the collected profiles from security point of view, since they do not contain sensitive information.
 
 ## Advanced usage
 
diff --git a/lib/storage/storage.go b/lib/storage/storage.go
index 24970e987..0f6555deb 100644
--- a/lib/storage/storage.go
+++ b/lib/storage/storage.go
@@ -469,8 +469,13 @@ type Metrics struct {
 	SlowPerDayIndexInserts uint64
 	SlowMetricNameLoads    uint64
 
-	HourlySeriesLimitRowsDropped uint64
-	DailySeriesLimitRowsDropped  uint64
+	HourlySeriesLimitRowsDropped   uint64
+	HourlySeriesLimitMaxSeries     uint64
+	HourlySeriesLimitCurrentSeries uint64
+
+	DailySeriesLimitRowsDropped   uint64
+	DailySeriesLimitMaxSeries     uint64
+	DailySeriesLimitCurrentSeries uint64
 
 	TimestampsBlocksMerged uint64
 	TimestampsBytesSaved   uint64
@@ -546,8 +551,17 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
 	m.SlowPerDayIndexInserts += atomic.LoadUint64(&s.slowPerDayIndexInserts)
 	m.SlowMetricNameLoads += atomic.LoadUint64(&s.slowMetricNameLoads)
 
-	m.HourlySeriesLimitRowsDropped += atomic.LoadUint64(&s.hourlySeriesLimitRowsDropped)
-	m.DailySeriesLimitRowsDropped += atomic.LoadUint64(&s.dailySeriesLimitRowsDropped)
+	if sl := s.hourlySeriesLimiter; sl != nil {
+		m.HourlySeriesLimitRowsDropped += atomic.LoadUint64(&s.hourlySeriesLimitRowsDropped)
+		m.HourlySeriesLimitMaxSeries += uint64(sl.MaxItems())
+		m.HourlySeriesLimitCurrentSeries += uint64(sl.CurrentItems())
+	}
+
+	if sl := s.dailySeriesLimiter; sl != nil {
+		m.DailySeriesLimitRowsDropped += atomic.LoadUint64(&s.dailySeriesLimitRowsDropped)
+		m.DailySeriesLimitMaxSeries += uint64(sl.MaxItems())
+		m.DailySeriesLimitCurrentSeries += uint64(sl.CurrentItems())
+	}
 
 	m.TimestampsBlocksMerged = atomic.LoadUint64(&timestampsBlocksMerged)
 	m.TimestampsBytesSaved = atomic.LoadUint64(&timestampsBytesSaved)


Test patch:
diff --git a/app/vmselect/promql/eval_test.go b/app/vmselect/promql/eval_test.go
index 43fb83154..76c735b21 100644
--- a/app/vmselect/promql/eval_test.go
+++ b/app/vmselect/promql/eval_test.go
@@ -48,3 +48,31 @@ m2{b="bar"} 1`, `{}`)
 	f(`m1{a="foo",b="bar"} 1
 m2{b="bar",c="x"} 1`, `{b="bar"}`)
 }
+
+func TestValidateMaxPointsPerSeriesFailure(t *testing.T) {
+	f := func(start, end, step int64, maxPoints int) {
+		t.Helper()
+		if err := ValidateMaxPointsPerSeries(start, end, step, maxPoints); err == nil {
+			t.Fatalf("expecint non-nil error for ValidateMaxPointsPerSeries(start=%d, end=%d, step=%d, maxPoints=%d)", start, end, step, maxPoints)
+		}
+	}
+	// zero step
+	f(0, 0, 0, 0)
+	f(0, 0, 0, 1)
+	// the maxPoints is smaller than the generated points
+	f(0, 1, 1, 0)
+	f(0, 1, 1, 1)
+	f(1659962171908, 1659966077742, 5000, 700)
+}
+
+func TestValidateMaxPointsPerSeriesSuccess(t *testing.T) {
+	f := func(start, end, step int64, maxPoints int) {
+		t.Helper()
+		if err := ValidateMaxPointsPerSeries(start, end, step, maxPoints); err != nil {
+			t.Fatalf("unexpected error in ValidateMaxPointsPerSeries(start=%d, end=%d, step=%d, maxPoints=%d): %s", start, end, step, maxPoints, err)
+		}
+	}
+	f(1, 1, 1, 2)
+	f(1659962171908, 1659966077742, 5000, 800)
+	f(1659962150000, 1659966070000, 10000, 393)
+}
diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go
index e1220b50d..d80b709b6 100644
--- a/app/vmselect/promql/exec_test.go
+++ b/app/vmselect/promql/exec_test.go
@@ -58,12 +58,13 @@ func TestExecSuccess(t *testing.T) {
 	f := func(q string, resultExpected []netstorage.Result) {
 		t.Helper()
 		ec := &EvalConfig{
-			Start:       start,
-			End:         end,
-			Step:        step,
-			MaxSeries:   1000,
-			Deadline:    searchutils.NewDeadline(time.Now(), time.Minute, ""),
-			RoundDigits: 100,
+			Start:              start,
+			End:                end,
+			Step:               step,
+			MaxPointsPerSeries: 1e4,
+			MaxSeries:          1000,
+			Deadline:           searchutils.NewDeadline(time.Now(), time.Minute, ""),
+			RoundDigits:        100,
 		}
 		for i := 0; i < 5; i++ {
 			result, err := Exec(nil, ec, q, false)
@@ -7743,12 +7744,13 @@ func TestExecError(t *testing.T) {
 	f := func(q string) {
 		t.Helper()
 		ec := &EvalConfig{
-			Start:       1000,
-			End:         2000,
-			Step:        100,
-			MaxSeries:   1000,
-			Deadline:    searchutils.NewDeadline(time.Now(), time.Minute, ""),
-			RoundDigits: 100,
+			Start:              1000,
+			End:                2000,
+			Step:               100,
+			MaxPointsPerSeries: 1e4,
+			MaxSeries:          1000,
+			Deadline:           searchutils.NewDeadline(time.Now(), time.Minute, ""),
+			RoundDigits:        100,
 		}
 		for i := 0; i < 4; i++ {
 			rv, err := Exec(nil, ec, q, false)
diff --git a/app/vmselect/promql/rollup_result_cache_test.go b/app/vmselect/promql/rollup_result_cache_test.go
index 5159f196d..45cc568c5 100644
--- a/app/vmselect/promql/rollup_result_cache_test.go
+++ b/app/vmselect/promql/rollup_result_cache_test.go
@@ -33,9 +33,10 @@ func TestRollupResultCache(t *testing.T) {
 	ResetRollupResultCache()
 	window := int64(456)
 	ec := &EvalConfig{
-		Start: 1000,
-		End:   2000,
-		Step:  200,
+		Start:              1000,
+		End:                2000,
+		Step:               200,
+		MaxPointsPerSeries: 1e4,
 
 		MayCache: true,
 	}
@@ -291,9 +292,10 @@ func TestRollupResultCache(t *testing.T) {
 
 func TestMergeTimeseries(t *testing.T) {
 	ec := &EvalConfig{
-		Start: 1000,
-		End:   2000,
-		Step:  200,
+		Start:              1000,
+		End:                2000,
+		Step:               200,
+		MaxPointsPerSeries: 1e4,
 	}
 	bStart := int64(1400)
 
diff --git a/app/vmselect/promql/rollup_test.go b/app/vmselect/promql/rollup_test.go
index b43a4fce4..fa589a0b1 100644
--- a/app/vmselect/promql/rollup_test.go
+++ b/app/vmselect/promql/rollup_test.go
@@ -578,13 +578,14 @@ func TestRollupNewRollupFuncError(t *testing.T) {
 func TestRollupNoWindowNoPoints(t *testing.T) {
 	t.Run("beforeStart", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupFirst,
-			Start:  0,
-			End:    4,
-			Step:   1,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              0,
+			End:                4,
+			Step:               1,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned != 0 {
 			t.Fatalf("expecting zero samplesScanned from rollupConfig.Do; got %d", samplesScanned)
@@ -595,13 +596,14 @@ func TestRollupNoWindowNoPoints(t *testing.T) {
 	})
 	t.Run("afterEnd", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupDelta,
-			Start:  120,
-			End:    148,
-			Step:   4,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupDelta,
+			Start:              120,
+			End:                148,
+			Step:               4,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -615,13 +617,14 @@ func TestRollupNoWindowNoPoints(t *testing.T) {
 func TestRollupWindowNoPoints(t *testing.T) {
 	t.Run("beforeStart", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupFirst,
-			Start:  0,
-			End:    4,
-			Step:   1,
-			Window: 3,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              0,
+			End:                4,
+			Step:               1,
+			Window:             3,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned != 0 {
 			t.Fatalf("expecting zero samplesScanned from rollupConfig.Do; got %d", samplesScanned)
@@ -632,13 +635,14 @@ func TestRollupWindowNoPoints(t *testing.T) {
 	})
 	t.Run("afterEnd", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupFirst,
-			Start:  161,
-			End:    191,
-			Step:   10,
-			Window: 3,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              161,
+			End:                191,
+			Step:               10,
+			Window:             3,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned != 0 {
 			t.Fatalf("expecting zero samplesScanned from rollupConfig.Do; got %d", samplesScanned)
@@ -652,13 +656,14 @@ func TestRollupWindowNoPoints(t *testing.T) {
 func TestRollupNoWindowPartialPoints(t *testing.T) {
 	t.Run("beforeStart", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupFirst,
-			Start:  0,
-			End:    25,
-			Step:   5,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              0,
+			End:                25,
+			Step:               5,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -669,13 +674,14 @@ func TestRollupNoWindowPartialPoints(t *testing.T) {
 	})
 	t.Run("afterEnd", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupFirst,
-			Start:  100,
-			End:    160,
-			Step:   20,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              100,
+			End:                160,
+			Step:               20,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -686,13 +692,14 @@ func TestRollupNoWindowPartialPoints(t *testing.T) {
 	})
 	t.Run("middle", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupFirst,
-			Start:  -50,
-			End:    150,
-			Step:   50,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              -50,
+			End:                150,
+			Step:               50,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -706,13 +713,14 @@ func TestRollupNoWindowPartialPoints(t *testing.T) {
 func TestRollupWindowPartialPoints(t *testing.T) {
 	t.Run("beforeStart", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupLast,
-			Start:  0,
-			End:    20,
-			Step:   5,
-			Window: 8,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupLast,
+			Start:              0,
+			End:                20,
+			Step:               5,
+			Window:             8,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -723,13 +731,14 @@ func TestRollupWindowPartialPoints(t *testing.T) {
 	})
 	t.Run("afterEnd", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupLast,
-			Start:  100,
-			End:    160,
-			Step:   20,
-			Window: 18,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupLast,
+			Start:              100,
+			End:                160,
+			Step:               20,
+			Window:             18,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -740,13 +749,14 @@ func TestRollupWindowPartialPoints(t *testing.T) {
 	})
 	t.Run("middle", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupLast,
-			Start:  0,
-			End:    150,
-			Step:   50,
-			Window: 19,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupLast,
+			Start:              0,
+			End:                150,
+			Step:               50,
+			Window:             19,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -760,13 +770,14 @@ func TestRollupWindowPartialPoints(t *testing.T) {
 func TestRollupFuncsLookbackDelta(t *testing.T) {
 	t.Run("1", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:          rollupFirst,
-			Start:         80,
-			End:           140,
-			Step:          10,
-			LookbackDelta: 1,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              80,
+			End:                140,
+			Step:               10,
+			LookbackDelta:      1,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -777,13 +788,14 @@ func TestRollupFuncsLookbackDelta(t *testing.T) {
 	})
 	t.Run("7", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:          rollupFirst,
-			Start:         80,
-			End:           140,
-			Step:          10,
-			LookbackDelta: 7,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              80,
+			End:                140,
+			Step:               10,
+			LookbackDelta:      7,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -794,13 +806,14 @@ func TestRollupFuncsLookbackDelta(t *testing.T) {
 	})
 	t.Run("0", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:          rollupFirst,
-			Start:         80,
-			End:           140,
-			Step:          10,
-			LookbackDelta: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              80,
+			End:                140,
+			Step:               10,
+			LookbackDelta:      0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -814,13 +827,14 @@ func TestRollupFuncsLookbackDelta(t *testing.T) {
 func TestRollupFuncsNoWindow(t *testing.T) {
 	t.Run("first", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupFirst,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupFirst,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -831,13 +845,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("count", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupCount,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupCount,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -848,13 +863,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("min", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupMin,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupMin,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -865,13 +881,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("max", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupMax,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupMax,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -882,13 +899,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("sum", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupSum,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupSum,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -899,13 +917,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("delta", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupDelta,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupDelta,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -916,13 +935,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("delta_prometheus", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupDeltaPrometheus,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupDeltaPrometheus,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -933,13 +953,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("idelta", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupIdelta,
-			Start:  10,
-			End:    130,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupIdelta,
+			Start:              10,
+			End:                130,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -950,13 +971,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("lag", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupLag,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupLag,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -967,13 +989,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("lifetime_1", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupLifetime,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupLifetime,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -984,13 +1007,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("lifetime_2", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupLifetime,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 200,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupLifetime,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             200,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1001,13 +1025,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("scrape_interval_1", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupScrapeInterval,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupScrapeInterval,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1018,13 +1043,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("scrape_interval_2", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupScrapeInterval,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 80,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupScrapeInterval,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             80,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1035,13 +1061,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("changes", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupChanges,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupChanges,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1052,13 +1079,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("changes_prometheus", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupChangesPrometheus,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupChangesPrometheus,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1069,13 +1097,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("changes_small_window", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupChanges,
-			Start:  0,
-			End:    45,
-			Step:   9,
-			Window: 9,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupChanges,
+			Start:              0,
+			End:                45,
+			Step:               9,
+			Window:             9,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1086,13 +1115,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("resets", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupResets,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupResets,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1103,13 +1133,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("avg", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupAvg,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupAvg,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1120,13 +1151,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("deriv", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupDerivSlow,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupDerivSlow,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1137,13 +1169,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("deriv_fast", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupDerivFast,
-			Start:  0,
-			End:    20,
-			Step:   4,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupDerivFast,
+			Start:              0,
+			End:                20,
+			Step:               4,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1154,13 +1187,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("ideriv", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupIderiv,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupIderiv,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1171,13 +1205,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("stddev", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupStddev,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupStddev,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1188,13 +1223,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("integrate", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupIntegrate,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupIntegrate,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1205,13 +1241,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("distinct_over_time_1", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupDistinct,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 0,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupDistinct,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             0,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1222,13 +1259,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("distinct_over_time_2", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupDistinct,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 80,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupDistinct,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             80,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1239,13 +1277,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("mode_over_time", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupModeOverTime,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 80,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupModeOverTime,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             80,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1256,13 +1295,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("rate_over_sum", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupRateOverSum,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 80,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupRateOverSum,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             80,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1273,13 +1313,14 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 	})
 	t.Run("zscore_over_time", func(t *testing.T) {
 		rc := rollupConfig{
-			Func:   rollupZScoreOverTime,
-			Start:  0,
-			End:    160,
-			Step:   40,
-			Window: 80,
-		}
-		rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+			Func:               rollupZScoreOverTime,
+			Start:              0,
+			End:                160,
+			Step:               40,
+			Window:             80,
+			MaxPointsPerSeries: 1e4,
+		}
+		rc.Timestamps = rc.getTimestamps()
 		values, samplesScanned := rc.Do(nil, testValues, testTimestamps)
 		if samplesScanned == 0 {
 			t.Fatalf("expecting non-zero samplesScanned from rollupConfig.Do")
@@ -1293,12 +1334,13 @@ func TestRollupFuncsNoWindow(t *testing.T) {
 func TestRollupBigNumberOfValues(t *testing.T) {
 	const srcValuesCount = 1e4
 	rc := rollupConfig{
-		Func:   rollupDefault,
-		End:    srcValuesCount,
-		Step:   srcValuesCount / 5,
-		Window: srcValuesCount / 4,
+		Func:               rollupDefault,
+		End:                srcValuesCount,
+		Step:               srcValuesCount / 5,
+		Window:             srcValuesCount / 4,
+		MaxPointsPerSeries: 1e4,
 	}
-	rc.Timestamps = getTimestamps(rc.Start, rc.End, rc.Step)
+	rc.Timestamps = rc.getTimestamps()
 	srcValues := make([]float64, srcValuesCount)
 	srcTimestamps := make([]int64, srcValuesCount)
 	for i := 0; i < srcValuesCount; i++ {