Repository: VictoriaMetrics/VictoriaMetrics
Pull request #2371 (2022-03-26 05:08:31)

build(deps): bump node-forge from 1.2.1 to 1.3.0 in /app/vmui/packages/vmui
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.2.1 to 1.3.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md">node-forge's changelog</a>.</em></p>
<blockquote>
<h2>1.3.0 - 2022-03-17</h2>
<h3>Security</h3>
<ul>
<li>Three RSA PKCS#1 v1.5 signature verification issues were reported by Moosa
Yahyazadeh (<a href="mailto:moosa-yahyazadeh@uiowa.edu">moosa-yahyazadeh@uiowa.edu</a>).</li>
<li><strong>HIGH</strong>: Leniency in checking <code>digestAlgorithm</code> structure can lead to
signature forgery.
<ul>
<li>The code is lenient in checking the digest algorithm structure. This can
allow a crafted structure that steals padding bytes and uses unchecked
portion of the PKCS#1 encoded message to forge a signature when a low
public exponent is being used. For more information, please see
<a href="https://mailarchive.ietf.org/arch/msg/openpgp/5rnE9ZRN1AokBVj3VqblGlP63QE/">&quot;Bleichenbacher's RSA signature forgery based on implementation
error&quot;</a>
by Hal Finney.</li>
<li>CVE ID: <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24771">CVE-2022-24771</a></li>
<li>GHSA ID: <a href="https://github.com/digitalbazaar/forge/security/advisories/GHSA-cfm4-qjh2-4765">GHSA-cfm4-qjh2-4765</a></li>
</ul>
</li>
<li><strong>HIGH</strong>: Failing to check tailing garbage bytes can lead to signature
forgery.
<ul>
<li>The code does not check for tailing garbage bytes after decoding a
<code>DigestInfo</code> ASN.1 structure. This can allow padding bytes to be removed
and garbage data added to forge a signature when a low public exponent is
being used.  For more information, please see <a href="https://mailarchive.ietf.org/arch/msg/openpgp/5rnE9ZRN1AokBVj3VqblGlP63QE/">&quot;Bleichenbacher's RSA
signature forgery based on implementation
error&quot;</a>
by Hal Finney.</li>
<li>CVE ID: <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24772">CVE-2022-24772</a></li>
<li>GHSA ID: <a href="https://github.com/digitalbazaar/forge/security/advisories/GHSA-x4jg-mjrx-434g">GHSA-x4jg-mjrx-434g</a></li>
</ul>
</li>
<li><strong>MEDIUM</strong>: Leniency in checking type octet.
<ul>
<li><code>DigestInfo</code> is not properly checked for proper ASN.1 structure. This can
lead to successful verification with signatures that contain invalid
structures but a valid digest.</li>
<li>CVE ID: <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24773">CVE-2022-24773</a></li>
<li>GHSA ID: <a href="https://github.com/digitalbazaar/forge/security/advisories/GHSA-2r2c-g63r-vccr">GHSA-2r2c-g63r-vccr</a></li>
</ul>
</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>[asn1] Add fallback to pretty print invalid UTF8 data.</li>
<li>[asn1] <code>fromDer</code> is now more strict and will default to ensuring all input
bytes are parsed or throw an error. A new option <code>parseAllBytes</code> can disable
this behavior.
<ul>
<li><strong>NOTE</strong>: The previous behavior is being changed since it can lead to
security issues with crafted inputs. It is possible that code doing custom
DER parsing may need to adapt to this new behavior and optional flag.</li>
</ul>
</li>
<li>[rsa] Add and use a validator to check for proper structure of parsed ASN.1
<code>RSASSA-PKCS-v1_5</code> <code>DigestInfo</code> data. Additionally check that the hash
algorithm identifier is a known value from RFC 8017
<code>PKCS1-v1-5DigestAlgorithms</code>. An invalid <code>DigestInfo</code> or algorithm identifier
will now throw an error.
<ul>
<li><strong>NOTE</strong>: The previous lenient behavior is being changed to be more strict
since it could lead to security issues with crafted inputs. It is possible
that code may have to handle the errors from these stricter checks.</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/digitalbazaar/forge/commit/6c5b90133d46af63d139b98bf65371732c8c7dad"><code>6c5b901</code></a> Release 1.3.0.</li>
<li><a href="https://github.com/digitalbazaar/forge/commit/0f3972ad5883a9869703c6f54a0627bc454bca47"><code>0f3972a</code></a> Update changelog.</li>
<li><a href="https://github.com/digitalbazaar/forge/commit/dc77b39dd347e7f8b60a0f25a311fe5f06130579"><code>dc77b39</code></a> Fix error checking.</li>
<li><a href="https://github.com/digitalbazaar/forge/commit/bb822c02df0b61211836472e29b9790cc541cdb2"><code>bb822c0</code></a> Add advisory links.</li>
<li><a href="https://github.com/digitalbazaar/forge/commit/d4395fec831622837ecfec9e428d4620e208f9a8"><code>d4395fe</code></a> Update changelog.</li>
<li><a href="https://github.com/digitalbazaar/forge/commit/a4405bb9d6b638084df478fa4ac60a410332c2d8"><code>a4405bb</code></a> Improve signature verification tests.</li>
<li><a href="https://github.com/digitalbazaar/forge/commit/aa9372d6dd78eb1479392b9274457036c2404b66"><code>aa9372d</code></a> Add missing RFC 8017 algorithm identifiers.</li>
<li><a href="https://github.com/digitalbazaar/forge/commit/3f0b49a0573ef1bb7af7f5673c0cfebf00424df1"><code>3f0b49a</code></a> Fix signature verification issues.</li>
<li><a href="https://github.com/digitalbazaar/forge/commit/c20f309311d83445e11abe7c313cc4b467c18914"><code>c20f309</code></a> Adjust remaining length.</li>
<li><a href="https://github.com/digitalbazaar/forge/commit/e27f61230f19fb9f085a163f31d0573305271b84"><code>e27f612</code></a> Remove unused option.</li>
<li>Additional commits viewable in <a href="https://github.com/digitalbazaar/forge/compare/v1.2.1...v1.3.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=node-forge&package-manager=npm_and_yarn&previous-version=1.2.1&new-version=1.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/VictoriaMetrics/VictoriaMetrics/network/alerts).

</details>

codecov[bot]: # [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/2371?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) Report
> Merging [#2371](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/2371?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) (b1914c9) into [master](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/a462b9785911648ef1534816ef411b35383e111c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) (a462b97) will **decrease** coverage by `0.02%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##           master    #2371      +/-   ##
==========================================
- Coverage   60.62%   60.59%   -0.03%     
==========================================
  Files         241      241              
  Lines       40973    40973              
==========================================
- Hits        24839    24827      -12     
- Misses      14494    14502       +8     
- Partials     1640     1644       +4     
```


| [Impacted Files](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/2371?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) | Coverage Δ | |
|---|---|---|
| [app/vmalert/remotewrite/remotewrite.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/2371/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-YXBwL3ZtYWxlcnQvcmVtb3Rld3JpdGUvcmVtb3Rld3JpdGUuZ28=) | `59.31% <0.00%> (-5.52%)` | :arrow_down: |
| [app/vmselect/promql/aggr\_incremental.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/2371/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-YXBwL3Ztc2VsZWN0L3Byb21xbC9hZ2dyX2luY3JlbWVudGFsLmdv) | `84.03% <0.00%> (-2.41%)` | :arrow_down: |
| [lib/storage/storage.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/2371/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-bGliL3N0b3JhZ2Uvc3RvcmFnZS5nbw==) | `67.71% <0.00%> (+0.22%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/2371?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/2371?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics). Last update [a462b97...b1914c9](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/2371?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).


Patch:
diff --git a/README.md b/README.md
index 996521017..121413b6a 100644
--- a/README.md
+++ b/README.md
@@ -820,13 +820,13 @@ Send a request to `http://<victoriametrics-addr>:8428/api/v1/export/native?match
 where `<timeseries_selector_for_export>` may contain any [time series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors)
 for metrics to export. Use `{__name__=~".*"}` selector for fetching all the time series.
 
-On large databases you may experience problems with limit on unique timeseries (default value is 300000). In this case you need to adjust `-search.maxUniqueTimeseries` parameter:
+On large databases you may experience problems with limit on the number of time series, which can be exported. In this case you need to adjust `-search.maxExportSeries` command-line flag:
 
 ```bash
 # count unique timeseries in database
 wget -O- -q 'http://your_victoriametrics_instance:8428/api/v1/series/count' | jq '.data[0]'
 
-# relaunch victoriametrics with search.maxUniqueTimeseries more than value from previous command
+# relaunch victoriametrics with search.maxExportSeries more than value from previous command
 ```
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
@@ -1835,6 +1835,12 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      The maximum number of concurrent search requests. It shouldn't be high, since a single request can saturate all the CPU cores. See also -search.maxQueueDuration (default 8)
   -search.maxExportDuration duration
      The maximum duration for /api/v1/export call (default 720h0m0s)
+  -search.maxExportSeries int
+     The maximum number of time series, which can be returned from /api/v1/export* APIs. This option allows limiting memory usage (default 1000000)
+  -search.maxFederateSeries int
+     The maximum number of time series, which can be returned from /federate. This option allows limiting memory usage (default 300000)
+  -search.maxGraphiteSeries int
+     The maximum number of time series, which can be scanned during queries to Graphite Render API. See https://docs.victoriametrics.com/#graphite-render-api-usage (default 300000)
   -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
@@ -1850,12 +1856,16 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      The maximum number of raw samples a single query can process across all time series. This protects from heavy queries, which select unexpectedly high number of raw samples. See also -search.maxSamplesPerSeries (default 1000000000)
   -search.maxSamplesPerSeries int
      The maximum number of raw samples a single query can scan per each time series. This option allows limiting memory usage (default 30000000)
+  -search.maxSeries int
+     The maximum number of time series, which can be returned from /api/v1/series. This option allows limiting memory usage (default 10000)
   -search.maxStalenessInterval duration
      The maximum interval for staleness calculations. By default it is automatically calculated from the median interval between samples. This flag could be useful for tuning Prometheus data model closer to Influx-style data model. See https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness for details. See also '-search.maxLookback' flag, which has the same meaning due to historical reasons
   -search.maxStatusRequestDuration duration
      The maximum duration for /api/v1/status/* requests (default 5m0s)
   -search.maxStepForPointsAdjustment duration
      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 (default 1m0s)
+  -search.maxTSDBStatusSeries int
+     The maximum number of time series, which can be processed during the call to /api/v1/status/tsdb. This option allows limiting memory usage (default 1000000)
   -search.maxTagKeys int
      The maximum number of tag keys returned from /api/v1/labels (default 100000)
   -search.maxTagValueSuffixesPerSearch int
@@ -1863,7 +1873,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
   -search.maxTagValues int
      The maximum number of tag values returned from /api/v1/label/<label_name>/values (default 100000)
   -search.maxUniqueTimeseries int
-     The maximum number of unique time series each search can scan. This option allows limiting memory usage (default 300000)
+     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 (default 300000)
   -search.minStalenessInterval duration
      The minimum interval for staleness calculations. This flag could be useful for removing gaps on graphs generated from time series with irregular intervals between samples. See also '-search.maxStalenessInterval'
   -search.noStaleMarkers
diff --git a/app/vmselect/graphite/tags_api.go b/app/vmselect/graphite/tags_api.go
index e5835beaa..05a078b15 100644
--- a/app/vmselect/graphite/tags_api.go
+++ b/app/vmselect/graphite/tags_api.go
@@ -54,7 +54,7 @@ func TagsDelSeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.Re
 			})
 		}
 		tfss := joinTagFilterss(tfs, etfs)
-		sq := storage.NewSearchQuery(0, ct, tfss)
+		sq := storage.NewSearchQuery(0, ct, tfss, 0)
 		n, err := netstorage.DeleteSeries(sq, deadline)
 		if err != nil {
 			return fmt.Errorf("cannot delete series for %q: %w", sq, err)
@@ -196,7 +196,7 @@ func TagsAutoCompleteValuesHandler(startTime time.Time, w http.ResponseWriter, r
 		}
 	} else {
 		// Slow path: use netstorage.SearchMetricNames for applying `expr` filters.
-		sq, err := getSearchQueryForExprs(startTime, etfs, exprs)
+		sq, err := getSearchQueryForExprs(startTime, etfs, exprs, limit*10)
 		if err != nil {
 			return err
 		}
@@ -282,7 +282,7 @@ func TagsAutoCompleteTagsHandler(startTime time.Time, w http.ResponseWriter, r *
 		}
 	} else {
 		// Slow path: use netstorage.SearchMetricNames for applying `expr` filters.
-		sq, err := getSearchQueryForExprs(startTime, etfs, exprs)
+		sq, err := getSearchQueryForExprs(startTime, etfs, exprs, limit*10)
 		if err != nil {
 			return err
 		}
@@ -349,7 +349,7 @@ func TagsFindSeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.R
 	if err != nil {
 		return fmt.Errorf("cannot setup tag filters: %w", err)
 	}
-	sq, err := getSearchQueryForExprs(startTime, etfs, exprs)
+	sq, err := getSearchQueryForExprs(startTime, etfs, exprs, limit*10)
 	if err != nil {
 		return err
 	}
@@ -474,14 +474,14 @@ func getInt(r *http.Request, argName string) (int, error) {
 	return n, nil
 }
 
-func getSearchQueryForExprs(startTime time.Time, etfs [][]storage.TagFilter, exprs []string) (*storage.SearchQuery, error) {
+func getSearchQueryForExprs(startTime time.Time, etfs [][]storage.TagFilter, exprs []string, maxMetrics int) (*storage.SearchQuery, error) {
 	tfs, err := exprsToTagFilters(exprs)
 	if err != nil {
 		return nil, err
 	}
 	ct := startTime.UnixNano() / 1e6
 	tfss := joinTagFilterss(tfs, etfs)
-	sq := storage.NewSearchQuery(0, ct, tfss)
+	sq := storage.NewSearchQuery(0, ct, tfss, maxMetrics)
 	return sq, nil
 }
 
diff --git a/app/vmselect/netstorage/netstorage.go b/app/vmselect/netstorage/netstorage.go
index 54dfc4b1f..3651316c9 100644
--- a/app/vmselect/netstorage/netstorage.go
+++ b/app/vmselect/netstorage/netstorage.go
@@ -26,7 +26,6 @@ var (
 	maxTagKeysPerSearch          = flag.Int("search.maxTagKeys", 100e3, "The maximum number of tag keys returned from /api/v1/labels")
 	maxTagValuesPerSearch        = flag.Int("search.maxTagValues", 100e3, "The maximum number of tag values returned from /api/v1/label/<label_name>/values")
 	maxTagValueSuffixesPerSearch = flag.Int("search.maxTagValueSuffixesPerSearch", 100e3, "The maximum number of tag value suffixes returned from /metrics/find")
-	maxMetricsPerSearch          = flag.Int("search.maxUniqueTimeseries", 300e3, "The maximum number of unique time series each search can scan. This option allows limiting memory usage")
 	maxSamplesPerSeries          = flag.Int("search.maxSamplesPerSeries", 30e6, "The maximum number of raw samples a single query can scan per each time series. This option allows limiting memory usage")
 	maxSamplesPerQuery           = flag.Int("search.maxSamplesPerQuery", 1e9, "The maximum number of raw samples a single query can process across all time series. This protects from heavy queries, which select unexpectedly high number of raw samples. See also -search.maxSamplesPerSeries")
 )
@@ -600,7 +599,7 @@ func DeleteSeries(sq *storage.SearchQuery, deadline searchutils.Deadline) (int,
 		MinTimestamp: sq.MinTimestamp,
 		MaxTimestamp: sq.MaxTimestamp,
 	}
-	tfss, err := setupTfss(tr, sq.TagFilterss, deadline)
+	tfss, err := setupTfss(tr, sq.TagFilterss, sq.MaxMetrics, deadline)
 	if err != nil {
 		return 0, err
 	}
@@ -805,11 +804,11 @@ func GetLabelEntries(deadline searchutils.Deadline) ([]storage.TagEntry, error)
 }
 
 // GetTSDBStatusForDate returns tsdb status according to https://prometheus.io/docs/prometheus/latest/querying/api/#tsdb-stats
-func GetTSDBStatusForDate(deadline searchutils.Deadline, date uint64, topN int) (*storage.TSDBStatus, error) {
+func GetTSDBStatusForDate(deadline searchutils.Deadline, date uint64, topN, maxMetrics int) (*storage.TSDBStatus, error) {
 	if deadline.Exceeded() {
 		return nil, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
 	}
-	status, err := vmstorage.GetTSDBStatusForDate(date, topN, deadline.Deadline())
+	status, err := vmstorage.GetTSDBStatusForDate(date, topN, maxMetrics, deadline.Deadline())
 	if err != nil {
 		return nil, fmt.Errorf("error during tsdb status request: %w", err)
 	}
@@ -827,12 +826,12 @@ func GetTSDBStatusWithFilters(deadline searchutils.Deadline, sq *storage.SearchQ
 		MinTimestamp: sq.MinTimestamp,
 		MaxTimestamp: sq.MaxTimestamp,
 	}
-	tfss, err := setupTfss(tr, sq.TagFilterss, deadline)
+	tfss, err := setupTfss(tr, sq.TagFilterss, sq.MaxMetrics, deadline)
 	if err != nil {
 		return nil, err
 	}
 	date := uint64(tr.MinTimestamp) / (3600 * 24 * 1000)
-	status, err := vmstorage.GetTSDBStatusWithFiltersForDate(tfss, date, topN, deadline.Deadline())
+	status, err := vmstorage.GetTSDBStatusWithFiltersForDate(tfss, date, topN, sq.MaxMetrics, deadline.Deadline())
 	if err != nil {
 		return nil, fmt.Errorf("error during tsdb status with filters request: %w", err)
 	}
@@ -883,7 +882,7 @@ func ExportBlocks(sq *storage.SearchQuery, deadline searchutils.Deadline, f func
 	if err := vmstorage.CheckTimeRange(tr); err != nil {
 		return err
 	}
-	tfss, err := setupTfss(tr, sq.TagFilterss, deadline)
+	tfss, err := setupTfss(tr, sq.TagFilterss, sq.MaxMetrics, deadline)
 	if err != nil {
 		return err
 	}
@@ -894,7 +893,7 @@ func ExportBlocks(sq *storage.SearchQuery, deadline searchutils.Deadline, f func
 	sr := getStorageSearch()
 	defer putStorageSearch(sr)
 	startTime := time.Now()
-	sr.Init(vmstorage.Storage, tfss, tr, *maxMetricsPerSearch, deadline.Deadline())
+	sr.Init(vmstorage.Storage, tfss, tr, sq.MaxMetrics, deadline.Deadline())
 	indexSearchDuration.UpdateDuration(startTime)
 
 	// Start workers that call f in parallel on available CPU cores.
@@ -991,12 +990,12 @@ func SearchMetricNames(sq *storage.SearchQuery, deadline searchutils.Deadline) (
 	if err := vmstorage.CheckTimeRange(tr); err != nil {
 		return nil, err
 	}
-	tfss, err := setupTfss(tr, sq.TagFilterss, deadline)
+	tfss, err := setupTfss(tr, sq.TagFilterss, sq.MaxMetrics, deadline)
 	if err != nil {
 		return nil, err
 	}
 
-	mns, err := vmstorage.SearchMetricNames(tfss, tr, *maxMetricsPerSearch, deadline.Deadline())
+	mns, err := vmstorage.SearchMetricNames(tfss, tr, sq.MaxMetrics, deadline.Deadline())
 	if err != nil {
 		return nil, fmt.Errorf("cannot find metric names: %w", err)
 	}
@@ -1019,7 +1018,7 @@ func ProcessSearchQuery(sq *storage.SearchQuery, fetchData bool, deadline search
 	if err := vmstorage.CheckTimeRange(tr); err != nil {
 		return nil, err
 	}
-	tfss, err := setupTfss(tr, sq.TagFilterss, deadline)
+	tfss, err := setupTfss(tr, sq.TagFilterss, sq.MaxMetrics, deadline)
 	if err != nil {
 		return nil, err
 	}
@@ -1029,7 +1028,7 @@ func ProcessSearchQuery(sq *storage.SearchQuery, fetchData bool, deadline search
 
 	sr := getStorageSearch()
 	startTime := time.Now()
-	maxSeriesCount := sr.Init(vmstorage.Storage, tfss, tr, *maxMetricsPerSearch, deadline.Deadline())
+	maxSeriesCount := sr.Init(vmstorage.Storage, tfss, tr, sq.MaxMetrics, deadline.Deadline())
 	indexSearchDuration.UpdateDuration(startTime)
 	m := make(map[string][]blockRef, maxSeriesCount)
 	orderedMetricNames := make([]string, 0, maxSeriesCount)
@@ -1111,7 +1110,7 @@ type blockRef struct {
 	addr    tmpBlockAddr
 }
 
-func setupTfss(tr storage.TimeRange, tagFilterss [][]storage.TagFilter, deadline searchutils.Deadline) ([]*storage.TagFilters, error) {
+func setupTfss(tr storage.TimeRange, tagFilterss [][]storage.TagFilter, maxMetrics int, deadline searchutils.Deadline) ([]*storage.TagFilters, error) {
 	tfss := make([]*storage.TagFilters, 0, len(tagFilterss))
 	for _, tagFilters := range tagFilterss {
 		tfs := storage.NewTagFilters()
@@ -1119,13 +1118,13 @@ func setupTfss(tr storage.TimeRange, tagFilterss [][]storage.TagFilter, deadline
 			tf := &tagFilters[i]
 			if string(tf.Key) == "__graphite__" {
 				query := tf.Value
-				paths, err := vmstorage.SearchGraphitePaths(tr, query, *maxMetricsPerSearch, deadline.Deadline())
+				paths, err := vmstorage.SearchGraphitePaths(tr, query, maxMetrics, deadline.Deadline())
 				if err != nil {
 					return nil, fmt.Errorf("error when searching for Graphite paths for query %q: %w", query, err)
 				}
-				if len(paths) >= *maxMetricsPerSearch {
-					return nil, fmt.Errorf("more than -search.maxUniqueTimeseries=%d time series match Graphite query %q; "+
-						"either narrow down the query or increase -search.maxUniqueTimeseries command-line flag value", *maxMetricsPerSearch, query)
+				if len(paths) >= maxMetrics {
+					return nil, fmt.Errorf("more than %d time series match Graphite query %q; "+
+						"either narrow down the query or increase the corresponding -search.max* command-line flag value", maxMetrics, query)
 				}
 				tfs.AddGraphiteQuery(query, paths, tf.IsNegative)
 				continue
diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go
index 20b46e677..9376fb1c0 100644
--- a/app/vmselect/prometheus/prometheus.go
+++ b/app/vmselect/prometheus/prometheus.go
@@ -42,6 +42,12 @@ var (
 		"See also '-search.maxLookback' flag, which has the same meaning due to historical reasons")
 	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", 300e3, "The maximum number of time series, which can be returned from /federate. This option allows limiting memory usage")
+	maxExportSeries     = flag.Int("search.maxExportSeries", 1e6, "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", 1e6, "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", 10e3, "The maximum number of time series, which can be returned from /api/v1/series. This option allows limiting memory usage")
 )
 
 // Default step used if not set.
@@ -78,7 +84,7 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
 	if err != nil {
 		return err
 	}
-	sq := storage.NewSearchQuery(start, end, tagFilterss)
+	sq := storage.NewSearchQuery(start, end, tagFilterss, *maxFederateSeries)
 	rss, err := netstorage.ProcessSearchQuery(sq, true, deadline)
 	if err != nil {
 		return fmt.Errorf("cannot fetch data for %q: %w", sq, err)
@@ -135,7 +141,7 @@ func ExportCSVHandler(startTime time.Time, w http.ResponseWriter, r *http.Reques
 	if err != nil {
 		return err
 	}
-	sq := storage.NewSearchQuery(start, end, tagFilterss)
+	sq := storage.NewSearchQuery(start, end, tagFilterss, *maxExportSeries)
 	w.Header().Set("Content-Type", "text/csv; charset=utf-8")
 	bw := bufferedwriter.Get(w)
 	defer bufferedwriter.Put(bw)
@@ -232,7 +238,7 @@ func ExportNativeHandler(startTime time.Time, w http.ResponseWriter, r *http.Req
 	if err != nil {
 		return err
 	}
-	sq := storage.NewSearchQuery(start, end, tagFilterss)
+	sq := storage.NewSearchQuery(start, end, tagFilterss, *maxExportSeries)
 	w.Header().Set("Content-Type", "VictoriaMetrics/native")
 	bw := bufferedwriter.Get(w)
 	defer bufferedwriter.Put(bw)
@@ -383,7 +389,7 @@ func exportHandler(w http.ResponseWriter, matches []string, etfs [][]storage.Tag
 	}
 	tagFilterss = searchutils.JoinTagFilterss(tagFilterss, etfs)
 
-	sq := storage.NewSearchQuery(start, end, tagFilterss)
+	sq := storage.NewSearchQuery(start, end, tagFilterss, *maxExportSeries)
 	w.Header().Set("Content-Type", contentType)
 	bw := bufferedwriter.Get(w)
 	defer bufferedwriter.Put(bw)
@@ -484,7 +490,7 @@ func DeleteHandler(startTime time.Time, r *http.Request) error {
 		return err
 	}
 	ct := startTime.UnixNano() / 1e6
-	sq := storage.NewSearchQuery(0, ct, tagFilterss)
+	sq := storage.NewSearchQuery(0, ct, tagFilterss, 0)
 	deletedCount, err := netstorage.DeleteSeries(sq, deadline)
 	if err != nil {
 		return fmt.Errorf("cannot delete time series: %w", err)
@@ -597,7 +603,7 @@ func labelValuesWithMatches(labelName string, matches []string, etfs [][]storage
 	if len(tagFilterss) == 0 {
 		logger.Panicf("BUG: tagFilterss must be non-empty")
 	}
-	sq := storage.NewSearchQuery(start, end, tagFilterss)
+	sq := storage.NewSearchQuery(start, end, tagFilterss, *maxSeriesLimit)
 	m := make(map[string]struct{})
 	if end-start > 24*3600*1000 {
 		// It is cheaper to call SearchMetricNames on time ranges exceeding a day.
@@ -709,12 +715,12 @@ func TSDBStatusHandler(startTime time.Time, w http.ResponseWriter, r *http.Reque
 	}
 	var status *storage.TSDBStatus
 	if len(matches) == 0 && len(etfs) == 0 {
-		status, err = netstorage.GetTSDBStatusForDate(deadline, date, topN)
+		status, err = netstorage.GetTSDBStatusForDate(deadline, date, topN, *maxTSDBStatusSeries)
 		if err != nil {
 			return fmt.Errorf(`cannot obtain tsdb status for date=%d, topN=%d: %w`, date, topN, err)
 		}
 	} else {
-		status, err = tsdbStatusWithMatches(matches, etfs, date, topN, deadline)
+		status, err = tsdbStatusWithMatches(matches, etfs, date, topN, *maxTSDBStatusSeries, deadline)
 		if err != nil {
 			return fmt.Errorf("cannot obtain tsdb status with matches for date=%d, topN=%d: %w", date, topN, err)
 		}
@@ -729,7 +735,7 @@ func TSDBStatusHandler(startTime time.Time, w http.ResponseWriter, r *http.Reque
 	return nil
 }
 
-func tsdbStatusWithMatches(matches []string, etfs [][]storage.TagFilter, date uint64, topN int, deadline searchutils.Deadline) (*storage.TSDBStatus, error) {
+func tsdbStatusWithMatches(matches []string, etfs [][]storage.TagFilter, date uint64, topN, maxMetrics int, deadline searchutils.Deadline) (*storage.TSDBStatus, error) {
 	tagFilterss, err := getTagFilterssFromMatches(matches)
 	if err != nil {
 		return nil, err
@@ -740,7 +746,7 @@ func tsdbStatusWithMatches(matches []string, etfs [][]storage.TagFilter, date ui
 	}
 	start := int64(date*secsPerDay) * 1000
 	end := int64(date*secsPerDay+secsPerDay) * 1000
-	sq := storage.NewSearchQuery(start, end, tagFilterss)
+	sq := storage.NewSearchQuery(start, end, tagFilterss, maxMetrics)
 	status, err := netstorage.GetTSDBStatusWithFilters(deadline, sq, topN)
 	if err != nil {
 		return nil, err
@@ -835,7 +841,7 @@ func labelsWithMatches(matches []string, etfs [][]storage.TagFilter, start, end
 	if len(tagFilterss) == 0 {
 		logger.Panicf("BUG: tagFilterss must be non-empty")
 	}
-	sq := storage.NewSearchQuery(start, end, tagFilterss)
+	sq := storage.NewSearchQuery(start, end, tagFilterss, *maxSeriesLimit)
 	m := make(map[string]struct{})
 	if end-start > 24*3600*1000 {
 		// It is cheaper to call SearchMetricNames on time ranges exceeding a day.
@@ -933,7 +939,7 @@ func SeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
 	if start >= end {
 		end = start + defaultStep
 	}
-	sq := storage.NewSearchQuery(start, end, tagFilterss)
+	sq := storage.NewSearchQuery(start, end, tagFilterss, *maxSeriesLimit)
 	if end-start > 24*3600*1000 {
 		// It is cheaper to call SearchMetricNames on time ranges exceeding a day.
 		mns, err := netstorage.SearchMetricNames(sq, deadline)
@@ -1080,6 +1086,7 @@ func QueryHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) e
 		Start:               start,
 		End:                 start,
 		Step:                step,
+		MaxSeries:           *maxUniqueTimeseries,
 		QuotedRemoteAddr:    httpserver.GetQuotedRemoteAddr(r),
 		Deadline:            deadline,
 		LookbackDelta:       lookbackDelta,
@@ -1170,6 +1177,7 @@ func queryRangeHandler(startTime time.Time, w http.ResponseWriter, query string,
 		Start:               start,
 		End:                 end,
 		Step:                step,
+		MaxSeries:           *maxUniqueTimeseries,
 		QuotedRemoteAddr:    httpserver.GetQuotedRemoteAddr(r),
 		Deadline:            deadline,
 		MayCache:            mayCache,
diff --git a/app/vmselect/promql/eval.go b/app/vmselect/promql/eval.go
index 0a88756a5..0ee22b6a7 100644
--- a/app/vmselect/promql/eval.go
+++ b/app/vmselect/promql/eval.go
@@ -93,6 +93,10 @@ type EvalConfig struct {
 	End   int64
 	Step  int64
 
+	// MaxSeries is the maximum number of time series, which can be scanned by the query.
+	// Zero means 'no limit'
+	MaxSeries int
+
 	// QuotedRemoteAddr contains quoted remote address.
 	QuotedRemoteAddr string
 
@@ -113,12 +117,13 @@ type EvalConfig struct {
 	timestampsOnce sync.Once
 }
 
-// newEvalConfig returns new EvalConfig copy from src.
-func newEvalConfig(src *EvalConfig) *EvalConfig {
+// copyEvalConfig returns src copy.
+func copyEvalConfig(src *EvalConfig) *EvalConfig {
 	var ec EvalConfig
 	ec.Start = src.Start
 	ec.End = src.End
 	ec.Step = src.Step
+	ec.MaxSeries = src.MaxSeries
 	ec.Deadline = src.Deadline
 	ec.MayCache = src.MayCache
 	ec.LookbackDelta = src.LookbackDelta
@@ -575,7 +580,7 @@ func evalRollupFunc(ec *EvalConfig, funcName string, rf rollupFunc, expr metrics
 		return nil, fmt.Errorf("`@` modifier must return a single series; it returns %d series instead", len(tssAt))
 	}
 	atTimestamp := int64(tssAt[0].Values[0] * 1000)
-	ecNew := newEvalConfig(ec)
+	ecNew := copyEvalConfig(ec)
 	ecNew.Start = atTimestamp
 	ecNew.End = atTimestamp
 	tss, err := evalRollupFuncWithoutAt(ecNew, funcName, rf, expr, re, iafc)
@@ -602,7 +607,7 @@ func evalRollupFuncWithoutAt(ec *EvalConfig, funcName string, rf rollupFunc, exp
 	var offset int64
 	if re.Offset != nil {
 		offset = re.Offset.Duration(ec.Step)
-		ecNew = newEvalConfig(ecNew)
+		ecNew = copyEvalConfig(ecNew)
 		ecNew.Start -= offset
 		ecNew.End -= offset
 		// There is no need in calling AdjustStartEnd() on ecNew if ecNew.MayCache is set to true,
@@ -615,7 +620,7 @@ func evalRollupFuncWithoutAt(ec *EvalConfig, funcName string, rf rollupFunc, exp
 		// in order to obtain expected OHLC results.
 		// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/309#issuecomment-582113462
 		step := ecNew.Step
-		ecNew = newEvalConfig(ecNew)
+		ecNew = copyEvalConfig(ecNew)
 		ecNew.Start += step
 		ecNew.End += step
 		offset -= step
@@ -679,7 +684,7 @@ func evalRollupFuncWithSubquery(ec *EvalConfig, funcName string, rf rollupFunc,
 	}
 	window := re.Window.Duration(ec.Step)
 
-	ecSQ := newEvalConfig(ec)
+	ecSQ := copyEvalConfig(ec)
 	ecSQ.Start -= window + maxSilenceInterval + step
 	ecSQ.End += step
 	ecSQ.Step = step
@@ -834,7 +839,7 @@ func evalRollupFuncWithMetricExpr(ec *EvalConfig, funcName string, rf rollupFunc
 	} else {
 		minTimestamp -= ec.Step
 	}
-	sq := storage.NewSearchQuery(minTimestamp, ec.End, tfss)
+	sq := storage.NewSearchQuery(minTimestamp, ec.End, tfss, ec.MaxSeries)
 	rss, err := netstorage.ProcessSearchQuery(sq, true, ec.Deadline)
 	if err != nil {
 		return nil, err
diff --git a/app/vmstorage/main.go b/app/vmstorage/main.go
index 4d45253b6..4c138be2f 100644
--- a/app/vmstorage/main.go
+++ b/app/vmstorage/main.go
@@ -227,17 +227,17 @@ func SearchTagEntries(maxTagKeys, maxTagValues int, deadline uint64) ([]storage.
 }
 
 // GetTSDBStatusForDate returns TSDB status for the given date.
-func GetTSDBStatusForDate(date uint64, topN int, deadline uint64) (*storage.TSDBStatus, error) {
+func GetTSDBStatusForDate(date uint64, topN, maxMetrics int, deadline uint64) (*storage.TSDBStatus, error) {
 	WG.Add(1)
-	status, err := Storage.GetTSDBStatusWithFiltersForDate(nil, date, topN, deadline)
+	status, err := Storage.GetTSDBStatusWithFiltersForDate(nil, date, topN, maxMetrics, deadline)
 	WG.Done()
 	return status, err
 }
 
 // GetTSDBStatusWithFiltersForDate returns TSDB status for given filters on the given date.
-func GetTSDBStatusWithFiltersForDate(tfss []*storage.TagFilters, date uint64, topN int, deadline uint64) (*storage.TSDBStatus, error) {
+func GetTSDBStatusWithFiltersForDate(tfss []*storage.TagFilters, date uint64, topN, maxMetrics int, deadline uint64) (*storage.TSDBStatus, error) {
 	WG.Add(1)
-	status, err := Storage.GetTSDBStatusWithFiltersForDate(tfss, date, topN, deadline)
+	status, err := Storage.GetTSDBStatusWithFiltersForDate(tfss, date, topN, maxMetrics, deadline)
 	WG.Done()
 	return status, err
 }
@@ -680,6 +680,10 @@ func registerStorageMetrics() {
 	metrics.NewGauge(`vm_cache_entries{type="storage/regexps"}`, func() float64 {
 		return float64(storage.RegexpCacheSize())
 	})
+	metrics.NewGauge(`vm_cache_entries{type="storage/regexpPrefixes"}`, func() float64 {
+		return float64(storage.RegexpPrefixesCacheSize())
+	})
+
 	metrics.NewGauge(`vm_cache_entries{type="storage/prefetchedMetricIDs"}`, func() float64 {
 		return float64(m().PrefetchedMetricIDsSize)
 	})
@@ -714,6 +718,12 @@ func registerStorageMetrics() {
 	metrics.NewGauge(`vm_cache_size_bytes{type="indexdb/tagFilters"}`, func() float64 {
 		return float64(idbm().TagFiltersCacheSizeBytes)
 	})
+	metrics.NewGauge(`vm_cache_size_bytes{type="storage/regexps"}`, func() float64 {
+		return float64(storage.RegexpCacheSizeBytes())
+	})
+	metrics.NewGauge(`vm_cache_size_bytes{type="storage/regexpPrefixes"}`, func() float64 {
+		return float64(storage.RegexpPrefixesCacheSizeBytes())
+	})
 	metrics.NewGauge(`vm_cache_size_bytes{type="storage/prefetchedMetricIDs"}`, func() float64 {
 		return float64(m().PrefetchedMetricIDsSizeBytes)
 	})
@@ -739,6 +749,12 @@ func registerStorageMetrics() {
 	metrics.NewGauge(`vm_cache_size_max_bytes{type="indexdb/tagFilters"}`, func() float64 {
 		return float64(idbm().TagFiltersCacheSizeMaxBytes)
 	})
+	metrics.NewGauge(`vm_cache_size_max_bytes{type="storage/regexps"}`, func() float64 {
+		return float64(storage.RegexpCacheMaxSizeBytes())
+	})
+	metrics.NewGauge(`vm_cache_size_max_bytes{type="storage/regexpPrefixes"}`, func() float64 {
+		return float64(storage.RegexpPrefixesCacheMaxSizeBytes())
+	})
 
 	metrics.NewGauge(`vm_cache_requests_total{type="storage/tsid"}`, func() float64 {
 		return float64(m().TSIDCacheRequests)
@@ -764,6 +780,9 @@ func registerStorageMetrics() {
 	metrics.NewGauge(`vm_cache_requests_total{type="storage/regexps"}`, func() float64 {
 		return float64(storage.RegexpCacheRequests())
 	})
+	metrics.NewGauge(`vm_cache_requests_total{type="storage/regexpPrefixes"}`, func() float64 {
+		return float64(storage.RegexpPrefixesCacheRequests())
+	})
 
 	metrics.NewGauge(`vm_cache_misses_total{type="storage/tsid"}`, func() float64 {
 		return float64(m().TSIDCacheMisses)
@@ -789,6 +808,9 @@ func registerStorageMetrics() {
 	metrics.NewGauge(`vm_cache_misses_total{type="storage/regexps"}`, func() float64 {
 		return float64(storage.RegexpCacheMisses())
 	})
+	metrics.NewGauge(`vm_cache_misses_total{type="storage/regexpPrefixes"}`, func() float64 {
+		return float64(storage.RegexpPrefixesCacheMisses())
+	})
 
 	metrics.NewGauge(`vm_deleted_metrics_total{type="indexdb"}`, func() float64 {
 		return float64(idbm().DeletedMetricsCount)
diff --git a/app/vmui/packages/vmui/package-lock.json b/app/vmui/packages/vmui/package-lock.json
index a2664808b..9813b2b42 100644
--- a/app/vmui/packages/vmui/package-lock.json
+++ b/app/vmui/packages/vmui/package-lock.json
@@ -13861,9 +13861,9 @@
       }
     },
     "node_modules/node-forge": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
-      "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
+      "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
       "dev": true,
       "peer": true,
       "engines": {
@@ -30004,9 +30004,9 @@
       }
     },
     "node-forge": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
-      "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
+      "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
       "dev": true,
       "peer": true
     },
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index baa280a05..323185351 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -15,6 +15,18 @@ The following tip changes can be tested by building VictoriaMetrics components f
 
 ## tip
 
+* FEATURE: add the following command-line flags, which can be used for fine-grained limiting of CPU and memory usage during various API calls:
+
+  * `-search.maxFederateSeries` for limiting the number of time series, which can be returned from [/federate](https://docs.victoriametrics.com/#federation).
+  * `-search.maxExportSeries` for limiting the number of time series, which can be returned from [/api/v1/export](https://docs.victoriametrics.com/#how-to-export-time-series).
+  * `-search.maxSeries` for limiting the number of time series, which can be returned from [/api/v1/series](https://docs.victoriametrics.com/url-examples.html#apiv1series).
+  * `-search.maxTSDBStatusSeries` for limiting the number of time series, which can be scanned during the request to [/api/v1/status/tsdb](https://docs.victoriametrics.com/#tsdb-stats).
+  * `-search.maxGraphiteSeries` for limiting the number of time series, which can be scanned during the request to [Graphite Render API](https://docs.victoriametrics.com/#graphite-render-api-usage).
+
+Previously the `-search.maxUniqueTimeseries` command-line flag was used as a global limit for all these APIs. Now the `-search.maxUniqueTimeseries` is used only for limiting the number of time series, which can be scanned during requests to [/api/v1/query](https://docs.victoriametrics.com/url-examples.html#apiv1query) and [/api/v1/query_range](https://docs.victoriametrics.com/url-examples.html#apiv1query_range).
+
+When using [cluster version of VictoriaMetrics](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html), these command-line flags (including `-search.maxUniqueTimeseries`) must be passed to `vmselect` instead of `vmstorage`.
+
 * BUGFIX: return `Content-Type: text/html` response header when requesting `/` HTTP path at VictoriaMetrics components. Previously `text/plain` response header was returned, which could lead to broken page formatting. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2323).
 
 ## [v1.75.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.75.0)
diff --git a/docs/README.md b/docs/README.md
index 996521017..121413b6a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -820,13 +820,13 @@ Send a request to `http://<victoriametrics-addr>:8428/api/v1/export/native?match
 where `<timeseries_selector_for_export>` may contain any [time series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors)
 for metrics to export. Use `{__name__=~".*"}` selector for fetching all the time series.
 
-On large databases you may experience problems with limit on unique timeseries (default value is 300000). In this case you need to adjust `-search.maxUniqueTimeseries` parameter:
+On large databases you may experience problems with limit on the number of time series, which can be exported. In this case you need to adjust `-search.maxExportSeries` command-line flag:
 
 ```bash
 # count unique timeseries in database
 wget -O- -q 'http://your_victoriametrics_instance:8428/api/v1/series/count' | jq '.data[0]'
 
-# relaunch victoriametrics with search.maxUniqueTimeseries more than value from previous command
+# relaunch victoriametrics with search.maxExportSeries more than value from previous command
 ```
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
@@ -1835,6 +1835,12 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      The maximum number of concurrent search requests. It shouldn't be high, since a single request can saturate all the CPU cores. See also -search.maxQueueDuration (default 8)
   -search.maxExportDuration duration
      The maximum duration for /api/v1/export call (default 720h0m0s)
+  -search.maxExportSeries int
+     The maximum number of time series, which can be returned from /api/v1/export* APIs. This option allows limiting memory usage (default 1000000)
+  -search.maxFederateSeries int
+     The maximum number of time series, which can be returned from /federate. This option allows limiting memory usage (default 300000)
+  -search.maxGraphiteSeries int
+     The maximum number of time series, which can be scanned during queries to Graphite Render API. See https://docs.victoriametrics.com/#graphite-render-api-usage (default 300000)
   -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
@@ -1850,12 +1856,16 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      The maximum number of raw samples a single query can process across all time series. This protects from heavy queries, which select unexpectedly high number of raw samples. See also -search.maxSamplesPerSeries (default 1000000000)
   -search.maxSamplesPerSeries int
      The maximum number of raw samples a single query can scan per each time series. This option allows limiting memory usage (default 30000000)
+  -search.maxSeries int
+     The maximum number of time series, which can be returned from /api/v1/series. This option allows limiting memory usage (default 10000)
   -search.maxStalenessInterval duration
      The maximum interval for staleness calculations. By default it is automatically calculated from the median interval between samples. This flag could be useful for tuning Prometheus data model closer to Influx-style data model. See https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness for details. See also '-search.maxLookback' flag, which has the same meaning due to historical reasons
   -search.maxStatusRequestDuration duration
      The maximum duration for /api/v1/status/* requests (default 5m0s)
   -search.maxStepForPointsAdjustment duration
      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 (default 1m0s)
+  -search.maxTSDBStatusSeries int
+     The maximum number of time series, which can be processed during the call to /api/v1/status/tsdb. This option allows limiting memory usage (default 1000000)
   -search.maxTagKeys int
      The maximum number of tag keys returned from /api/v1/labels (default 100000)
   -search.maxTagValueSuffixesPerSearch int
@@ -1863,7 +1873,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
   -search.maxTagValues int
      The maximum number of tag values returned from /api/v1/label/<label_name>/values (default 100000)
   -search.maxUniqueTimeseries int
-     The maximum number of unique time series each search can scan. This option allows limiting memory usage (default 300000)
+     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 (default 300000)
   -search.minStalenessInterval duration
      The minimum interval for staleness calculations. This flag could be useful for removing gaps on graphs generated from time series with irregular intervals between samples. See also '-search.maxStalenessInterval'
   -search.noStaleMarkers
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index e0d6e3a65..cf8aa1a6e 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -824,13 +824,13 @@ Send a request to `http://<victoriametrics-addr>:8428/api/v1/export/native?match
 where `<timeseries_selector_for_export>` may contain any [time series selector](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors)
 for metrics to export. Use `{__name__=~".*"}` selector for fetching all the time series.
 
-On large databases you may experience problems with limit on unique timeseries (default value is 300000). In this case you need to adjust `-search.maxUniqueTimeseries` parameter:
+On large databases you may experience problems with limit on the number of time series, which can be exported. In this case you need to adjust `-search.maxExportSeries` command-line flag:
 
 ```bash
 # count unique timeseries in database
 wget -O- -q 'http://your_victoriametrics_instance:8428/api/v1/series/count' | jq '.data[0]'
 
-# relaunch victoriametrics with search.maxUniqueTimeseries more than value from previous command
+# relaunch victoriametrics with search.maxExportSeries more than value from previous command
 ```
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
@@ -1839,6 +1839,12 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      The maximum number of concurrent search requests. It shouldn't be high, since a single request can saturate all the CPU cores. See also -search.maxQueueDuration (default 8)
   -search.maxExportDuration duration
      The maximum duration for /api/v1/export call (default 720h0m0s)
+  -search.maxExportSeries int
+     The maximum number of time series, which can be returned from /api/v1/export* APIs. This option allows limiting memory usage (default 1000000)
+  -search.maxFederateSeries int
+     The maximum number of time series, which can be returned from /federate. This option allows limiting memory usage (default 300000)
+  -search.maxGraphiteSeries int
+     The maximum number of time series, which can be scanned during queries to Graphite Render API. See https://docs.victoriametrics.com/#graphite-render-api-usage (default 300000)
   -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
@@ -1854,12 +1860,16 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      The maximum number of raw samples a single query can process across all time series. This protects from heavy queries, which select unexpectedly high number of raw samples. See also -search.maxSamplesPerSeries (default 1000000000)
   -search.maxSamplesPerSeries int
      The maximum number of raw samples a single query can scan per each time series. This option allows limiting memory usage (default 30000000)
+  -search.maxSeries int
+     The maximum number of time series, which can be returned from /api/v1/series. This option allows limiting memory usage (default 10000)
   -search.maxStalenessInterval duration
      The maximum interval for staleness calculations. By default it is automatically calculated from the median interval between samples. This flag could be useful for tuning Prometheus data model closer to Influx-style data model. See https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness for details. See also '-search.maxLookback' flag, which has the same meaning due to historical reasons
   -search.maxStatusRequestDuration duration
      The maximum duration for /api/v1/status/* requests (default 5m0s)
   -search.maxStepForPointsAdjustment duration
      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 (default 1m0s)
+  -search.maxTSDBStatusSeries int
+     The maximum number of time series, which can be processed during the call to /api/v1/status/tsdb. This option allows limiting memory usage (default 1000000)
   -search.maxTagKeys int
      The maximum number of tag keys returned from /api/v1/labels (default 100000)
   -search.maxTagValueSuffixesPerSearch int
@@ -1867,7 +1877,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
   -search.maxTagValues int
      The maximum number of tag values returned from /api/v1/label/<label_name>/values (default 100000)
   -search.maxUniqueTimeseries int
-     The maximum number of unique time series each search can scan. This option allows limiting memory usage (default 300000)
+     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 (default 300000)
   -search.minStalenessInterval duration
      The minimum interval for staleness calculations. This flag could be useful for removing gaps on graphs generated from time series with irregular intervals between samples. See also '-search.maxStalenessInterval'
   -search.noStaleMarkers
diff --git a/lib/lrucache/lrucache.go b/lib/lrucache/lrucache.go
new file mode 100644
index 000000000..c52e20850
--- /dev/null
+++ b/lib/lrucache/lrucache.go
@@ -0,0 +1,327 @@
+package lrucache
+
+import (
+	"container/heap"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
+	xxhash "github.com/cespare/xxhash/v2"
+)
+
+// Cache caches Entry entries.
+//
+// Call NewCache() for creating new Cache.
+type Cache struct {
+	shards []*cache
+
+	cleanerMustStopCh chan struct{}
+	cleanerStoppedCh  chan struct{}
+}
+
+// NewCache creates new cache.
+//
+// Cache size in bytes is limited by the value returned by getMaxSizeBytes() callback.
+// Call MustStop() in order to free up resources occupied by Cache.
+func NewCache(getMaxSizeBytes func() int) *Cache {
+	cpusCount := cgroup.AvailableCPUs()
+	shardsCount := cgroup.AvailableCPUs()
+	// Increase the number of shards with the increased number of available CPU cores.
+	// This should reduce contention on per-shard mutexes.
+	multiplier := cpusCount
+	if multiplier > 16 {
+		multiplier = 16
+	}
+	shardsCount *= multiplier
+	shards := make([]*cache, shardsCount)
+	getMaxShardBytes := func() int {
+		n := getMaxSizeBytes()
+		return n / shardsCount
+	}
+	for i := range shards {
+		shards[i] = newCache(getMaxShardBytes)
+	}
+	c := &Cache{
+		shards:            shards,
+		cleanerMustStopCh: make(chan struct{}),
+		cleanerStoppedCh:  make(chan struct{}),
+	}
+	go c.cleaner()
+	return c
+}
+
+// MustStop frees up resources occupied by c.
+func (c *Cache) MustStop() {
+	close(c.cleanerMustStopCh)
+	<-c.cleanerStoppedCh
+}
+
+// GetEntry returns an Entry for the given key k from c.
+func (c *Cache) GetEntry(k string) Entry {
+	idx := uint64(0)
+	if len(c.shards) > 1 {
+		h := hashUint64(k)
+		idx = h % uint64(len(c.shards))
+	}
+	shard := c.shards[idx]
+	return shard.GetEntry(k)
+}
+
+// PutEntry puts the given Entry e under the given key k into c.
+func (c *Cache) PutEntry(k string, e Entry) {
+	idx := uint64(0)
+	if len(c.shards) > 1 {
+		h := hashUint64(k)
+		idx = h % uint64(len(c.shards))
+	}
+	shard := c.shards[idx]
+	shard.PutEntry(k, e)
+}
+
+// Len returns the number of blocks in the cache c.
+func (c *Cache) Len() int {
+	n := 0
+	for _, shard := range c.shards {
+		n += shard.Len()
+	}
+	return n
+}
+
+// SizeBytes returns an approximate size in bytes of all the blocks stored in the cache c.
+func (c *Cache) SizeBytes() int {
+	n := 0
+	for _, shard := range c.shards {
+		n += shard.SizeBytes()
+	}
+	return n
+}
+
+// SizeMaxBytes returns the max allowed size in bytes for c.
+func (c *Cache) SizeMaxBytes() int {
+	n := 0
+	for _, shard := range c.shards {
+		n += shard.SizeMaxBytes()
+	}
+	return n
+}
+
+// Requests returns the number of requests served by c.
+func (c *Cache) Requests() uint64 {
+	n := uint64(0)
+	for _, shard := range c.shards {
+		n += shard.Requests()
+	}
+	return n
+}
+
+// Misses returns the number of cache misses for c.
+func (c *Cache) Misses() uint64 {
+	n := uint64(0)
+	for _, shard := range c.shards {
+		n += shard.Misses()
+	}
+	return n
+}
+
+func (c *Cache) cleaner() {
+	ticker := time.NewTicker(53 * time.Second)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-c.cleanerMustStopCh:
+			close(c.cleanerStoppedCh)
+			return
+		case <-ticker.C:
+			c.cleanByTimeout()
+		}
+	}
+}
+
+func (c *Cache) cleanByTimeout() {
+	for _, shard := range c.shards {
+		shard.cleanByTimeout()
+	}
+}
+
+type cache struct {
+	// Atomically updated fields must go first in the struct, so they are properly
+	// aligned to 8 bytes on 32-bit architectures.
+	// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/212
+	requests uint64
+	misses   uint64
+
+	// sizeBytes contains an approximate size for all the blocks stored in the cache.
+	sizeBytes int64
+
+	// getMaxSizeBytes() is a callback, which returns the maximum allowed cache size in bytes.
+	getMaxSizeBytes func() int
+
+	// mu protects all the fields below.
+	mu sync.Mutex
+
+	// m contains cached entries
+	m map[string]*cacheEntry
+
+	// The heap for removing the least recently used entries from m.
+	lah lastAccessHeap
+}
+
+func hashUint64(s string) uint64 {
+	b := bytesutil.ToUnsafeBytes(s)
+	return xxhash.Sum64(b)
+}
+
+// Entry is an item, which may be cached in the Cache.
+type Entry interface {
+	// SizeBytes must return the approximate size of the given entry in bytes
+	SizeBytes() int
+}
+
+type cacheEntry struct {
+	// The timestamp in seconds for the last access to the given entry.
+	lastAccessTime uint64
+
+	// heapIdx is the index for the entry in lastAccessHeap.
+	heapIdx int
+
+	// k contains the associated key for the given entry.
+	k string
+
+	// e contains the cached entry.
+	e Entry
+}
+
+func newCache(getMaxSizeBytes func() int) *cache {
+	var c cache
+	c.getMaxSizeBytes = getMaxSizeBytes
+	c.m = make(map[string]*cacheEntry)
+	return &c
+}
+
+func (c *cache) updateSizeBytes(n int) {
+	atomic.AddInt64(&c.sizeBytes, int64(n))
+}
+
+func (c *cache) cleanByTimeout() {
+	// Delete items accessed more than three minutes ago.
+	// This time should be enough for repeated queries.
+	lastAccessTime := fasttime.UnixTimestamp() - 3*60
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	for len(c.lah) > 0 {
+		if lastAccessTime < c.lah[0].lastAccessTime {
+			break
+		}
+		c.removeLeastRecentlyAccessedItem()
+	}
+}
+
+func (c *cache) GetEntry(k string) Entry {
+	atomic.AddUint64(&c.requests, 1)
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	ce := c.m[k]
+	if ce == nil {
+		atomic.AddUint64(&c.misses, 1)
+		return nil
+	}
+	currentTime := fasttime.UnixTimestamp()
+	if ce.lastAccessTime != currentTime {
+		ce.lastAccessTime = currentTime
+		heap.Fix(&c.lah, ce.heapIdx)
+	}
+	return ce.e
+}
+
+func (c *cache) PutEntry(k string, e Entry) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	ce := c.m[k]
+	if ce != nil {
+		// The entry has been already registered by concurrent goroutine.
+		return
+	}
+	ce = &cacheEntry{
+		lastAccessTime: fasttime.UnixTimestamp(),
+		k:              k,
+		e:              e,
+	}
+	heap.Push(&c.lah, ce)
+	c.m[k] = ce
+	c.updateSizeBytes(e.SizeBytes())
+	maxSizeBytes := c.getMaxSizeBytes()
+	for c.SizeBytes() > maxSizeBytes && len(c.lah) > 0 {
+		c.removeLeastRecentlyAccessedItem()
+	}
+}
+
+func (c *cache) removeLeastRecentlyAccessedItem() {
+	ce := c.lah[0]
+	c.updateSizeBytes(-ce.e.SizeBytes())
+	delete(c.m, ce.k)
+	heap.Pop(&c.lah)
+}
+
+func (c *cache) Len() int {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	return len(c.m)
+}
+
+func (c *cache) SizeBytes() int {
+	return int(atomic.LoadInt64(&c.sizeBytes))
+}
+
+func (c *cache) SizeMaxBytes() int {
+	return c.getMaxSizeBytes()
+}
+
+func (c *cache) Requests() uint64 {
+	return atomic.LoadUint64(&c.requests)
+}
+
+func (c *cache) Misses() uint64 {
+	return atomic.LoadUint64(&c.misses)
+}
+
+// lastAccessHeap implements heap.Interface
+type lastAccessHeap []*cacheEntry
+
+func (lah *lastAccessHeap) Len() int {
+	return len(*lah)
+}
+func (lah *lastAccessHeap) Swap(i, j int) {
+	h := *lah
+	a := h[i]
+	b := h[j]
+	a.heapIdx = j
+	b.heapIdx = i
+	h[i] = b
+	h[j] = a
+}
+func (lah *lastAccessHeap) Less(i, j int) bool {
+	h := *lah
+	return h[i].lastAccessTime < h[j].lastAccessTime
+}
+func (lah *lastAccessHeap) Push(x interface{}) {
+	e := x.(*cacheEntry)
+	h := *lah
+	e.heapIdx = len(h)
+	*lah = append(h, e)
+}
+func (lah *lastAccessHeap) Pop() interface{} {
+	h := *lah
+	e := h[len(h)-1]
+
+	// Remove the reference to deleted entry, so Go GC could free up memory occupied by the deleted entry.
+	h[len(h)-1] = nil
+
+	*lah = h[:len(h)-1]
+	return e
+}
diff --git a/lib/storage/index_db.go b/lib/storage/index_db.go
index b470db59a..bab0783d1 100644
--- a/lib/storage/index_db.go
+++ b/lib/storage/index_db.go
@@ -1315,9 +1315,9 @@ func (is *indexSearch) getSeriesCount() (uint64, error) {
 }
 
 // GetTSDBStatusWithFiltersForDate returns topN entries for tsdb status for the given tfss and the given date.
-func (db *indexDB) GetTSDBStatusWithFiltersForDate(tfss []*TagFilters, date uint64, topN int, deadline uint64) (*TSDBStatus, error) {
+func (db *indexDB) GetTSDBStatusWithFiltersForDate(tfss []*TagFilters, date uint64, topN, maxMetrics int, deadline uint64) (*TSDBStatus, error) {
 	is := db.getIndexSearch(deadline)
-	status, err := is.getTSDBStatusWithFiltersForDate(tfss, date, topN)
+	status, err := is.getTSDBStatusWithFiltersForDate(tfss, date, topN, maxMetrics)
 	db.putIndexSearch(is)
 	if err != nil {
 		return nil, err
@@ -1327,7 +1327,7 @@ func (db *indexDB) GetTSDBStatusWithFiltersForDate(tfss []*TagFilters, date uint
 	}
 	ok := db.doExtDB(func(extDB *indexDB) {
 		is := extDB.getIndexSearch(deadline)
-		status, err = is.getTSDBStatusWithFiltersForDate(tfss, date, topN)
+		status, err = is.getTSDBStatusWithFiltersForDate(tfss, date, topN, maxMetrics)
 		extDB.putIndexSearch(is)
 	})
 	if ok && err != nil {
@@ -1337,14 +1337,14 @@ func (db *indexDB) GetTSDBStatusWithFiltersForDate(tfss []*TagFilters, date uint
 }
 
 // getTSDBStatusWithFiltersForDate returns topN entries for tsdb status for the given tfss and the given date.
-func (is *indexSearch) getTSDBStatusWithFiltersForDate(tfss []*TagFilters, date uint64, topN int) (*TSDBStatus, error) {
+func (is *indexSearch) getTSDBStatusWithFiltersForDate(tfss []*TagFilters, date uint64, topN, maxMetrics int) (*TSDBStatus, error) {
 	var filter *uint64set.Set
 	if len(tfss) > 0 {
 		tr := TimeRange{
 			MinTimestamp: int64(date) * msecPerDay,
 			MaxTimestamp: int64(date+1) * msecPerDay,
 		}
-		metricIDs, err := is.searchMetricIDsInternal(tfss, tr, 2e9)
+		metricIDs, err := is.searchMetricIDsInternal(tfss, tr, maxMetrics)
 		if err != nil {
 			return nil, err
 		}
diff --git a/lib/storage/search.go b/lib/storage/search.go
index 7b9e15dc1..ff62d0f7c 100644
--- a/lib/storage/search.go
+++ b/lib/storage/search.go
@@ -226,17 +226,27 @@ func (s *Search) NextMetricBlock() bool {
 
 // SearchQuery is used for sending search queries from vmselect to vmstorage.
 type SearchQuery struct {
+	// The time range for searching time series
 	MinTimestamp int64
 	MaxTimestamp int64
-	TagFilterss  [][]TagFilter
+
+	// Tag filters for the search query
+	TagFilterss [][]TagFilter
+
+	// The maximum number of time series the search query can return.
+	MaxMetrics int
 }
 
 // NewSearchQuery creates new search query for the given args.
-func NewSearchQuery(start, end int64, tagFilterss [][]TagFilter) *SearchQuery {
+func NewSearchQuery(start, end int64, tagFilterss [][]TagFilter, maxMetrics int) *SearchQuery {
+	if maxMetrics <= 0 {
+		maxMetrics = 2e9
+	}
 	return &SearchQuery{
 		MinTimestamp: start,
 		MaxTimestamp: end,
 		TagFilterss:  tagFilterss,
+		MaxMetrics:   maxMetrics,
 	}
 }
 
diff --git a/lib/storage/storage.go b/lib/storage/storage.go
index 0de5c1d97..848239b6c 100644
--- a/lib/storage/storage.go
+++ b/lib/storage/storage.go
@@ -1468,8 +1468,8 @@ func (s *Storage) GetSeriesCount(deadline uint64) (uint64, error) {
 }
 
 // GetTSDBStatusWithFiltersForDate returns TSDB status data for /api/v1/status/tsdb with match[] filters.
-func (s *Storage) GetTSDBStatusWithFiltersForDate(tfss []*TagFilters, date uint64, topN int, deadline uint64) (*TSDBStatus, error) {
-	return s.idb().GetTSDBStatusWithFiltersForDate(tfss, date, topN, deadline)
+func (s *Storage) GetTSDBStatusWithFiltersForDate(tfss []*TagFilters, date uint64, topN, maxMetrics int, deadline uint64) (*TSDBStatus, error) {
+	return s.idb().GetTSDBStatusWithFiltersForDate(tfss, date, topN, maxMetrics, deadline)
 }
 
 // MetricRow is a metric to insert into storage.
diff --git a/lib/storage/tag_filters.go b/lib/storage/tag_filters.go
index fc190969b..c22a67f30 100644
--- a/lib/storage/tag_filters.go
+++ b/lib/storage/tag_filters.go
@@ -9,8 +9,11 @@ import (
 	"strings"
 	"sync"
 	"sync/atomic"
+	"unsafe"
 
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/lrucache"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
 )
 
@@ -471,42 +474,42 @@ func (tf *tagFilter) matchSuffix(b []byte) (bool, error) {
 
 // RegexpCacheSize returns the number of cached regexps for tag filters.
 func RegexpCacheSize() int {
-	regexpCacheLock.RLock()
-	n := len(regexpCacheMap)
-	regexpCacheLock.RUnlock()
-	return n
+	return regexpCache.Len()
 }
 
-// RegexpCacheRequests returns the number of requests to regexp cache.
+// RegexpCacheSizeBytes returns an approximate size in bytes for the cached regexps for tag filters.
+func RegexpCacheSizeBytes() int {
+	return regexpCache.SizeBytes()
+}
+
+// RegexpCacheMaxSizeBytes returns the maximum size in bytes for the cached regexps for tag filters.
+func RegexpCacheMaxSizeBytes() int {
+	return regexpCache.SizeMaxBytes()
+}
+
+// RegexpCacheRequests returns the number of requests to regexp cache for tag filters.
 func RegexpCacheRequests() uint64 {
-	return atomic.LoadUint64(&regexpCacheRequests)
+	return regexpCache.Requests()
 }
 
-// RegexpCacheMisses returns the number of cache misses for regexp cache.
+// RegexpCacheMisses returns the number of cache misses for regexp cache for tag filters.
 func RegexpCacheMisses() uint64 {
-	return atomic.LoadUint64(&regexpCacheMisses)
+	return regexpCache.Misses()
 }
 
-func getRegexpFromCache(expr []byte) (regexpCacheValue, error) {
-	atomic.AddUint64(&regexpCacheRequests, 1)
-
-	regexpCacheLock.RLock()
-	rcv, ok := regexpCacheMap[string(expr)]
-	regexpCacheLock.RUnlock()
-	if ok {
+func getRegexpFromCache(expr []byte) (*regexpCacheValue, error) {
+	if rcv := regexpCache.GetEntry(bytesutil.ToUnsafeString(expr)); rcv != nil {
 		// Fast path - the regexp found in the cache.
-		return rcv, nil
+		return rcv.(*regexpCacheValue), nil
 	}
-
 	// Slow path - build the regexp.
-	atomic.AddUint64(&regexpCacheMisses, 1)
 	exprOrig := string(expr)
 
 	expr = []byte(tagCharsRegexpEscaper.Replace(exprOrig))
 	exprStr := fmt.Sprintf("^(%s)$", expr)
 	re, err := regexp.Compile(exprStr)
 	if err != nil {
-		return rcv, fmt.Errorf("invalid regexp %q: %w", exprStr, err)
+		return nil, fmt.Errorf("invalid regexp %q: %w", exprStr, err)
 	}
 
 	sExpr := string(expr)
@@ -521,26 +524,16 @@ func getRegexpFromCache(expr []byte) (regexpCacheValue, error) {
 	}
 
 	// Put the reMatch in the cache.
+	var rcv regexpCacheValue
 	rcv.orValues = orValues
 	rcv.reMatch = reMatch
 	rcv.reCost = reCost
 	rcv.literalSuffix = literalSuffix
+	// heuristic for rcv in-memory size
+	rcv.sizeBytes = 8*len(exprOrig) + len(literalSuffix)
+	regexpCache.PutEntry(exprOrig, &rcv)
 
-	regexpCacheLock.Lock()
-	if overflow := len(regexpCacheMap) - getMaxRegexpCacheSize(); overflow > 0 {
-		overflow = int(float64(len(regexpCacheMap)) * 0.1)
-		for k := range regexpCacheMap {
-			delete(regexpCacheMap, k)
-			overflow--
-			if overflow <= 0 {
-				break
-			}
-		}
-	}
-	regexpCacheMap[exprOrig] = rcv
-	regexpCacheLock.Unlock()
-
-	return rcv, nil
+	return &rcv, nil
 }
 
 func newMatchFuncForOrSuffixes(orValues []string) (reMatch func(b []byte) bool, reCost uint64) {
@@ -888,11 +881,7 @@ var tagCharsReverseRegexpEscaper = strings.NewReplacer(
 
 func getMaxRegexpCacheSize() int {
 	maxRegexpCacheSizeOnce.Do(func() {
-		n := memory.Allowed() / 1024 / 1024
-		if n < 100 {
-			n = 100
-		}
-		maxRegexpCacheSize = n
+		maxRegexpCacheSize = int(0.05 * float64(memory.Allowed()))
 	})
 	return maxRegexpCacheSize
 }
@@ -903,11 +892,7 @@ var (
 )
 
 var (
-	regexpCacheMap  = make(map[string]regexpCacheValue)
-	regexpCacheLock sync.RWMutex
-
-	regexpCacheRequests uint64
-	regexpCacheMisses   uint64
+	regexpCache = lrucache.NewCache(getMaxRegexpCacheSize)
 )
 
 type regexpCacheValue struct {
@@ -915,15 +900,18 @@ type regexpCacheValue struct {
 	reMatch       func(b []byte) bool
 	reCost        uint64
 	literalSuffix string
+	sizeBytes     int
+}
+
+// SizeBytes implements lrucache.Entry interface
+func (rcv *regexpCacheValue) SizeBytes() int {
+	return rcv.sizeBytes
 }
 
 func getRegexpPrefix(b []byte) ([]byte, []byte) {
 	// Fast path - search the prefix in the cache.
-	prefixesCacheLock.RLock()
-	ps, ok := prefixesCacheMap[string(b)]
-	prefixesCacheLock.RUnlock()
-
-	if ok {
+	if ps := prefixesCache.GetEntry(bytesutil.ToUnsafeString(b)); ps != nil {
+		ps := ps.(*prefixSuffix)
 		return ps.prefix, ps.suffix
 	}
 
@@ -931,33 +919,18 @@ func getRegexpPrefix(b []byte) ([]byte, []byte) {
 	prefix, suffix := extractRegexpPrefix(b)
 
 	// Put the prefix and the suffix to the cache.
-	prefixesCacheLock.Lock()
-	if overflow := len(prefixesCacheMap) - getMaxPrefixesCacheSize(); overflow > 0 {
-		overflow = int(float64(len(prefixesCacheMap)) * 0.1)
-		for k := range prefixesCacheMap {
-			delete(prefixesCacheMap, k)
-			overflow--
-			if overflow <= 0 {
-				break
-			}
-		}
-	}
-	prefixesCacheMap[string(b)] = prefixSuffix{
+	ps := &prefixSuffix{
 		prefix: prefix,
 		suffix: suffix,
 	}
-	prefixesCacheLock.Unlock()
+	prefixesCache.PutEntry(string(b), ps)
 
 	return prefix, suffix
 }
 
 func getMaxPrefixesCacheSize() int {
 	maxPrefixesCacheSizeOnce.Do(func() {
-		n := memory.Allowed() / 1024 / 1024
-		if n < 100 {
-			n = 100
-		}
-		maxPrefixesCacheSize = n
+		maxPrefixesCacheSize = int(0.05 * float64(memory.Allowed()))
 	})
 	return maxPrefixesCacheSize
 }
@@ -968,15 +941,44 @@ var (
 )
 
 var (
-	prefixesCacheMap  = make(map[string]prefixSuffix)
-	prefixesCacheLock sync.RWMutex
+	prefixesCache = lrucache.NewCache(getMaxPrefixesCacheSize)
 )
 
+// RegexpPrefixesCacheSize returns the number of cached regexp prefixes for tag filters.
+func RegexpPrefixesCacheSize() int {
+	return prefixesCache.Len()
+}
+
+// RegexpPrefixesCacheSizeBytes returns an approximate size in bytes for cached regexp prefixes for tag filters.
+func RegexpPrefixesCacheSizeBytes() int {
+	return prefixesCache.SizeBytes()
+}
+
+// RegexpPrefixesCacheMaxSizeBytes returns the maximum size in bytes for cached regexp prefixes for tag filters in bytes.
+func RegexpPrefixesCacheMaxSizeBytes() int {
+	return prefixesCache.SizeMaxBytes()
+}
+
+// RegexpPrefixesCacheRequests returns the number of requests to regexp prefixes cache.
+func RegexpPrefixesCacheRequests() uint64 {
+	return prefixesCache.Requests()
+}
+
+// RegexpPrefixesCacheMisses returns the number of cache misses for regexp prefixes cache.
+func RegexpPrefixesCacheMisses() uint64 {
+	return prefixesCache.Misses()
+}
+
 type prefixSuffix struct {
 	prefix []byte
 	suffix []byte
 }
 
+// SizeBytes implements lrucache.Entry interface
+func (ps *prefixSuffix) SizeBytes() int {
+	return cap(ps.prefix) + cap(ps.suffix) + int(unsafe.Sizeof(*ps))
+}
+
 func extractRegexpPrefix(b []byte) ([]byte, []byte) {
 	sre, err := syntax.Parse(string(b), syntax.Perl)
 	if err != nil {


Test patch:
diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go
index 508b11d37..b6b5eb525 100644
--- a/app/vmselect/promql/exec_test.go
+++ b/app/vmselect/promql/exec_test.go
@@ -61,6 +61,7 @@ func TestExecSuccess(t *testing.T) {
 			Start:       start,
 			End:         end,
 			Step:        step,
+			MaxSeries:   1000,
 			Deadline:    searchutils.NewDeadline(time.Now(), time.Minute, ""),
 			RoundDigits: 100,
 		}
@@ -7496,6 +7497,7 @@ func TestExecError(t *testing.T) {
 			Start:       1000,
 			End:         2000,
 			Step:        100,
+			MaxSeries:   1000,
 			Deadline:    searchutils.NewDeadline(time.Now(), time.Minute, ""),
 			RoundDigits: 100,
 		}
diff --git a/lib/blockcache/blockcache_test.go b/lib/blockcache/blockcache_test.go
index 362f9cf59..842516c10 100644
--- a/lib/blockcache/blockcache_test.go
+++ b/lib/blockcache/blockcache_test.go
@@ -133,20 +133,20 @@ func TestCacheConcurrentAccess(t *testing.T) {
 	var wg sync.WaitGroup
 	wg.Add(workers)
 	for i := 0; i < workers; i++ {
-		go func() {
+		go func(worker int) {
 			defer wg.Done()
-			testCacheSetGet(c)
-		}()
+			testCacheSetGet(c, worker)
+		}(i)
 	}
 	wg.Wait()
 }
 
-func testCacheSetGet(c *Cache) {
+func testCacheSetGet(c *Cache, worker int) {
 	for i := 0; i < 1000; i++ {
 		part := (interface{})(i)
 		b := testBlock{}
 		k := Key{
-			Offset: uint64(i),
+			Offset: uint64(worker*1000 + i),
 			Part:   part,
 		}
 		c.PutBlock(k, &b)
diff --git a/lib/lrucache/lrucache_test.go b/lib/lrucache/lrucache_test.go
new file mode 100644
index 000000000..160deb86e
--- /dev/null
+++ b/lib/lrucache/lrucache_test.go
@@ -0,0 +1,126 @@
+package lrucache
+
+import (
+	"fmt"
+	"sync"
+	"testing"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
+)
+
+func TestCache(t *testing.T) {
+	sizeMaxBytes := 64 * 1024
+	// Multiply sizeMaxBytes by the square of available CPU cores
+	// in order to get proper distribution of sizes between cache shards.
+	// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2204
+	cpus := cgroup.AvailableCPUs()
+	sizeMaxBytes *= cpus * cpus
+	getMaxSize := func() int {
+		return sizeMaxBytes
+	}
+	c := NewCache(getMaxSize)
+	defer c.MustStop()
+	if n := c.SizeBytes(); n != 0 {
+		t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, 0)
+	}
+	if n := c.SizeMaxBytes(); n != sizeMaxBytes {
+		t.Fatalf("unexpected SizeMaxBytes(); got %d; want %d", n, sizeMaxBytes)
+	}
+	k := "foobar"
+	var e testEntry
+	entrySize := e.SizeBytes()
+	// Put a single entry into cache
+	c.PutEntry(k, &e)
+	if n := c.Len(); n != 1 {
+		t.Fatalf("unexpected number of items in the cache; got %d; want %d", n, 1)
+	}
+	if n := c.SizeBytes(); n != entrySize {
+		t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, entrySize)
+	}
+	if n := c.Requests(); n != 0 {
+		t.Fatalf("unexpected number of requests; got %d; want %d", n, 0)
+	}
+	if n := c.Misses(); n != 0 {
+		t.Fatalf("unexpected number of misses; got %d; want %d", n, 0)
+	}
+	// Obtain this entry from the cache
+	if e1 := c.GetEntry(k); e1 != &e {
+		t.Fatalf("unexpected entry obtained; got %v; want %v", e1, &e)
+	}
+	if n := c.Requests(); n != 1 {
+		t.Fatalf("unexpected number of requests; got %d; want %d", n, 1)
+	}
+	if n := c.Misses(); n != 0 {
+		t.Fatalf("unexpected number of misses; got %d; want %d", n, 0)
+	}
+	// Obtain non-existing entry from the cache
+	if e1 := c.GetEntry("non-existing-key"); e1 != nil {
+		t.Fatalf("unexpected non-nil block obtained for non-existing key: %v", e1)
+	}
+	if n := c.Requests(); n != 2 {
+		t.Fatalf("unexpected number of requests; got %d; want %d", n, 2)
+	}
+	if n := c.Misses(); n != 1 {
+		t.Fatalf("unexpected number of misses; got %d; want %d", n, 1)
+	}
+	// Store the entry again.
+	c.PutEntry(k, &e)
+	if n := c.SizeBytes(); n != entrySize {
+		t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, entrySize)
+	}
+	if e1 := c.GetEntry(k); e1 != &e {
+		t.Fatalf("unexpected entry obtained; got %v; want %v", e1, &e)
+	}
+	if n := c.Requests(); n != 3 {
+		t.Fatalf("unexpected number of requests; got %d; want %d", n, 3)
+	}
+	if n := c.Misses(); n != 1 {
+		t.Fatalf("unexpected number of misses; got %d; want %d", n, 1)
+	}
+
+	// Manually clean the cache. The entry shouldn't be deleted because it was recently accessed.
+	c.cleanByTimeout()
+	if n := c.SizeBytes(); n != entrySize {
+		t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, entrySize)
+	}
+}
+
+func TestCacheConcurrentAccess(t *testing.T) {
+	const sizeMaxBytes = 16 * 1024 * 1024
+	getMaxSize := func() int {
+		return sizeMaxBytes
+	}
+	c := NewCache(getMaxSize)
+	defer c.MustStop()
+
+	workers := 5
+	var wg sync.WaitGroup
+	wg.Add(workers)
+	for i := 0; i < workers; i++ {
+		go func(worker int) {
+			defer wg.Done()
+			testCacheSetGet(c, worker)
+		}(i)
+	}
+	wg.Wait()
+}
+
+func testCacheSetGet(c *Cache, worker int) {
+	for i := 0; i < 1000; i++ {
+		e := testEntry{}
+		k := fmt.Sprintf("key_%d_%d", worker, i)
+		c.PutEntry(k, &e)
+		if e1 := c.GetEntry(k); e1 != &e {
+			panic(fmt.Errorf("unexpected entry obtained; got %v; want %v", e1, &e))
+		}
+		if e1 := c.GetEntry("non-existing-key"); e1 != nil {
+			panic(fmt.Errorf("unexpected non-nil entry obtained: %v", e1))
+		}
+	}
+}
+
+type testEntry struct{}
+
+func (tb *testEntry) SizeBytes() int {
+	return 42
+}
diff --git a/lib/storage/index_db_test.go b/lib/storage/index_db_test.go
index 72ecec2fa..0a147b447 100644
--- a/lib/storage/index_db_test.go
+++ b/lib/storage/index_db_test.go
@@ -1766,7 +1766,7 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
 	}
 
 	// Check GetTSDBStatusWithFiltersForDate with nil filters.
-	status, err := db.GetTSDBStatusWithFiltersForDate(nil, baseDate, 5, noDeadline)
+	status, err := db.GetTSDBStatusWithFiltersForDate(nil, baseDate, 5, 1e6, noDeadline)
 	if err != nil {
 		t.Fatalf("error in GetTSDBStatusWithFiltersForDate with nil filters: %s", err)
 	}
@@ -1834,7 +1834,7 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
 	if err := tfs.Add([]byte("day"), []byte("0"), false, false); err != nil {
 		t.Fatalf("cannot add filter: %s", err)
 	}
-	status, err = db.GetTSDBStatusWithFiltersForDate([]*TagFilters{tfs}, baseDate, 5, noDeadline)
+	status, err = db.GetTSDBStatusWithFiltersForDate([]*TagFilters{tfs}, baseDate, 5, 1e6, noDeadline)
 	if err != nil {
 		t.Fatalf("error in GetTSDBStatusWithFiltersForDate: %s", err)
 	}