Repository: VictoriaMetrics/VictoriaMetrics
Pull request #921 (2020-11-23 07:10:11)

Changes consul discovery api

codecov[bot]: # [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921?src=pr&el=h1) Report
> Merging [#921](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921?src=pr&el=desc) (e9e92ea) into [master](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/9d160f90489d10b3b6d5c427cd05c5f8d349f246?el=desc) (9d160f9) will **decrease** coverage by `0.15%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/graphs/tree.svg?width=650&height=150&src=pr&token=YND8dYVcXn)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #921      +/-   ##
==========================================
- Coverage   61.27%   61.11%   -0.16%     
==========================================
  Files         198      197       -1     
  Lines       22433    22401      -32     
==========================================
- Hits        13745    13691      -54     
- Misses       7370     7401      +31     
+ Partials     1318     1309       -9     
```


| [Impacted Files](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [lib/promscrape/discovery/consul/api.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-bGliL3Byb21zY3JhcGUvZGlzY292ZXJ5L2NvbnN1bC9hcGkuZ28=) | `0.00% <0.00%> (ø)` | |
| [lib/promscrape/discovery/consul/consul.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-bGliL3Byb21zY3JhcGUvZGlzY292ZXJ5L2NvbnN1bC9jb25zdWwuZ28=) | `0.00% <0.00%> (ø)` | |
| [lib/promscrape/discovery/consul/service\_node.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-bGliL3Byb21zY3JhcGUvZGlzY292ZXJ5L2NvbnN1bC9zZXJ2aWNlX25vZGUuZ28=) | `60.71% <0.00%> (+30.88%)` | :arrow_up: |
| [lib/promscrape/discovery/consul/watch.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-bGliL3Byb21zY3JhcGUvZGlzY292ZXJ5L2NvbnN1bC93YXRjaC5nbw==) | `0.00% <0.00%> (ø)` | |
| [lib/promscrape/scraper.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-bGliL3Byb21zY3JhcGUvc2NyYXBlci5nbw==) | `0.00% <0.00%> (ø)` | |
| [lib/fasttime/fasttime.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-bGliL2Zhc3R0aW1lL2Zhc3R0aW1lLmdv) | `37.50% <0.00%> (-37.50%)` | :arrow_down: |
| [lib/mergeset/block\_stream\_reader.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-bGliL21lcmdlc2V0L2Jsb2NrX3N0cmVhbV9yZWFkZXIuZ28=) | `49.30% <0.00%> (-13.20%)` | :arrow_down: |
| [app/vmselect/promql/aggr\_incremental.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-YXBwL3Ztc2VsZWN0L3Byb21xbC9hZ2dyX2luY3JlbWVudGFsLmdv) | `86.43% <0.00%> (-1.56%)` | :arrow_down: |
| [lib/mergeset/table.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-bGliL21lcmdlc2V0L3RhYmxlLmdv) | `65.83% <0.00%> (-1.25%)` | :arrow_down: |
| [lib/storage/partition.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvcGFydGl0aW9uLmdv) | `68.12% <0.00%> (-0.26%)` | :arrow_down: |
| ... and [8 more](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/921/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/README.md b/README.md
index 85675eb54..3386b7a4c 100644
--- a/README.md
+++ b/README.md
@@ -49,17 +49,22 @@ Click on a link in order to read the corresponding case study
 
 * VictoriaMetrics can be used as long-term storage for Prometheus or for [vmagent](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmagent/README.md).
   See [these docs](#prometheus-setup) for details.
-* Supports [Prometheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/), so it can be used as Prometheus drop-in replacement in Grafana.
-  VictoriaMetrics implements [MetricsQL](https://github.com/VictoriaMetrics/VictoriaMetrics/wiki/MetricsQL) query language, which inspired by PromQL. MetricsQL is backwards-compatible with PromQL.
-* Supports global query view. Multiple Prometheus instances or any other data sources may write data into VictoriaMetrics. Later this data may be queried in a single query.
+* VictoriaMetrics supports [Prometheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/), so it can be used as Prometheus drop-in replacement in Grafana.
+* VictoriaMetrics implements [MetricsQL](https://github.com/VictoriaMetrics/VictoriaMetrics/wiki/MetricsQL) query language backwards compatible with PromQL.
+* VictoriaMetrics provides global query view. Multiple Prometheus instances or any other data sources may ingest data into VictoriaMetrics.
+  Later this data may be queried via a single query.
 * High performance and good scalability for both [inserts](https://medium.com/@valyala/high-cardinality-tsdb-benchmarks-victoriametrics-vs-timescaledb-vs-influxdb-13e6ee64dd6b)
   and [selects](https://medium.com/@valyala/when-size-matters-benchmarking-victoriametrics-vs-timescale-and-influxdb-6035811952d4).
   [Outperforms InfluxDB and TimescaleDB by up to 20x](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
-* [Uses 10x less RAM than InfluxDB](https://medium.com/@valyala/insert-benchmarks-with-inch-influxdb-vs-victoriametrics-e31a41ae2893) when working with millions of unique time series (aka high cardinality).
+* [Uses 10x less RAM than InfluxDB](https://medium.com/@valyala/insert-benchmarks-with-inch-influxdb-vs-victoriametrics-e31a41ae2893)
+  and [up to 7x less RAM than Prometheus, Thanos or Cortex](https://valyala.medium.com/prometheus-vs-victoriametrics-benchmark-on-node-exporter-metrics-4ca29c75590f)
+  when dealing with millions of unique time series (aka high cardinality).
 * Optimized for time series with high churn rate. Think about [prometheus-operator](https://github.com/coreos/prometheus-operator) metrics from frequent deployments in Kubernetes.
 * High data compression, so [up to 70x more data points](https://medium.com/@valyala/when-size-matters-benchmarking-victoriametrics-vs-timescale-and-influxdb-6035811952d4)
-  may be crammed into limited storage comparing to TimescaleDB.
-* Optimized for storage with high-latency IO and low IOPS (HDD and network storage in AWS, Google Cloud, Microsoft Azure, etc). See [graphs from these benchmarks](https://medium.com/@valyala/high-cardinality-tsdb-benchmarks-victoriametrics-vs-timescaledb-vs-influxdb-13e6ee64dd6b).
+  may be crammed into limited storage comparing to TimescaleDB
+  and [up to 7x less storage space is required comparing to Prometheus, Thanos or Cortex](https://valyala.medium.com/prometheus-vs-victoriametrics-benchmark-on-node-exporter-metrics-4ca29c75590f).
+* Optimized for storage with high-latency IO and low IOPS (HDD and network storage in AWS, Google Cloud, Microsoft Azure, etc).
+  See [graphs from these benchmarks](https://medium.com/@valyala/high-cardinality-tsdb-benchmarks-victoriametrics-vs-timescaledb-vs-influxdb-13e6ee64dd6b).
 * A single-node VictoriaMetrics may substitute moderately sized clusters built with competing solutions such as Thanos, M3DB, Cortex, InfluxDB or TimescaleDB.
   See [vertical scalability benchmarks](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae),
   [comparing Thanos to VictoriaMetrics cluster](https://medium.com/@valyala/comparing-thanos-to-victoriametrics-cluster-b193bea1683)
@@ -68,7 +73,7 @@ Click on a link in order to read the corresponding case study
 * Easy operation:
   * VictoriaMetrics consists of a single [small executable](https://medium.com/@valyala/stripping-dependency-bloat-in-victoriametrics-docker-image-983fb5912b0d) without external dependencies.
   * All the configuration is done via explicit command-line flags with reasonable defaults.
-  * All the data is stored in a single directory pointed by `-storageDataPath` flag.
+  * All the data is stored in a single directory pointed by `-storageDataPath` command-line flag.
   * Easy and fast backups from [instant snapshots](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282)
   to S3 or GCS with [vmbackup](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmbackup/README.md) / [vmrestore](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmrestore/README.md).
   See [this article](https://medium.com/@valyala/speeding-up-backups-for-big-time-series-databases-533c1a927883) for more details.
@@ -91,6 +96,7 @@ Click on a link in order to read the corresponding case study
 * Has open source [cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster).
 * See also technical [Articles about VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/wiki/Articles).
 
+
 ## Operation
 
 ### Table of contents
@@ -175,7 +181,8 @@ The following command-line flags are used the most:
 
 Other flags have good enough default values, so set them only if you really need this. Pass `-help` to see all the available flags with description and default values.
 
-See how to [ingest data to VictoriaMetrics](#how-to-import-time-series-data) and how to [query VictoriaMetrics](#grafana-setup).
+See how to [ingest data to VictoriaMetrics](#how-to-import-time-series-data), how to [query VictoriaMetrics](#grafana-setup)
+and how to [handle alerts](#alerting).
 VictoriaMetrics accepts [Prometheus querying API requests](#prometheus-querying-api-usage) on port `8428` by default.
 
 It is recommended setting up [monitoring](#monitoring) for VictoriaMetrics.
@@ -242,8 +249,9 @@ Read more about tuning remote write for Prometheus [here](https://prometheus.io/
 
 It is recommended upgrading Prometheus to [v2.12.0](https://github.com/prometheus/prometheus/releases) or newer, since previous versions may have issues with `remote_write`.
 
-Take a look also at [vmagent](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmagent/README.md),
-which can be used as faster and less resource-hungry alternative to Prometheus in certain cases.
+Take a look also at [vmagent](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmagent/README.md)
+and [vmalert](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/README.md),
+which can be used as faster and less resource-hungry alternative to Prometheus.
 
 
 ## Grafana setup
@@ -419,7 +427,7 @@ Data sent to VictoriaMetrics via `Graphite plaintext protocol` may be read via t
 * [Prometheus querying API](#prometheus-querying-api-usage)
 * Metric names can be explored via [Graphite metrics API](#graphite-metrics-api-usage)
 * Tags can be explored via [Graphite tags API](#graphite-tags-api-usage)
-* [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/master/cmd/carbonapi/carbonapi.example.prometheus.yaml)
+* [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/main/cmd/carbonapi/carbonapi.example.victoriametrics.yaml)
 
 ## How to send data from OpenTSDB-compatible agents
 
@@ -566,6 +574,7 @@ VictoriaMetrics supports the following handlers from [Graphite Tags API](https:/
 * [/tags/findSeries](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags)
 * [/tags/autoComplete/tags](https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support)
 * [/tags/autoComplete/values](https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support)
+* [/tags/delSeries](https://graphite.readthedocs.io/en/stable/tags.html#removing-series-from-the-tagdb)
 
 
 ## How to build from sources
@@ -1340,7 +1349,7 @@ See also [high availability docs](#high-availability) and [backup docs](#backups
 
 VictoriaMetrics supports backups via [vmbackup](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmbackup/README.md)
 and [vmrestore](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmrestore/README.md) tools.
-We also provide provide `vmbackuper` tool for paid enterprise subscribers - see [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/466) for details.
+We also provide `vmbackuper` tool for paid enterprise subscribers - see [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/466) for details.
 
 
 ## Profiling
diff --git a/app/victoria-metrics/main.go b/app/victoria-metrics/main.go
index 130974f12..6349dd3f3 100644
--- a/app/victoria-metrics/main.go
+++ b/app/victoria-metrics/main.go
@@ -17,6 +17,7 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
 )
 
@@ -25,6 +26,8 @@ var (
 	minScrapeInterval = flag.Duration("dedup.minScrapeInterval", 0, "Remove superflouos samples from time series if they are located closer to each other than this duration. "+
 		"This may be useful for reducing overhead when multiple identically configured Prometheus instances write data to the same VictoriaMetrics. "+
 		"Deduplication is disabled if the -dedup.minScrapeInterval is 0")
+	dryRun = flag.Bool("dryRun", false, "Whether to check only -promscrape.config and then exit. "+
+		"Unknown config entries are allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse")
 )
 
 func main() {
@@ -34,6 +37,18 @@ func main() {
 	buildinfo.Init()
 	logger.Init()
 	cgroup.UpdateGOMAXPROCSToCPUQuota()
+
+	if promscrape.IsDryRun() {
+		*dryRun = true
+	}
+	if *dryRun {
+		if err := promscrape.CheckConfig(); err != nil {
+			logger.Fatalf("error when checking -promscrape.config: %s", err)
+		}
+		logger.Infof("-promscrape.config is ok; exitting with 0 status code")
+		return
+	}
+
 	logger.Infof("starting VictoriaMetrics at %q...", *httpListenAddr)
 	startTime := time.Now()
 	storage.SetMinScrapeIntervalForDeduplication(*minScrapeInterval)
diff --git a/app/vmagent/main.go b/app/vmagent/main.go
index 04c6cd8b2..df0fc9ee2 100644
--- a/app/vmagent/main.go
+++ b/app/vmagent/main.go
@@ -48,7 +48,8 @@ var (
 		"Usually :4242 must be set. Doesn't work if empty")
 	opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty")
 	dryRun                 = flag.Bool("dryRun", false, "Whether to check only config files without running vmagent. The following files are checked: "+
-		"-promscrape.config, -remoteWrite.relabelConfig, -remoteWrite.urlRelabelConfig . See also -promscrape.config.dryRun")
+		"-promscrape.config, -remoteWrite.relabelConfig, -remoteWrite.urlRelabelConfig . "+
+		"Unknown config entries are allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse")
 )
 
 var (
@@ -68,15 +69,19 @@ func main() {
 	logger.Init()
 	cgroup.UpdateGOMAXPROCSToCPUQuota()
 
-	if *dryRun {
-		if err := flag.Set("promscrape.config.strictParse", "true"); err != nil {
-			logger.Panicf("BUG: cannot set promscrape.config.strictParse=true: %s", err)
+	if promscrape.IsDryRun() {
+		if err := promscrape.CheckConfig(); err != nil {
+			logger.Fatalf("error when checking -promscrape.config: %s", err)
 		}
+		logger.Infof("-promscrape.config is ok; exitting with 0 status code")
+		return
+	}
+	if *dryRun {
 		if err := remotewrite.CheckRelabelConfigs(); err != nil {
 			logger.Fatalf("error when checking relabel configs: %s", err)
 		}
 		if err := promscrape.CheckConfig(); err != nil {
-			logger.Fatalf("error when checking Prometheus config: %s", err)
+			logger.Fatalf("error when checking -promscrape.config: %s", err)
 		}
 		logger.Infof("all the configs are ok; exitting with 0 status code")
 		return
diff --git a/app/vmalert/config/config.go b/app/vmalert/config/config.go
index 1ea260e4e..da998ca5c 100644
--- a/app/vmalert/config/config.go
+++ b/app/vmalert/config/config.go
@@ -95,7 +95,7 @@ type Rule struct {
 	Record      string            `yaml:"record,omitempty"`
 	Alert       string            `yaml:"alert,omitempty"`
 	Expr        string            `yaml:"expr"`
-	For         PromDuration      `yaml:"for,omitempty"`
+	For         PromDuration      `yaml:"for"`
 	Labels      map[string]string `yaml:"labels,omitempty"`
 	Annotations map[string]string `yaml:"annotations,omitempty"`
 
@@ -115,6 +115,11 @@ func NewPromDuration(d time.Duration) PromDuration {
 	}
 }
 
+// MarshalYAML implements yaml.Marshaler interface.
+func (pd PromDuration) MarshalYAML() (interface{}, error) {
+	return pd.Duration().String(), nil
+}
+
 // UnmarshalYAML implements yaml.Unmarshaler interface.
 func (pd *PromDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	var s string
diff --git a/app/vmbackup/main.go b/app/vmbackup/main.go
index bbaa2a77a..550e3fde8 100644
--- a/app/vmbackup/main.go
+++ b/app/vmbackup/main.go
@@ -43,7 +43,7 @@ func main() {
 	cgroup.UpdateGOMAXPROCSToCPUQuota()
 
 	if len(*snapshotCreateURL) > 0 {
-		logger.Infof("%s", "Snapshots enabled")
+		logger.Infof("Snapshots enabled")
 		logger.Infof("Snapshot create url %s", *snapshotCreateURL)
 		if len(*snapshotDeleteURL) <= 0 {
 			err := flag.Set("snapshot.deleteURL", strings.Replace(*snapshotCreateURL, "/create", "/delete", 1))
@@ -55,17 +55,17 @@ func main() {
 
 		name, err := snapshot.Create(*snapshotCreateURL)
 		if err != nil {
-			logger.Fatalf("%s", err)
+			logger.Fatalf("cannot create snapshot: %s", err)
 		}
 		err = flag.Set("snapshotName", name)
 		if err != nil {
-			logger.Fatalf("Failed to set snapshotName flag: %v", err)
+			logger.Fatalf("cannot set snapshotName flag: %v", err)
 		}
 
 		defer func() {
 			err := snapshot.Delete(*snapshotDeleteURL, name)
 			if err != nil {
-				logger.Fatalf("%s", err)
+				logger.Fatalf("cannot delete snapshot: %s", err)
 			}
 		}()
 	}
diff --git a/app/vmbackup/snapshot/snapshot.go b/app/vmbackup/snapshot/snapshot.go
index 852c0b3b4..199d21114 100644
--- a/app/vmbackup/snapshot/snapshot.go
+++ b/app/vmbackup/snapshot/snapshot.go
@@ -20,26 +20,27 @@ type snapshot struct {
 // Create creates a snapshot and the provided api endpoint and returns
 // the snapshot name
 func Create(createSnapshotURL string) (string, error) {
-	logger.Infof("%s", "Creating snapshot")
+	logger.Infof("Creating snapshot")
 	u, err := url.Parse(createSnapshotURL)
 	if err != nil {
 		return "", err
 	}
-
 	resp, err := http.Get(u.String())
 	if err != nil {
 		return "", err
 	}
-
 	body, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
 		return "", err
 	}
+	if resp.StatusCode != http.StatusOK {
+		return "", fmt.Errorf("unexpected status code returned from %q; expecting %d; got %d; response body: %q", createSnapshotURL, resp.StatusCode, http.StatusOK, body)
+	}
 
 	snap := snapshot{}
 	err = json.Unmarshal(body, &snap)
 	if err != nil {
-		return "", err
+		return "", fmt.Errorf("cannot parse JSON response from %q: %w; response body: %q", createSnapshotURL, err, body)
 	}
 
 	if snap.Status == "ok" {
@@ -58,26 +59,26 @@ func Delete(deleteSnapshotURL string, snapshotName string) error {
 	formData := url.Values{
 		"snapshot": {snapshotName},
 	}
-
 	u, err := url.Parse(deleteSnapshotURL)
 	if err != nil {
 		return err
 	}
-
 	resp, err := http.PostForm(u.String(), formData)
 	if err != nil {
 		return err
 	}
-
 	body, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
 		return err
 	}
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("unexpected status code returned from %q; expecting %d; got %d; response body: %q", deleteSnapshotURL, resp.StatusCode, http.StatusOK, body)
+	}
 
 	snap := snapshot{}
 	err = json.Unmarshal(body, &snap)
 	if err != nil {
-		return err
+		return fmt.Errorf("cannot parse JSON response from %q: %w; response body: %q", deleteSnapshotURL, err, body)
 	}
 
 	if snap.Status == "ok" {
diff --git a/app/vminsert/graphite/tags.go b/app/vminsert/graphite/tags.go
deleted file mode 100644
index fe49fcebf..000000000
--- a/app/vminsert/graphite/tags.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package graphite
-
-import (
-	"fmt"
-	"net/http"
-	"sort"
-	"time"
-
-	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
-	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
-	graphiteparser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/graphite"
-	"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
-	"github.com/VictoriaMetrics/metrics"
-)
-
-// TagsTagSeriesHandler implements /tags/tagSeries handler.
-//
-// See https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb
-func TagsTagSeriesHandler(w http.ResponseWriter, r *http.Request) error {
-	return registerMetrics(w, r, false)
-}
-
-// TagsTagMultiSeriesHandler implements /tags/tagMultiSeries handler.
-//
-// See https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb
-func TagsTagMultiSeriesHandler(w http.ResponseWriter, r *http.Request) error {
-	return registerMetrics(w, r, true)
-}
-
-func registerMetrics(w http.ResponseWriter, r *http.Request, isJSONResponse bool) error {
-	startTime := time.Now()
-	if err := r.ParseForm(); err != nil {
-		return fmt.Errorf("cannot parse form values: %w", err)
-	}
-	paths := r.Form["path"]
-	var row graphiteparser.Row
-	var labels []prompb.Label
-	var b []byte
-	var tagsPool []graphiteparser.Tag
-	mrs := make([]storage.MetricRow, len(paths))
-	ct := time.Now().UnixNano() / 1e6
-	canonicalPaths := make([]string, len(paths))
-	for i, path := range paths {
-		var err error
-		tagsPool, err = row.UnmarshalMetricAndTags(path, tagsPool[:0])
-		if err != nil {
-			return fmt.Errorf("cannot parse path=%q: %w", path, err)
-		}
-
-		// Construct canonical path according to https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb
-		sort.Slice(row.Tags, func(i, j int) bool {
-			return row.Tags[i].Key < row.Tags[j].Key
-		})
-		b = append(b[:0], row.Metric...)
-		for _, tag := range row.Tags {
-			b = append(b, ';')
-			b = append(b, tag.Key...)
-			b = append(b, '=')
-			b = append(b, tag.Value...)
-		}
-		canonicalPaths[i] = string(b)
-
-		// Convert parsed metric and tags to labels.
-		labels = append(labels[:0], prompb.Label{
-			Name:  []byte("__name__"),
-			Value: []byte(row.Metric),
-		})
-		for _, tag := range row.Tags {
-			labels = append(labels, prompb.Label{
-				Name:  []byte(tag.Key),
-				Value: []byte(tag.Value),
-			})
-		}
-
-		// Put labels with the current timestamp to MetricRow
-		mr := &mrs[i]
-		mr.MetricNameRaw = storage.MarshalMetricNameRaw(mr.MetricNameRaw[:0], labels)
-		mr.Timestamp = ct
-	}
-	if err := vmstorage.RegisterMetricNames(mrs); err != nil {
-		return fmt.Errorf("cannot register paths: %w", err)
-	}
-
-	// Return response
-	contentType := "text/plain; charset=utf-8"
-	if isJSONResponse {
-		contentType = "application/json; charset=utf-8"
-	}
-	w.Header().Set("Content-Type", contentType)
-	WriteTagsTagMultiSeriesResponse(w, canonicalPaths, isJSONResponse)
-	if isJSONResponse {
-		tagsTagMultiSeriesDuration.UpdateDuration(startTime)
-	} else {
-		tagsTagSeriesDuration.UpdateDuration(startTime)
-	}
-	return nil
-}
-
-var (
-	tagsTagSeriesDuration      = metrics.NewSummary(`vm_request_duration_seconds{path="/tags/tagSeries"}`)
-	tagsTagMultiSeriesDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/tags/tagMultiSeries"}`)
-)
diff --git a/app/vminsert/graphite/tags_tag_multi_series_response.qtpl.go b/app/vminsert/graphite/tags_tag_multi_series_response.qtpl.go
deleted file mode 100644
index 773a6ce4e..000000000
--- a/app/vminsert/graphite/tags_tag_multi_series_response.qtpl.go
+++ /dev/null
@@ -1,75 +0,0 @@
-// Code generated by qtc from "tags_tag_multi_series_response.qtpl". DO NOT EDIT.
-// See https://github.com/valyala/quicktemplate for details.
-
-// TagsTagMultiSeriesResponse generates response for /tags/tagMultiSeries .See https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb
-
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:5
-package graphite
-
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:5
-import (
-	qtio422016 "io"
-
-	qt422016 "github.com/valyala/quicktemplate"
-)
-
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:5
-var (
-	_ = qtio422016.Copy
-	_ = qt422016.AcquireByteBuffer
-)
-
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:5
-func StreamTagsTagMultiSeriesResponse(qw422016 *qt422016.Writer, canonicalPaths []string, isJSONResponse bool) {
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:6
-	if isJSONResponse {
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:6
-		qw422016.N().S(`[`)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:6
-	}
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:7
-	for i, path := range canonicalPaths {
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:8
-		qw422016.N().Q(path)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:9
-		if i+1 < len(canonicalPaths) {
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:9
-			qw422016.N().S(`,`)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:9
-		}
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:10
-	}
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:11
-	if isJSONResponse {
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:11
-		qw422016.N().S(`]`)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:11
-	}
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-}
-
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-func WriteTagsTagMultiSeriesResponse(qq422016 qtio422016.Writer, canonicalPaths []string, isJSONResponse bool) {
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-	StreamTagsTagMultiSeriesResponse(qw422016, canonicalPaths, isJSONResponse)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-	qt422016.ReleaseWriter(qw422016)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-}
-
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-func TagsTagMultiSeriesResponse(canonicalPaths []string, isJSONResponse bool) string {
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-	WriteTagsTagMultiSeriesResponse(qb422016, canonicalPaths, isJSONResponse)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-	qs422016 := string(qb422016.B)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-	return qs422016
-//line app/vminsert/graphite/tags_tag_multi_series_response.qtpl:12
-}
diff --git a/app/vminsert/main.go b/app/vminsert/main.go
index 1716b1c4e..3bfd7f2c3 100644
--- a/app/vminsert/main.go
+++ b/app/vminsert/main.go
@@ -40,7 +40,7 @@ var (
 		"Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. "+
 		"Usually :4242 must be set. Doesn't work if empty")
 	opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty")
-	maxLabelsPerTimeseries = flag.Int("maxLabelsPerTimeseries", 30, "The maximum number of labels accepted per time series. Superflouos labels are dropped")
+	maxLabelsPerTimeseries = flag.Int("maxLabelsPerTimeseries", 30, "The maximum number of labels accepted per time series. Superfluous labels are dropped")
 )
 
 var (
@@ -153,22 +153,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
 		influxQueryRequests.Inc()
 		fmt.Fprintf(w, `{"results":[{"series":[{"values":[]}]}]}`)
 		return true
-	case "/tags/tagSeries":
-		graphiteTagsTagSeriesRequests.Inc()
-		if err := graphite.TagsTagSeriesHandler(w, r); err != nil {
-			graphiteTagsTagSeriesErrors.Inc()
-			httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
-			return true
-		}
-		return true
-	case "/tags/tagMultiSeries":
-		graphiteTagsTagMultiSeriesRequests.Inc()
-		if err := graphite.TagsTagMultiSeriesHandler(w, r); err != nil {
-			graphiteTagsTagMultiSeriesErrors.Inc()
-			httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
-			return true
-		}
-		return true
 	case "/targets":
 		promscrapeTargetsRequests.Inc()
 		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -223,12 +207,6 @@ var (
 
 	influxQueryRequests = metrics.NewCounter(`vm_http_requests_total{path="/query", protocol="influx"}`)
 
-	graphiteTagsTagSeriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/tags/tagSeries", protocol="graphite"}`)
-	graphiteTagsTagSeriesErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/tags/tagSeries", protocol="graphite"}`)
-
-	graphiteTagsTagMultiSeriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/tags/tagMultiSeries", protocol="graphite"}`)
-	graphiteTagsTagMultiSeriesErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/tags/tagMultiSeries", protocol="graphite"}`)
-
 	promscrapeTargetsRequests      = metrics.NewCounter(`vm_http_requests_total{path="/targets"}`)
 	promscrapeAPIV1TargetsRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/targets"}`)
 
diff --git a/app/vmselect/graphite/tags_api.go b/app/vmselect/graphite/tags_api.go
index df0d613e7..ba46aeb2d 100644
--- a/app/vmselect/graphite/tags_api.go
+++ b/app/vmselect/graphite/tags_api.go
@@ -12,10 +12,146 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/bufferedwriter"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
+	graphiteparser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/graphite"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
 	"github.com/VictoriaMetrics/metrics"
 )
 
+// TagsDelSeriesHandler implements /tags/delSeries handler.
+//
+// See https://graphite.readthedocs.io/en/stable/tags.html#removing-series-from-the-tagdb
+func TagsDelSeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
+	if err := r.ParseForm(); err != nil {
+		return fmt.Errorf("cannot parse form values: %w", err)
+	}
+	paths := r.Form["path"]
+	totalDeleted := 0
+	var row graphiteparser.Row
+	var tagsPool []graphiteparser.Tag
+	ct := time.Now().UnixNano() / 1e6
+	for _, path := range paths {
+		var err error
+		tagsPool, err = row.UnmarshalMetricAndTags(path, tagsPool[:0])
+		if err != nil {
+			return fmt.Errorf("cannot parse path=%q: %w", path, err)
+		}
+		tfs := make([]storage.TagFilter, 0, 1+len(row.Tags))
+		tfs = append(tfs, storage.TagFilter{
+			Key:   nil,
+			Value: []byte(row.Metric),
+		})
+		for _, tag := range row.Tags {
+			tfs = append(tfs, storage.TagFilter{
+				Key:   []byte(tag.Key),
+				Value: []byte(tag.Value),
+			})
+		}
+		sq := storage.NewSearchQuery(0, ct, [][]storage.TagFilter{tfs})
+		n, err := netstorage.DeleteSeries(sq)
+		if err != nil {
+			return fmt.Errorf("cannot delete series for %q: %w", sq, err)
+		}
+		totalDeleted += n
+	}
+
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	if totalDeleted > 0 {
+		fmt.Fprintf(w, "true")
+	} else {
+		fmt.Fprintf(w, "false")
+	}
+	return nil
+}
+
+// TagsTagSeriesHandler implements /tags/tagSeries handler.
+//
+// See https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb
+func TagsTagSeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
+	return registerMetrics(startTime, w, r, false)
+}
+
+// TagsTagMultiSeriesHandler implements /tags/tagMultiSeries handler.
+//
+// See https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb
+func TagsTagMultiSeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
+	return registerMetrics(startTime, w, r, true)
+}
+
+func registerMetrics(startTime time.Time, w http.ResponseWriter, r *http.Request, isJSONResponse bool) error {
+	if err := r.ParseForm(); err != nil {
+		return fmt.Errorf("cannot parse form values: %w", err)
+	}
+	paths := r.Form["path"]
+	var row graphiteparser.Row
+	var labels []prompb.Label
+	var b []byte
+	var tagsPool []graphiteparser.Tag
+	mrs := make([]storage.MetricRow, len(paths))
+	ct := time.Now().UnixNano() / 1e6
+	canonicalPaths := make([]string, len(paths))
+	for i, path := range paths {
+		var err error
+		tagsPool, err = row.UnmarshalMetricAndTags(path, tagsPool[:0])
+		if err != nil {
+			return fmt.Errorf("cannot parse path=%q: %w", path, err)
+		}
+
+		// Construct canonical path according to https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb
+		sort.Slice(row.Tags, func(i, j int) bool {
+			return row.Tags[i].Key < row.Tags[j].Key
+		})
+		b = append(b[:0], row.Metric...)
+		for _, tag := range row.Tags {
+			b = append(b, ';')
+			b = append(b, tag.Key...)
+			b = append(b, '=')
+			b = append(b, tag.Value...)
+		}
+		canonicalPaths[i] = string(b)
+
+		// Convert parsed metric and tags to labels.
+		labels = append(labels[:0], prompb.Label{
+			Name:  []byte("__name__"),
+			Value: []byte(row.Metric),
+		})
+		for _, tag := range row.Tags {
+			labels = append(labels, prompb.Label{
+				Name:  []byte(tag.Key),
+				Value: []byte(tag.Value),
+			})
+		}
+
+		// Put labels with the current timestamp to MetricRow
+		mr := &mrs[i]
+		mr.MetricNameRaw = storage.MarshalMetricNameRaw(mr.MetricNameRaw[:0], labels)
+		mr.Timestamp = ct
+	}
+	if err := vmstorage.RegisterMetricNames(mrs); err != nil {
+		return fmt.Errorf("cannot register paths: %w", err)
+	}
+
+	// Return response
+	contentType := "text/plain; charset=utf-8"
+	if isJSONResponse {
+		contentType = "application/json; charset=utf-8"
+	}
+	w.Header().Set("Content-Type", contentType)
+	WriteTagsTagMultiSeriesResponse(w, canonicalPaths, isJSONResponse)
+	if isJSONResponse {
+		tagsTagMultiSeriesDuration.UpdateDuration(startTime)
+	} else {
+		tagsTagSeriesDuration.UpdateDuration(startTime)
+	}
+	return nil
+}
+
+var (
+	tagsTagSeriesDuration      = metrics.NewSummary(`vm_request_duration_seconds{path="/tags/tagSeries"}`)
+	tagsTagMultiSeriesDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/tags/tagMultiSeries"}`)
+)
+
 // TagsAutoCompleteValuesHandler implements /tags/autoComplete/values endpoint from Graphite Tags API.
 //
 // See https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support
diff --git a/app/vminsert/graphite/tags_tag_multi_series_response.qtpl b/app/vmselect/graphite/tags_tag_multi_series_response.qtpl
similarity index 100%
rename from app/vminsert/graphite/tags_tag_multi_series_response.qtpl
rename to app/vmselect/graphite/tags_tag_multi_series_response.qtpl
diff --git a/app/vmselect/graphite/tags_tag_multi_series_response.qtpl.go b/app/vmselect/graphite/tags_tag_multi_series_response.qtpl.go
new file mode 100644
index 000000000..d996a925d
--- /dev/null
+++ b/app/vmselect/graphite/tags_tag_multi_series_response.qtpl.go
@@ -0,0 +1,75 @@
+// Code generated by qtc from "tags_tag_multi_series_response.qtpl". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+// TagsTagMultiSeriesResponse generates response for /tags/tagMultiSeries .See https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb
+
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:5
+package graphite
+
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:5
+import (
+	qtio422016 "io"
+
+	qt422016 "github.com/valyala/quicktemplate"
+)
+
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:5
+var (
+	_ = qtio422016.Copy
+	_ = qt422016.AcquireByteBuffer
+)
+
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:5
+func StreamTagsTagMultiSeriesResponse(qw422016 *qt422016.Writer, canonicalPaths []string, isJSONResponse bool) {
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:6
+	if isJSONResponse {
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:6
+		qw422016.N().S(`[`)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:6
+	}
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:7
+	for i, path := range canonicalPaths {
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:8
+		qw422016.N().Q(path)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:9
+		if i+1 < len(canonicalPaths) {
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:9
+			qw422016.N().S(`,`)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:9
+		}
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:10
+	}
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:11
+	if isJSONResponse {
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:11
+		qw422016.N().S(`]`)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:11
+	}
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+}
+
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+func WriteTagsTagMultiSeriesResponse(qq422016 qtio422016.Writer, canonicalPaths []string, isJSONResponse bool) {
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+	StreamTagsTagMultiSeriesResponse(qw422016, canonicalPaths, isJSONResponse)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+	qt422016.ReleaseWriter(qw422016)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+}
+
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+func TagsTagMultiSeriesResponse(canonicalPaths []string, isJSONResponse bool) string {
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+	qb422016 := qt422016.AcquireByteBuffer()
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+	WriteTagsTagMultiSeriesResponse(qb422016, canonicalPaths, isJSONResponse)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+	qs422016 := string(qb422016.B)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+	qt422016.ReleaseByteBuffer(qb422016)
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+	return qs422016
+//line app/vmselect/graphite/tags_tag_multi_series_response.qtpl:12
+}
diff --git a/app/vmselect/main.go b/app/vmselect/main.go
index cc8aab099..3ec78f12e 100644
--- a/app/vmselect/main.go
+++ b/app/vmselect/main.go
@@ -23,7 +23,7 @@ import (
 )
 
 var (
-	deleteAuthKey         = flag.String("deleteAuthKey", "", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series")
+	deleteAuthKey         = flag.String("deleteAuthKey", "", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries")
 	maxConcurrentRequests = flag.Int("search.maxConcurrentRequests", getDefaultMaxConcurrentRequests(), "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")
 	maxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the request waits for execution when -search.maxConcurrentRequests "+
@@ -269,6 +269,22 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
 			return true
 		}
 		return true
+	case "/tags/tagSeries":
+		graphiteTagsTagSeriesRequests.Inc()
+		if err := graphite.TagsTagSeriesHandler(startTime, w, r); err != nil {
+			graphiteTagsTagSeriesErrors.Inc()
+			httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
+			return true
+		}
+		return true
+	case "/tags/tagMultiSeries":
+		graphiteTagsTagMultiSeriesRequests.Inc()
+		if err := graphite.TagsTagMultiSeriesHandler(startTime, w, r); err != nil {
+			graphiteTagsTagMultiSeriesErrors.Inc()
+			httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
+			return true
+		}
+		return true
 	case "/tags":
 		graphiteTagsRequests.Inc()
 		if err := graphite.TagsHandler(startTime, w, r); err != nil {
@@ -303,6 +319,19 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
 			return true
 		}
 		return true
+	case "/tags/delSeries":
+		graphiteTagsDelSeriesRequests.Inc()
+		authKey := r.FormValue("authKey")
+		if authKey != *deleteAuthKey {
+			httpserver.Errorf(w, r, "invalid authKey %q. It must match the value from -deleteAuthKey command line flag", authKey)
+			return true
+		}
+		if err := graphite.TagsDelSeriesHandler(startTime, w, r); err != nil {
+			graphiteTagsDelSeriesErrors.Inc()
+			httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
+			return true
+		}
+		return true
 	case "/api/v1/rules":
 		// Return dumb placeholder
 		rulesRequests.Inc()
@@ -416,6 +445,12 @@ var (
 	graphiteMetricsIndexRequests = metrics.NewCounter(`vm_http_requests_total{path="/metrics/index.json"}`)
 	graphiteMetricsIndexErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/metrics/index.json"}`)
 
+	graphiteTagsTagSeriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/tags/tagSeries"}`)
+	graphiteTagsTagSeriesErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/tags/tagSeries"}`)
+
+	graphiteTagsTagMultiSeriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/tags/tagMultiSeries"}`)
+	graphiteTagsTagMultiSeriesErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/tags/tagMultiSeries"}`)
+
 	graphiteTagsRequests = metrics.NewCounter(`vm_http_requests_total{path="/tags"}`)
 	graphiteTagsErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/tags"}`)
 
@@ -431,6 +466,9 @@ var (
 	graphiteTagsAutoCompleteValuesRequests = metrics.NewCounter(`vm_http_requests_total{path="/tags/autoComplete/values"}`)
 	graphiteTagsAutoCompleteValuesErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/tags/autoComplete/values"}`)
 
+	graphiteTagsDelSeriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/tags/delSeries"}`)
+	graphiteTagsDelSeriesErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/tags/delSeries"}`)
+
 	rulesRequests    = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/rules"}`)
 	alertsRequests   = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/alerts"}`)
 	metadataRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/metadata"}`)
diff --git a/app/vmselect/netstorage/tmp_blocks_file.go b/app/vmselect/netstorage/tmp_blocks_file.go
index 71bd7561c..7a16131a3 100644
--- a/app/vmselect/netstorage/tmp_blocks_file.go
+++ b/app/vmselect/netstorage/tmp_blocks_file.go
@@ -133,10 +133,7 @@ func (tbf *tmpBlocksFile) Finalize() error {
 		return fmt.Errorf("cannot write the remaining %d bytes to %q: %w", len(tbf.buf), fname, err)
 	}
 	tbf.buf = tbf.buf[:0]
-	r, err := fs.OpenReaderAt(fname)
-	if err != nil {
-		logger.Panicf("FATAL: cannot open %q: %s", fname, err)
-	}
+	r := fs.MustOpenReaderAt(fname)
 	// Hint the OS that the file is read almost sequentiallly.
 	// This should reduce the number of disk seeks, which is important
 	// for HDDs.
diff --git a/app/vmselect/promql/binary_op.go b/app/vmselect/promql/binary_op.go
index d26282330..7d827607a 100644
--- a/app/vmselect/promql/binary_op.go
+++ b/app/vmselect/promql/binary_op.go
@@ -62,6 +62,9 @@ func newBinaryOpCmpFunc(cf func(left, right float64) bool) binaryOpFunc {
 		if cf(left, right) {
 			return 1
 		}
+		if math.IsNaN(left) {
+			return nan
+		}
 		return 0
 	}
 	return newBinaryOpFunc(cfe)
diff --git a/app/vmselect/promql/exec.go b/app/vmselect/promql/exec.go
index 68b9c0dcd..c41fbac00 100644
--- a/app/vmselect/promql/exec.go
+++ b/app/vmselect/promql/exec.go
@@ -149,11 +149,11 @@ func adjustCmpOps(e metricsql.Expr) metricsql.Expr {
 		if !metricsql.IsBinaryOpCmp(be.Op) {
 			return
 		}
-		if _, ok := be.Left.(*metricsql.NumberExpr); !ok {
+		if isNumberExpr(be.Right) || !isScalarExpr(be.Left) {
 			return
 		}
 		// Convert 'num cmpOp query' expression to `query reverseCmpOp num` expression
-		// like Prometheus does. For isntance, `0.5 < foo` must be converted to `foo > 0.5`
+		// like Prometheus does. For instance, `0.5 < foo` must be converted to `foo > 0.5`
 		// in order to return valid values for `foo` that are bigger than 0.5.
 		be.Right, be.Left = be.Left, be.Right
 		be.Op = getReverseCmpOp(be.Op)
@@ -161,6 +161,22 @@ func adjustCmpOps(e metricsql.Expr) metricsql.Expr {
 	return e
 }
 
+func isNumberExpr(e metricsql.Expr) bool {
+	_, ok := e.(*metricsql.NumberExpr)
+	return ok
+}
+
+func isScalarExpr(e metricsql.Expr) bool {
+	if isNumberExpr(e) {
+		return true
+	}
+	if fe, ok := e.(*metricsql.FuncExpr); ok {
+		// time() returns scalar in PromQL - see https://prometheus.io/docs/prometheus/latest/querying/functions/#time
+		return strings.ToLower(fe.Name) == "time"
+	}
+	return false
+}
+
 func getReverseCmpOp(op string) string {
 	switch op {
 	case ">":
diff --git a/app/vmselect/promql/rollup.go b/app/vmselect/promql/rollup.go
index c711f04ad..2685f72ca 100644
--- a/app/vmselect/promql/rollup.go
+++ b/app/vmselect/promql/rollup.go
@@ -1142,10 +1142,10 @@ func rollupSum(rfa *rollupFuncArg) float64 {
 	// before calling rollup funcs.
 	values := rfa.values
 	if len(values) == 0 {
-		if math.IsNaN(rfa.prevValue) {
-			return nan
-		}
-		return 0
+		// Do not take into account rfa.prevValue, since it may lead
+		// to inconsistent results comparing to Prometheus on broken time series
+		// with irregular data points.
+		return nan
 	}
 	var sum float64
 	for _, v := range values {
diff --git a/app/vmselect/promql/transform.go b/app/vmselect/promql/transform.go
index 660f8bb8e..8d350dc29 100644
--- a/app/vmselect/promql/transform.go
+++ b/app/vmselect/promql/transform.go
@@ -265,6 +265,9 @@ func newTransformFuncDateTime(f func(t time.Time) int) transformFunc {
 		}
 		tf := func(values []float64) {
 			for i, v := range values {
+				if math.IsNaN(v) {
+					continue
+				}
 				t := time.Unix(int64(v), 0).UTC()
 				values[i] = float64(f(t))
 			}
diff --git a/app/vmselect/searchutils/searchutils.go b/app/vmselect/searchutils/searchutils.go
index 85e0a4688..c3c5c5511 100644
--- a/app/vmselect/searchutils/searchutils.go
+++ b/app/vmselect/searchutils/searchutils.go
@@ -184,5 +184,7 @@ func (d *Deadline) Deadline() uint64 {
 
 // String returns human-readable string representation for d.
 func (d *Deadline) String() string {
-	return fmt.Sprintf("%.3f seconds; the timeout can be adjusted with `%s` command-line flag", d.timeout.Seconds(), d.flagHint)
+	startTime := time.Unix(int64(d.deadline), 0).Add(-d.timeout)
+	elapsed := time.Since(startTime)
+	return fmt.Sprintf("%.3f seconds (elapsed %.3f seconds); the timeout can be adjusted with `%s` command-line flag", d.timeout.Seconds(), elapsed.Seconds(), d.flagHint)
 }
diff --git a/dashboards/victoriametrics.json b/dashboards/victoriametrics.json
index 948c79f43..d44e06730 100644
--- a/dashboards/victoriametrics.json
+++ b/dashboards/victoriametrics.json
@@ -1251,7 +1251,7 @@
           "dashLength": 10,
           "dashes": false,
           "datasource": "$ds",
-          "description": "Shows the time needed to reach the 100% of disk capacity based on the following params:\n* free disk space;\n* rows ingestion rate;\n* compression.\n\nUse this panel for capacity planning in order to estimate the time remaining for running out of the disk space.\n\n",
+          "description": "Shows the time needed to reach the 100% of disk capacity based on the following params:\n* free disk space;\n* row ingestion rate;\n* dedup rate;\n* compression.\n\nUse this panel for capacity planning in order to estimate the time remaining for running out of the disk space.\n\n",
           "fieldConfig": {
             "defaults": {
               "custom": {},
@@ -1295,7 +1295,7 @@
           "steppedLine": false,
           "targets": [
             {
-              "expr": "vm_free_disk_space_bytes{job=\"$job\", instance=\"$instance\"} / (sum(rate(vm_rows_added_to_storage_total{job=\"$job\", instance=\"$instance\"}[1d])) * (sum(vm_data_size_bytes{job=\"$job\", instance=\"$instance\", type!=\"indexdb\"}) / sum(vm_rows{job=\"$job\", instance=\"$instance\", type!=\"indexdb\"})))",
+              "expr": "vm_free_disk_space_bytes{job=\"$job\", instance=\"$instance\"} / ignoring(path) ((rate(vm_rows_added_to_storage_total{job=\"$job\", instance=\"$instance\"}[1d]) - ignoring(type) rate(vm_deduplicated_samples_total{job=\"$job\", instance=\"$instance\", type=\"merge\"}[1d])) * (sum(vm_data_size_bytes{job=\"$job\", instance=\"$instance\", type!=\"indexdb\"}) / sum(vm_rows{job=\"$job\", instance=\"$instance\", type!=\"indexdb\"})))",
               "format": "time_series",
               "hide": false,
               "interval": "",
@@ -3395,4 +3395,4 @@
   "title": "VictoriaMetrics",
   "uid": "wNf0q_kZk",
   "version": 1
-}
\ No newline at end of file
+}
diff --git a/docs/Articles.md b/docs/Articles.md
index 68530972b..bc6714aab 100644
--- a/docs/Articles.md
+++ b/docs/Articles.md
@@ -2,6 +2,7 @@
 
 ## Third-party articles and slides about VictoriaMetrics
 
+* [Foiled by the Firewall: A Tale of Transition From Prometheus to VictoriaMetrics](https://www.percona.com/blog/2020/12/01/foiled-by-the-firewall-a-tale-of-transition-from-prometheus-to-victoriametrics/)
 * [Better Prometheus rate() function with VictoriaMetrics](https://www.percona.com/blog/2020/02/28/better-prometheus-rate-function-with-victoriametrics/)
 * [Making peace with Prometheus rate()](https://blog.doit-intl.com/making-peace-with-prometheus-rate-43a3ea75c4cf)
 * [Infrastructure monitoring with Prometheus at Zerodha](https://zerodha.tech/blog/infra-monitoring-at-zerodha/)
@@ -19,6 +20,7 @@
 * [Calculating the Error of Quantile Estimation with Histograms](https://linuxczar.net/blog/2020/08/13/histogram-error/)
 * [Monitoring private clouds with VictoriaMetrics at LeroyMerlin](https://www.youtube.com/watch?v=74swsWqf0Uc)
 * [Monitoring Kubernetes with VictoriaMetrics+Prometheus](https://speakerdeck.com/bo0km4n/victoriametrics-plus-prometheusdegou-zhu-surufu-shu-kubernetesfalsejian-shi-ji-pan)
+* [High-performance Graphite storage solution on top of VictoriaMetrics](https://golangexample.com/a-high-performance-graphite-storage-solution/)
 
 
 ## Our articles
@@ -48,3 +50,6 @@
 * [Filtering and modifying time series during import to VictoriaMetrics](https://medium.com/@romanhavronenko/victoriametrics-how-to-migrate-data-from-prometheus-filtering-and-modifying-time-series-6d40cea4bf21)
 * [Anomaly Detection in VictoriaMetrics](https://medium.com/@VictoriaMetrics/anomaly-detection-in-victoriametrics-9528538786a7)
 * [How to use relabeling in Prometheus and VictoriaMetrics](https://valyala.medium.com/how-to-use-relabeling-in-prometheus-and-victoriametrics-8b90fc22c4b2)
+* [First look at performance comparison between InfluxDB IOx and VictoriaMetrics](https://medium.com/@VictoriaMetrics/first-look-at-perfomance-comparassion-between-influxdb-iox-and-victoriametrics-e590f847935b)
+* [Prometheus vs VictoriaMetrics benchmark on node-exporter metrics](https://valyala.medium.com/prometheus-vs-victoriametrics-benchmark-on-node-exporter-metrics-4ca29c75590f)
+* [How to monitor Go applications with VictoriaMetrics](https://victoriametrics.medium.com/how-to-monitor-go-applications-with-victoriametrics-c04703110870)
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 44de82a62..43c291865 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -2,6 +2,18 @@
 
 # tip
 
+* BUGFIX: properly parse timestamps in OpenMetrics format - they are exposed as floating-point number in seconds instead of integer milliseconds
+  unlike in Prometheus exposition format. See [the docs](https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md#timestamps).
+* BUGFIX: return `nan` for `a >bool b` query when `a` equals to `nan` like Prometheus does. Previously `0` was returned in this case. This applies to any comparison operation
+  with `bool` modifier. See [these docs](https://prometheus.io/docs/prometheus/latest/querying/operators/#comparison-binary-operators) for details.
+* BUGFIX: properly parse hex numbers in MetricsQL. Previously hex numbers with non-decimal digits such as `0x3b` couldn't be parsed.
+* BUGFIX: handle `time() cmp_op metric` like Prometheus does - i.e. return `metric` value if `cmp_op` comparison is true. Previously `time()` value was returned.
+* BUGFIX: return `nan` for `minute(m)` query when `m` equals to `nan` like Prometheus does. This applies to all the time-related functions such as `day_of_month`, `day_of_week`,
+  `days_in_month`, `hour`, `month` and `year`.
+
+
+# [v1.48.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.48.0)
+
 * FEATURE: added [Snap package for single-node VictoriaMetrics](https://snapcraft.io/victoriametrics). This simplifies installation under Ubuntu to a single command:
   ```bash
   snap install victoriametrics
@@ -9,13 +21,21 @@
 * FEATURE: vmselect: add `-replicationFactor` command-line flag for reducing query duration when replication is enabled and a part of vmstorage nodes
   are temporarily slow and/or temporarily unavailable. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/711
 * FEATURE: vminsert: export `vm_rpc_vmstorage_is_reachable` metric, which can be used for monitoring reachability of vmstorage nodes from vminsert nodes.
-* FEATURE: vmagent: add Netflix Eureka service discovery (aka [eureka_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#eureka_sd_config)).
-See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/851
+* FEATURE: vmagent: add [Netflix Eureka](https://github.com/Netflix/eureka) service discovery (aka [eureka_sd_config](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#eureka_sd_config)). See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/851
+* FEATURE: add `filters` option to `dockerswarm_sd_config` like Prometheus did in v2.23.0 - see https://github.com/prometheus/prometheus/pull/8074
+* FEATURE: expose `__meta_ec2_ipv6_addresses` label for `ec2_sd_config` like Prometheus will do in the next release.
 * FEATURE: add `-loggerWarnsPerSecondLimit` command-line flag for rate limiting of WARN messages in logs. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/905
 * FEATURE: apply `loggerErrorsPerSecondLimit` and `-loggerWarnsPerSecondLimit` rate limit per caller. I.e. log messages are suppressed if the same caller logs the same message
   at the rate exceeding the given limit. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/905#issuecomment-729395855
 * FEATURE: add remoteAddr to slow query log in order to simplify identifying the client that sends slow queries to VictoriaMetrics.
   Slow query logging is controlled with `-search.logSlowQueryDuration` command-line flag.
+* FEATURE: add `/tags/delSeries` handler from Graphite Tags API. See https://victoriametrics.github.io/#graphite-tags-api-usage
+* FEATURE: log metric name plus all its labels when the metric timestamp is out of the configured retention. This should simplify detecting the source of metrics with unexpected timestamps.
+* FEATURE: add `-dryRun` command-line flag to single-node VictoriaMetrics in order to check config file pointed by `-promscrape.config`.
+
+* BUGFIX: properly parse Prometheus metrics with [exemplars](https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md#exemplars-1) such as `foo 123 # {bar="baz"} 1`.
+* BUGFIX: properly parse "infinity" values in [OpenMetrics format](https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md#abnf).
+  See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/924
 
 
 # [v1.47.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.47.0)
@@ -52,7 +72,7 @@ See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/851
 
 * FEATURE: optimize requests to `/api/v1/labels` and `/api/v1/label/<name>/values` when `start` and `end` args are set.
 * FEATURE: reduce memory usage when query touches big number of time series.
-* FEATURE: vmagent: reduce memory usage when `kubernetes_sd_config` discovers big number of scrape targets (e.g. hundreds of thouthands) and the majority of these targets (99%)
+* FEATURE: vmagent: reduce memory usage when `kubernetes_sd_config` discovers big number of scrape targets (e.g. hundreds of thousands) and the majority of these targets (99%)
   are dropped during relabeling. Previously labels for all the dropped targets were displayed at `/api/v1/targets` page. Now only up to `-promscrape.maxDroppedTargets` such
   targets are displayed. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/878 for details.
 * FEATURE: vmagent: reduce memory usage when scraping big number of targets with big number of temporary labels starting with `__`.
diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md
index fed3173d3..090601648 100644
--- a/docs/Cluster-VictoriaMetrics.md
+++ b/docs/Cluster-VictoriaMetrics.md
@@ -205,11 +205,14 @@ or [an alternative dashboard for VictoriaMetrics cluster](https://grafana.com/gr
       - `metrics/find` - searches Graphite metrics. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find).
       - `metrics/expand` - expands Graphite metrics. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand).
       - `metrics/index.json` - returns all the metric names. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json).
+      - `tags/tagSeries` - registers time series. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb).
+      - `tags/tagMultiSeries` - register multiple time series. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#adding-series-to-the-tagdb).
       - `tags` - returns tag names. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags).
       - `tags/<tag_name>` - returns tag values for the given `<tag_name>`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags).
       - `tags/findSeries` - returns series matching the given `expr`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags).
       - `tags/autoComplete/tags` - returns tags matching the given `tagPrefix` and/or `expr`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support).
       - `tags/autoComplete/values` - returns tag values matching the given `valuePrefix` and/or `expr`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support).
+      - `tags/delSeries` - deletes series matching the given `path`. See [these docs](https://graphite.readthedocs.io/en/stable/tags.html#removing-series-from-the-tagdb).
 
 * URL for time series deletion: `http://<vmselect>:8481/delete/<accountID>/prometheus/api/v1/admin/tsdb/delete_series?match[]=<timeseries_selector_for_delete>`.
   Note that the `delete_series` handler should be used only in exceptional cases such as deletion of accidentally ingested incorrect time series. It shouldn't
diff --git a/docs/FAQ.md b/docs/FAQ.md
index e0aa8ab02..cde802c27 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -142,6 +142,7 @@ The architecture is [optimized for storing and querying large amounts of time se
 
 Yes:
 
+* [Prometheus vs VictoriaMetrics benchmark on node-exporter metrics](https://valyala.medium.com/prometheus-vs-victoriametrics-benchmark-on-node-exporter-metrics-4ca29c75590f)
 * [Benchmarking time series workloads on Apache Kudu using TSBS](https://blog.cloudera.com/benchmarking-time-series-workloads-on-apache-kudu-using-tsbs/)
 * [Billy: how VictoriaMetrics deals with more than 500 billion rows](https://medium.com/@valyala/billy-how-victoriametrics-deals-with-more-than-500-billion-rows-e82ff8f725da)
 * [Measuring vertical scalability for time series databases: VictoriaMetrics vs InfluxDB vs TimescaleDB](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index 85675eb54..3386b7a4c 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -49,17 +49,22 @@ Click on a link in order to read the corresponding case study
 
 * VictoriaMetrics can be used as long-term storage for Prometheus or for [vmagent](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmagent/README.md).
   See [these docs](#prometheus-setup) for details.
-* Supports [Prometheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/), so it can be used as Prometheus drop-in replacement in Grafana.
-  VictoriaMetrics implements [MetricsQL](https://github.com/VictoriaMetrics/VictoriaMetrics/wiki/MetricsQL) query language, which inspired by PromQL. MetricsQL is backwards-compatible with PromQL.
-* Supports global query view. Multiple Prometheus instances or any other data sources may write data into VictoriaMetrics. Later this data may be queried in a single query.
+* VictoriaMetrics supports [Prometheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/), so it can be used as Prometheus drop-in replacement in Grafana.
+* VictoriaMetrics implements [MetricsQL](https://github.com/VictoriaMetrics/VictoriaMetrics/wiki/MetricsQL) query language backwards compatible with PromQL.
+* VictoriaMetrics provides global query view. Multiple Prometheus instances or any other data sources may ingest data into VictoriaMetrics.
+  Later this data may be queried via a single query.
 * High performance and good scalability for both [inserts](https://medium.com/@valyala/high-cardinality-tsdb-benchmarks-victoriametrics-vs-timescaledb-vs-influxdb-13e6ee64dd6b)
   and [selects](https://medium.com/@valyala/when-size-matters-benchmarking-victoriametrics-vs-timescale-and-influxdb-6035811952d4).
   [Outperforms InfluxDB and TimescaleDB by up to 20x](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
-* [Uses 10x less RAM than InfluxDB](https://medium.com/@valyala/insert-benchmarks-with-inch-influxdb-vs-victoriametrics-e31a41ae2893) when working with millions of unique time series (aka high cardinality).
+* [Uses 10x less RAM than InfluxDB](https://medium.com/@valyala/insert-benchmarks-with-inch-influxdb-vs-victoriametrics-e31a41ae2893)
+  and [up to 7x less RAM than Prometheus, Thanos or Cortex](https://valyala.medium.com/prometheus-vs-victoriametrics-benchmark-on-node-exporter-metrics-4ca29c75590f)
+  when dealing with millions of unique time series (aka high cardinality).
 * Optimized for time series with high churn rate. Think about [prometheus-operator](https://github.com/coreos/prometheus-operator) metrics from frequent deployments in Kubernetes.
 * High data compression, so [up to 70x more data points](https://medium.com/@valyala/when-size-matters-benchmarking-victoriametrics-vs-timescale-and-influxdb-6035811952d4)
-  may be crammed into limited storage comparing to TimescaleDB.
-* Optimized for storage with high-latency IO and low IOPS (HDD and network storage in AWS, Google Cloud, Microsoft Azure, etc). See [graphs from these benchmarks](https://medium.com/@valyala/high-cardinality-tsdb-benchmarks-victoriametrics-vs-timescaledb-vs-influxdb-13e6ee64dd6b).
+  may be crammed into limited storage comparing to TimescaleDB
+  and [up to 7x less storage space is required comparing to Prometheus, Thanos or Cortex](https://valyala.medium.com/prometheus-vs-victoriametrics-benchmark-on-node-exporter-metrics-4ca29c75590f).
+* Optimized for storage with high-latency IO and low IOPS (HDD and network storage in AWS, Google Cloud, Microsoft Azure, etc).
+  See [graphs from these benchmarks](https://medium.com/@valyala/high-cardinality-tsdb-benchmarks-victoriametrics-vs-timescaledb-vs-influxdb-13e6ee64dd6b).
 * A single-node VictoriaMetrics may substitute moderately sized clusters built with competing solutions such as Thanos, M3DB, Cortex, InfluxDB or TimescaleDB.
   See [vertical scalability benchmarks](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae),
   [comparing Thanos to VictoriaMetrics cluster](https://medium.com/@valyala/comparing-thanos-to-victoriametrics-cluster-b193bea1683)
@@ -68,7 +73,7 @@ Click on a link in order to read the corresponding case study
 * Easy operation:
   * VictoriaMetrics consists of a single [small executable](https://medium.com/@valyala/stripping-dependency-bloat-in-victoriametrics-docker-image-983fb5912b0d) without external dependencies.
   * All the configuration is done via explicit command-line flags with reasonable defaults.
-  * All the data is stored in a single directory pointed by `-storageDataPath` flag.
+  * All the data is stored in a single directory pointed by `-storageDataPath` command-line flag.
   * Easy and fast backups from [instant snapshots](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282)
   to S3 or GCS with [vmbackup](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmbackup/README.md) / [vmrestore](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmrestore/README.md).
   See [this article](https://medium.com/@valyala/speeding-up-backups-for-big-time-series-databases-533c1a927883) for more details.
@@ -91,6 +96,7 @@ Click on a link in order to read the corresponding case study
 * Has open source [cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster).
 * See also technical [Articles about VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/wiki/Articles).
 
+
 ## Operation
 
 ### Table of contents
@@ -175,7 +181,8 @@ The following command-line flags are used the most:
 
 Other flags have good enough default values, so set them only if you really need this. Pass `-help` to see all the available flags with description and default values.
 
-See how to [ingest data to VictoriaMetrics](#how-to-import-time-series-data) and how to [query VictoriaMetrics](#grafana-setup).
+See how to [ingest data to VictoriaMetrics](#how-to-import-time-series-data), how to [query VictoriaMetrics](#grafana-setup)
+and how to [handle alerts](#alerting).
 VictoriaMetrics accepts [Prometheus querying API requests](#prometheus-querying-api-usage) on port `8428` by default.
 
 It is recommended setting up [monitoring](#monitoring) for VictoriaMetrics.
@@ -242,8 +249,9 @@ Read more about tuning remote write for Prometheus [here](https://prometheus.io/
 
 It is recommended upgrading Prometheus to [v2.12.0](https://github.com/prometheus/prometheus/releases) or newer, since previous versions may have issues with `remote_write`.
 
-Take a look also at [vmagent](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmagent/README.md),
-which can be used as faster and less resource-hungry alternative to Prometheus in certain cases.
+Take a look also at [vmagent](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmagent/README.md)
+and [vmalert](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/README.md),
+which can be used as faster and less resource-hungry alternative to Prometheus.
 
 
 ## Grafana setup
@@ -419,7 +427,7 @@ Data sent to VictoriaMetrics via `Graphite plaintext protocol` may be read via t
 * [Prometheus querying API](#prometheus-querying-api-usage)
 * Metric names can be explored via [Graphite metrics API](#graphite-metrics-api-usage)
 * Tags can be explored via [Graphite tags API](#graphite-tags-api-usage)
-* [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/master/cmd/carbonapi/carbonapi.example.prometheus.yaml)
+* [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/main/cmd/carbonapi/carbonapi.example.victoriametrics.yaml)
 
 ## How to send data from OpenTSDB-compatible agents
 
@@ -566,6 +574,7 @@ VictoriaMetrics supports the following handlers from [Graphite Tags API](https:/
 * [/tags/findSeries](https://graphite.readthedocs.io/en/stable/tags.html#exploring-tags)
 * [/tags/autoComplete/tags](https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support)
 * [/tags/autoComplete/values](https://graphite.readthedocs.io/en/stable/tags.html#auto-complete-support)
+* [/tags/delSeries](https://graphite.readthedocs.io/en/stable/tags.html#removing-series-from-the-tagdb)
 
 
 ## How to build from sources
@@ -1340,7 +1349,7 @@ See also [high availability docs](#high-availability) and [backup docs](#backups
 
 VictoriaMetrics supports backups via [vmbackup](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmbackup/README.md)
 and [vmrestore](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmrestore/README.md) tools.
-We also provide provide `vmbackuper` tool for paid enterprise subscribers - see [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/466) for details.
+We also provide `vmbackuper` tool for paid enterprise subscribers - see [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/466) for details.
 
 
 ## Profiling
diff --git a/go.mod b/go.mod
index 8b886db80..019f3e084 100644
--- a/go.mod
+++ b/go.mod
@@ -7,15 +7,15 @@ require (
 
 	// Do not use the original github.com/valyala/fasthttp because of issues
 	// like https://github.com/valyala/fasthttp/commit/996610f021ff45fdc98c2ce7884d5fa4e7f9199b
-	github.com/VictoriaMetrics/fasthttp v1.0.7
+	github.com/VictoriaMetrics/fasthttp v1.0.9
 	github.com/VictoriaMetrics/metrics v1.12.3
-	github.com/VictoriaMetrics/metricsql v0.7.2
+	github.com/VictoriaMetrics/metricsql v0.7.3
 	github.com/aws/aws-sdk-go v1.35.31
 	github.com/cespare/xxhash/v2 v2.1.1
 	github.com/golang/snappy v0.0.2
 	github.com/klauspost/compress v1.11.3
 	github.com/stretchr/testify v1.5.1 // indirect
-	github.com/valyala/fastjson v1.6.1
+	github.com/valyala/fastjson v1.6.3
 	github.com/valyala/fastrand v1.0.0
 	github.com/valyala/fasttemplate v1.2.1
 	github.com/valyala/gozstd v1.8.3
diff --git a/go.sum b/go.sum
index b14e0bfb2..725ebe641 100644
--- a/go.sum
+++ b/go.sum
@@ -40,13 +40,13 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/VictoriaMetrics/fastcache v1.5.7 h1:4y6y0G8PRzszQUYIQHHssv/jgPHAb5qQuuDNdCbyAgw=
 github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8=
-github.com/VictoriaMetrics/fasthttp v1.0.7 h1:9RntF8jE9z+ooyXy0tb3Pl76jan4DLaCQoCQFp/cIUE=
-github.com/VictoriaMetrics/fasthttp v1.0.7/go.mod h1:eaGv8oDDOWE8JQa6GgBgpCwjJhRxavmprtf2oWw8b3o=
+github.com/VictoriaMetrics/fasthttp v1.0.9 h1:Fja1tfcNMNoUD7RJDYpjGx2CsSfXkUbISKY4kNafdN4=
+github.com/VictoriaMetrics/fasthttp v1.0.9/go.mod h1:3SeUL4zwB/p/a9aEeRc6gdlbrtNHXBJR6N376EgiSHU=
 github.com/VictoriaMetrics/metrics v1.12.2/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE=
 github.com/VictoriaMetrics/metrics v1.12.3 h1:Fe6JHC6MSEKa+BtLhPN8WIvS+HKPzMc2evEpNeCGy7I=
 github.com/VictoriaMetrics/metrics v1.12.3/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE=
-github.com/VictoriaMetrics/metricsql v0.7.2 h1:ZdFPiA9Etrf3dow43IcPvLjPi5BYWIYj194wPKIhKfs=
-github.com/VictoriaMetrics/metricsql v0.7.2/go.mod h1:ylO7YITho/Iw6P71oEaGyHbO94bGoGtzWfLGqFhMIg8=
+github.com/VictoriaMetrics/metricsql v0.7.3 h1:ReI+UBleCkGMmYDt69gsuGld71l1WpQDIlhgU1N5xQ8=
+github.com/VictoriaMetrics/metricsql v0.7.3/go.mod h1:ylO7YITho/Iw6P71oEaGyHbO94bGoGtzWfLGqFhMIg8=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
 github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
@@ -164,8 +164,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
-github.com/valyala/fastjson v1.6.1 h1:qJs/Kz/HebWzk8LmhOrSm7kdOyJBr1XB+zSkYtEEfQE=
-github.com/valyala/fastjson v1.6.1/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
+github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc=
+github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
 github.com/valyala/fastrand v1.0.0 h1:LUKT9aKer2dVQNUi3waewTbKV+7H17kvWFNKs2ObdkI=
 github.com/valyala/fastrand v1.0.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
 github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
diff --git a/lib/fs/reader_at.go b/lib/fs/reader_at.go
index 175292cc0..00b05d24e 100644
--- a/lib/fs/reader_at.go
+++ b/lib/fs/reader_at.go
@@ -158,13 +158,13 @@ func (r *ReaderAt) MustFadviseSequentialRead(prefetch bool) {
 	}
 }
 
-// OpenReaderAt opens ReaderAt for reading from filename.
+// MustOpenReaderAt opens ReaderAt for reading from filename.
 //
 // MustClose must be called on the returned ReaderAt when it is no longer needed.
-func OpenReaderAt(path string) (*ReaderAt, error) {
+func MustOpenReaderAt(path string) *ReaderAt {
 	f, err := os.Open(path)
 	if err != nil {
-		return nil, fmt.Errorf("cannot open file %q for reader: %w", path, err)
+		logger.Panicf("FATAL: cannot open file %q for reading: %s", path, err)
 	}
 	var r ReaderAt
 	r.f = f
@@ -172,7 +172,8 @@ func OpenReaderAt(path string) (*ReaderAt, error) {
 	if !*disableMmap {
 		fi, err := f.Stat()
 		if err != nil {
-			return nil, fmt.Errorf("error in stat: %w", err)
+			MustClose(f)
+			logger.Panicf("FATAL: error in fstat(%q): %s", path, err)
 		}
 		size := fi.Size()
 		bm := &pageCacheBitmap{
@@ -188,12 +189,12 @@ func OpenReaderAt(path string) (*ReaderAt, error) {
 		data, err := mmapFile(f, size)
 		if err != nil {
 			MustClose(f)
-			return nil, fmt.Errorf("cannot init reader for %q: %w", path, err)
+			logger.Panicf("FATAL: cannot mmap %q: %s", path, err)
 		}
 		r.mmapData = data
 	}
 	readersCount.Inc()
-	return &r, nil
+	return &r
 }
 
 func pageCacheBitmapCleaner(pcbm *atomic.Value, stopCh <-chan struct{}) {
diff --git a/lib/logger/logger.go b/lib/logger/logger.go
index d747c2027..0e64ef332 100644
--- a/lib/logger/logger.go
+++ b/lib/logger/logger.go
@@ -22,9 +22,9 @@ var (
 	loggerOutput      = flag.String("loggerOutput", "stderr", "Output for the logs. Supported values: stderr, stdout")
 	disableTimestamps = flag.Bool("loggerDisableTimestamps", false, "Whether to disable writing timestamps in logs")
 
-	errorsPerSecondLimit = flag.Int("loggerErrorsPerSecondLimit", 10, "Per-second limit on the number of ERROR messages. If more than the given number of errors "+
+	errorsPerSecondLimit = flag.Int("loggerErrorsPerSecondLimit", 0, "Per-second limit on the number of ERROR messages. If more than the given number of errors "+
 		"are emitted per second, then the remaining errors are suppressed. Zero value disables the rate limit")
-	warnsPerSecondLimit = flag.Int("loggerWarnsPerSecondLimit", 10, "Per-second limit on the number of WARN messages. If more than the given number of warns "+
+	warnsPerSecondLimit = flag.Int("loggerWarnsPerSecondLimit", 0, "Per-second limit on the number of WARN messages. If more than the given number of warns "+
 		"are emitted per second, then the remaining warns are suppressed. Zero value disables the rate limit")
 )
 
diff --git a/lib/mergeset/encoding.go b/lib/mergeset/encoding.go
index 3c9561e5e..9a48a8c02 100644
--- a/lib/mergeset/encoding.go
+++ b/lib/mergeset/encoding.go
@@ -31,6 +31,12 @@ type inmemoryBlock struct {
 func (ib *inmemoryBlock) Reset() {
 	ib.commonPrefix = ib.commonPrefix[:0]
 	ib.data = ib.data[:0]
+
+	items := ib.items
+	for i := range items {
+		// Remove reference to by slice, so GC could free the byte slice.
+		items[i] = nil
+	}
 	ib.items = ib.items[:0]
 }
 
diff --git a/lib/mergeset/part.go b/lib/mergeset/part.go
index 86bbc5bb8..ed4d41c9c 100644
--- a/lib/mergeset/part.go
+++ b/lib/mergeset/part.go
@@ -78,30 +78,15 @@ func openFilePart(path string) (*part, error) {
 	metaindexSize := fs.MustFileSize(metaindexPath)
 
 	indexPath := path + "/index.bin"
-	indexFile, err := fs.OpenReaderAt(indexPath)
-	if err != nil {
-		metaindexFile.MustClose()
-		return nil, fmt.Errorf("cannot open %q: %w", indexPath, err)
-	}
+	indexFile := fs.MustOpenReaderAt(indexPath)
 	indexSize := fs.MustFileSize(indexPath)
 
 	itemsPath := path + "/items.bin"
-	itemsFile, err := fs.OpenReaderAt(itemsPath)
-	if err != nil {
-		metaindexFile.MustClose()
-		indexFile.MustClose()
-		return nil, fmt.Errorf("cannot open %q: %w", itemsPath, err)
-	}
+	itemsFile := fs.MustOpenReaderAt(itemsPath)
 	itemsSize := fs.MustFileSize(itemsPath)
 
 	lensPath := path + "/lens.bin"
-	lensFile, err := fs.OpenReaderAt(lensPath)
-	if err != nil {
-		metaindexFile.MustClose()
-		indexFile.MustClose()
-		itemsFile.MustClose()
-		return nil, fmt.Errorf("cannot open %q: %w", lensPath, err)
-	}
+	lensFile := fs.MustOpenReaderAt(lensPath)
 	lensSize := fs.MustFileSize(lensPath)
 
 	size := metaindexSize + indexSize + itemsSize + lensSize
@@ -260,7 +245,7 @@ func (idxbc *indexBlockCache) Get(k uint64) *indexBlock {
 func (idxbc *indexBlockCache) Put(k uint64, idxb *indexBlock) {
 	idxbc.mu.Lock()
 
-	// Remove superflouos entries.
+	// Remove superfluous entries.
 	if overflow := len(idxbc.m) - getMaxCachedIndexBlocksPerPart(); overflow > 0 {
 		// Remove 10% of items from the cache.
 		overflow = int(float64(len(idxbc.m)) * 0.1)
@@ -408,7 +393,7 @@ func (ibc *inmemoryBlockCache) Get(k inmemoryBlockCacheKey) *inmemoryBlock {
 func (ibc *inmemoryBlockCache) Put(k inmemoryBlockCacheKey, ib *inmemoryBlock) {
 	ibc.mu.Lock()
 
-	// Clean superflouos entries in cache.
+	// Clean superfluous entries in cache.
 	if overflow := len(ibc.m) - getMaxCachedInmemoryBlocksPerPart(); overflow > 0 {
 		// Remove 10% of items from the cache.
 		overflow = int(float64(len(ibc.m)) * 0.1)
diff --git a/lib/mergeset/table.go b/lib/mergeset/table.go
index b4084b848..4284bc623 100644
--- a/lib/mergeset/table.go
+++ b/lib/mergeset/table.go
@@ -369,7 +369,7 @@ func (tb *Table) AddItems(items [][]byte) error {
 			tb.rawItemsBlocks = append(tb.rawItemsBlocks, ib)
 		}
 	}
-	if len(tb.rawItemsBlocks) >= 1024 {
+	if len(tb.rawItemsBlocks) >= 512 {
 		blocksToMerge = tb.rawItemsBlocks
 		tb.rawItemsBlocks = nil
 		tb.rawItemsLastFlushTime = fasttime.UnixTimestamp()
diff --git a/lib/promscrape/client.go b/lib/promscrape/client.go
index b7a93ffbd..98e1ff230 100644
--- a/lib/promscrape/client.go
+++ b/lib/promscrape/client.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
 	"github.com/VictoriaMetrics/fasthttp"
@@ -198,14 +199,14 @@ func (c *client) ReadData(dst []byte) ([]byte, error) {
 	}
 	if ce := resp.Header.Peek("Content-Encoding"); string(ce) == "gzip" {
 		var err error
-		var src []byte
 		if swapResponseBodies {
-			src = append(src, dst...)
-			dst = dst[:0]
+			zb := gunzipBufPool.Get()
+			zb.B, err = fasthttp.AppendGunzipBytes(zb.B[:0], dst)
+			dst = append(dst[:0], zb.B...)
+			gunzipBufPool.Put(zb)
 		} else {
-			src = resp.Body()
+			dst, err = fasthttp.AppendGunzipBytes(dst, resp.Body())
 		}
-		dst, err = fasthttp.AppendGunzipBytes(dst, src)
 		if err != nil {
 			fasthttp.ReleaseResponse(resp)
 			scrapesGunzipFailed.Inc()
@@ -215,16 +216,18 @@ func (c *client) ReadData(dst []byte) ([]byte, error) {
 	} else if !swapResponseBodies {
 		dst = append(dst, resp.Body()...)
 	}
+	fasthttp.ReleaseResponse(resp)
 	if statusCode != fasthttp.StatusOK {
 		metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_scrapes_total{status_code="%d"}`, statusCode)).Inc()
 		return dst, fmt.Errorf("unexpected status code returned when scraping %q: %d; expecting %d; response body: %q",
 			c.scrapeURL, statusCode, fasthttp.StatusOK, dst)
 	}
 	scrapesOK.Inc()
-	fasthttp.ReleaseResponse(resp)
 	return dst, nil
 }
 
+var gunzipBufPool bytesutil.ByteBufferPool
+
 var (
 	scrapesTimedout     = metrics.NewCounter(`vm_promscrape_scrapes_timed_out_total`)
 	scrapesOK           = metrics.NewCounter(`vm_promscrape_scrapes_total{status_code="200"}`)
diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go
index 1a8b1fdd2..007ec267c 100644
--- a/lib/promscrape/config.go
+++ b/lib/promscrape/config.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/url"
-	"os"
 	"path/filepath"
 	"strings"
 	"sync/atomic"
@@ -134,16 +133,14 @@ func loadConfig(path string) (cfg *Config, data []byte, err error) {
 	if err := cfgObj.parse(data, path); err != nil {
 		return nil, nil, fmt.Errorf("cannot parse Prometheus config from %q: %w", path, err)
 	}
-	if *dryRun {
-		// This is a dirty hack for checking Prometheus config only.
-		// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/362
-		// and https://github.com/VictoriaMetrics/VictoriaMetrics/issues/508 for details.
-		logger.Infof("Success: the config at %q has no errors; exitting with 0 status code", path)
-		os.Exit(0)
-	}
 	return &cfgObj, data, nil
 }
 
+// IsDryRun returns true if -promscrape.config.dryRun command-line flag is set
+func IsDryRun() bool {
+	return *dryRun
+}
+
 func (cfg *Config) parse(data []byte, path string) error {
 	if err := unmarshalMaybeStrict(data, cfg); err != nil {
 		return fmt.Errorf("cannot unmarshal data: %w", err)
diff --git a/lib/promscrape/discovery/consul/api.go b/lib/promscrape/discovery/consul/api.go
index 8010616ac..f041a06d1 100644
--- a/lib/promscrape/discovery/consul/api.go
+++ b/lib/promscrape/discovery/consul/api.go
@@ -3,23 +3,21 @@ package consul
 import (
 	"fmt"
 	"io/ioutil"
-	"net/url"
 	"os"
+	"strconv"
 	"strings"
 
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+	"github.com/VictoriaMetrics/fasthttp"
 )
 
 // apiConfig contains config for API server
+// with consulWatch service.
 type apiConfig struct {
-	client       *discoveryutils.Client
-	tagSeparator string
-	services     []string
-	tags         []string
-	datacenter   string
-	allowStale   bool
-	nodeMeta     map[string]string
+	tagSeparator  string
+	consulWatcher *consulWatch
 }
 
 var configMap = discoveryutils.NewConfigMap()
@@ -72,15 +70,14 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
 	if err != nil {
 		return nil, err
 	}
-	cfg := &apiConfig{
-		client: client,
 
-		tagSeparator: tagSeparator,
-		services:     sdc.Services,
-		tags:         sdc.Tags,
-		datacenter:   dc,
-		allowStale:   sdc.AllowStale,
-		nodeMeta:     sdc.NodeMeta,
+	cw, err := newConsulWatch(client, sdc, dc)
+	if err != nil {
+		return nil, fmt.Errorf("cannot start consul watcher: %w", err)
+	}
+	cfg := &apiConfig{
+		tagSeparator:  tagSeparator,
+		consulWatcher: cw,
 	}
 	return cfg, nil
 }
@@ -117,20 +114,49 @@ func getDatacenter(client *discoveryutils.Client, dc string) (string, error) {
 	return a.Config.Datacenter, nil
 }
 
-func getAPIResponse(cfg *apiConfig, path string) ([]byte, error) {
-	separator := "?"
-	if strings.Contains(path, "?") {
-		separator = "&"
-	}
-	path += fmt.Sprintf("%sdc=%s", separator, url.QueryEscape(cfg.datacenter))
-	if cfg.allowStale {
-		// See https://www.consul.io/api/features/consistency
-		path += "&stale"
-	}
-	if len(cfg.nodeMeta) > 0 {
-		for k, v := range cfg.nodeMeta {
-			path += fmt.Sprintf("&node-meta=%s", url.QueryEscape(k+":"+v))
+// returns ServiceNodesState and version index.
+func getServiceState(client *discoveryutils.Client, svc, baseArgs string, index uint64) ([]ServiceNode, uint64, error) {
+	path := fmt.Sprintf("/v1/health/service/%s%s", svc, baseArgs)
+
+	data, newIndex, err := getBlockingAPIResponse(client, path, index)
+	if err != nil {
+		return nil, index, err
+	}
+	sns, err := parseServiceNodes(data)
+	if err != nil {
+		return nil, index, err
+	}
+	return sns, newIndex, nil
+}
+
+// returns consul api response with new index version of object.
+// https://www.consul.io/api-docs/features/blocking
+func getBlockingAPIResponse(client *discoveryutils.Client, path string, index uint64) ([]byte, uint64, error) {
+	path += "&index=" + strconv.FormatUint(index, 10)
+	path = path + fmt.Sprintf("&wait=%s", watchTime)
+	getMeta := func(resp *fasthttp.Response) {
+		if ind := resp.Header.Peek("X-Consul-Index"); len(ind) > 0 {
+			newIndex, err := strconv.ParseUint(string(ind), 10, 64)
+			if err != nil {
+				logger.Errorf("failed to parse consul index: %v", err)
+				return
+			}
+			// reset index
+			// https://www.consul.io/api-docs/features/blocking#implementation-details
+			if newIndex < 1 {
+				index = 1
+				return
+			}
+			if index > newIndex {
+				index = 0
+				return
+			}
+			index = newIndex
 		}
 	}
-	return cfg.client.GetAPIResponse(path)
+	data, err := client.GetBlockingAPIResponse(path, getMeta)
+	if err != nil {
+		return nil, index, fmt.Errorf("failed query consul api path=%q, err=%w", path, err)
+	}
+	return data, index, nil
 }
diff --git a/lib/promscrape/discovery/consul/consul.go b/lib/promscrape/discovery/consul/consul.go
index 735190839..72ed464ac 100644
--- a/lib/promscrape/discovery/consul/consul.go
+++ b/lib/promscrape/discovery/consul/consul.go
@@ -1,11 +1,25 @@
 package consul
 
 import (
+	"flag"
 	"fmt"
+	"time"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
 )
 
+var (
+	// SDCheckInterval - check interval for consul discovery.
+	SDCheckInterval = flag.Duration("promscrape.consulSDCheckInterval", 30*time.Second, "Interval for checking for changes in consul. "+
+		"This works only if `consul_sd_configs` is configured in '-promscrape.config' file. "+
+		"See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config for details")
+	// duration for consul blocking request, maximum wait time is 10 min.
+	// But fasthttp client has readTimeout for 1 min, so we use 50s timeout.
+	// also consul adds random delay up to wait/16, so there is no need in jitter.
+	// https://www.consul.io/api-docs/features/blocking
+	watchTime = time.Second * 50
+)
+
 // SDConfig represents service discovery config for Consul.
 //
 // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config
diff --git a/lib/promscrape/discovery/consul/service_node.go b/lib/promscrape/discovery/consul/service_node.go
index 2b3b9821a..ae332091b 100644
--- a/lib/promscrape/discovery/consul/service_node.go
+++ b/lib/promscrape/discovery/consul/service_node.go
@@ -3,19 +3,15 @@ package consul
 import (
 	"encoding/json"
 	"fmt"
-	"net/url"
 	"strconv"
 	"strings"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
 )
 
-// getServiceNodesLabels returns labels for Consul service nodes obtained from the given cfg
+// getServiceNodesLabels returns labels for Consul service nodes with given tagSeparator.
 func getServiceNodesLabels(cfg *apiConfig) ([]map[string]string, error) {
-	sns, err := getAllServiceNodes(cfg)
-	if err != nil {
-		return nil, err
-	}
+	sns := cfg.consulWatcher.getServiceNodes()
 	var ms []map[string]string
 	for _, sn := range sns {
 		ms = sn.appendTargetLabels(ms, cfg.tagSeparator)
@@ -23,113 +19,6 @@ func getServiceNodesLabels(cfg *apiConfig) ([]map[string]string, error) {
 	return ms, nil
 }
 
-func getAllServiceNodes(cfg *apiConfig) ([]ServiceNode, error) {
-	// Obtain a list of services
-	// See https://www.consul.io/api/catalog.html#list-services
-	data, err := getAPIResponse(cfg, "/v1/catalog/services")
-	if err != nil {
-		return nil, fmt.Errorf("cannot obtain services: %w", err)
-	}
-	var m map[string][]string
-	if err := json.Unmarshal(data, &m); err != nil {
-		return nil, fmt.Errorf("cannot parse services response %q: %w", data, err)
-	}
-	serviceNames := make(map[string]bool)
-	for serviceName, tags := range m {
-		if !shouldCollectServiceByName(cfg.services, serviceName) {
-			continue
-		}
-		if !shouldCollectServiceByTags(cfg.tags, tags) {
-			continue
-		}
-		serviceNames[serviceName] = true
-	}
-
-	// Query all the serviceNames in parallel
-	type response struct {
-		sns []ServiceNode
-		err error
-	}
-	responsesCh := make(chan response, len(serviceNames))
-	for serviceName := range serviceNames {
-		go func(serviceName string) {
-			sns, err := getServiceNodes(cfg, serviceName)
-			responsesCh <- response{
-				sns: sns,
-				err: err,
-			}
-		}(serviceName)
-	}
-	var sns []ServiceNode
-	err = nil
-	for i := 0; i < len(serviceNames); i++ {
-		resp := <-responsesCh
-		if resp.err != nil && err == nil {
-			err = resp.err
-		}
-		sns = append(sns, resp.sns...)
-	}
-	if err != nil {
-		return nil, err
-	}
-	return sns, nil
-}
-
-func shouldCollectServiceByName(filterServices []string, service string) bool {
-	if len(filterServices) == 0 {
-		return true
-	}
-	for _, filterService := range filterServices {
-		if filterService == service {
-			return true
-		}
-	}
-	return false
-}
-
-func shouldCollectServiceByTags(filterTags, tags []string) bool {
-	if len(filterTags) == 0 {
-		return true
-	}
-	for _, filterTag := range filterTags {
-		hasTag := false
-		for _, tag := range tags {
-			if tag == filterTag {
-				hasTag = true
-				break
-			}
-		}
-		if !hasTag {
-			return false
-		}
-	}
-	return true
-}
-
-func getServiceNodes(cfg *apiConfig, serviceName string) ([]ServiceNode, error) {
-	// See https://www.consul.io/api/health.html#list-nodes-for-service
-	path := fmt.Sprintf("/v1/health/service/%s", serviceName)
-	// The /v1/health/service/:service endpoint supports background refresh caching,
-	// which guarantees fresh results obtained from local Consul agent.
-	// See https://www.consul.io/api-docs/health#list-nodes-for-service
-	// and https://www.consul.io/api/features/caching for details.
-	// Query cached results in order to reduce load on Consul cluster.
-	// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/574 .
-	path += "?cached"
-	var tagsArgs []string
-	for _, tag := range cfg.tags {
-		tagsArgs = append(tagsArgs, fmt.Sprintf("tag=%s", url.QueryEscape(tag)))
-	}
-	if len(tagsArgs) > 0 {
-		path += "&" + strings.Join(tagsArgs, "&")
-	}
-	data, err := getAPIResponse(cfg, path)
-	if err != nil {
-		return nil, fmt.Errorf("cannot obtain instances for serviceName=%q: %w", serviceName, err)
-	}
-	return parseServiceNodes(data)
-}
-
 // ServiceNode is Consul service node.
 //
 // See https://www.consul.io/api/health.html#list-nodes-for-service
diff --git a/lib/promscrape/discovery/consul/watch.go b/lib/promscrape/discovery/consul/watch.go
new file mode 100644
index 000000000..80dd39e70
--- /dev/null
+++ b/lib/promscrape/discovery/consul/watch.go
@@ -0,0 +1,235 @@
+package consul
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+type serviceWatch struct {
+	stopCh       chan struct{}
+	serviceNodes []ServiceNode
+}
+
+// watcher for consul api, updates targets in background with long-polling.
+type consulWatch struct {
+	baseQueryArgs       string
+	client              *discoveryutils.Client
+	lastAccessTime      atomic.Value
+	nodeMeta            string
+	shouldWatchServices []string
+	shouldWatchTags     []string
+	// guards services
+	servicesLock sync.Mutex
+	services     map[string]serviceWatch
+	stopCh       chan struct{}
+}
+
+// init new watcher and start background service discovery for Consul.
+func newConsulWatch(client *discoveryutils.Client, sdc *SDConfig, datacenter string) (*consulWatch, error) {
+	baseQueryArgs := fmt.Sprintf("?sdc=%s", url.QueryEscape(datacenter))
+	var nodeMeta string
+	if len(sdc.NodeMeta) > 0 {
+		for k, v := range sdc.NodeMeta {
+			nodeMeta += fmt.Sprintf("&node-meta=%s", url.QueryEscape(k+":"+v))
+		}
+	}
+	if sdc.AllowStale {
+		baseQueryArgs += "&stale"
+	}
+	cw := consulWatch{
+		client:              client,
+		baseQueryArgs:       baseQueryArgs,
+		shouldWatchServices: sdc.Services,
+		shouldWatchTags:     sdc.Tags,
+		services:            make(map[string]serviceWatch),
+	}
+
+	watchServiceNames, _, err := cw.getServiceNames(0)
+	if err != nil {
+		return nil, err
+	}
+	var wg sync.WaitGroup
+	cw.servicesLock.Lock()
+	for serviceName := range watchServiceNames {
+		stopCh := make(chan struct{})
+		cw.services[serviceName] = serviceWatch{stopCh: stopCh}
+		wg.Add(1)
+		go func(serviceName string) {
+			defer wg.Done()
+			cw.watchForServiceUpdates(serviceName, stopCh)
+		}(serviceName)
+	}
+	cw.servicesLock.Unlock()
+	// wait for first init.
+	wg.Wait()
+	go cw.watchForServices()
+	return &cw, nil
+}
+
+// stops all service watchers.
+func (cw *consulWatch) stopServiceWatchersAll() {
+	cw.servicesLock.Lock()
+	for _, sw := range cw.services {
+		close(sw.stopCh)
+	}
+	cw.servicesLock.Unlock()
+}
+
+// getServiceNames returns serviceNames and index version.
+func (cw *consulWatch) getServiceNames(index uint64) (map[string]struct{}, uint64, error) {
+	sns := make(map[string]struct{})
+	path := fmt.Sprintf("/v1/catalog/services%s", cw.baseQueryArgs)
+	if len(cw.nodeMeta) > 0 {
+		path += cw.nodeMeta
+	}
+	data, newIndex, err := getBlockingAPIResponse(cw.client, path, index)
+	if err != nil {
+		return nil, index, err
+	}
+	var m map[string][]string
+	if err := json.Unmarshal(data, &m); err != nil {
+		return nil, index, fmt.Errorf("cannot parse services response=%q, err=%w", data, err)
+	}
+	for k, tags := range m {
+		if !shouldCollectServiceByName(cw.shouldWatchServices, k) {
+			continue
+		}
+		if !shouldCollectServiceByTags(cw.shouldWatchTags, tags) {
+			continue
+		}
+		sns[k] = struct{}{}
+	}
+	return sns, newIndex, nil
+}
+
+// listen for new services and update it.
+func (cw *consulWatch) watchForServices() {
+	ticker := time.NewTicker(*SDCheckInterval)
+	defer ticker.Stop()
+	var index uint64
+	for {
+		select {
+		case <-cw.stopCh:
+			cw.stopServiceWatchersAll()
+			return
+		case <-ticker.C:
+			if time.Since(cw.lastAccessTime.Load().(time.Time)) > *SDCheckInterval*2 {
+				// exit watch and stop all background watchers.
+				cw.stopServiceWatchersAll()
+				return
+			}
+			m, newIndex, err := cw.getServiceNames(index)
+			if err != nil {
+				logger.Errorf("failed get serviceNames from consul api: err=%v", err)
+				continue
+			}
+			// nothing changed.
+			if index == newIndex {
+				continue
+			}
+			cw.servicesLock.Lock()
+			// start new services watchers.
+			for svc := range m {
+				if _, ok := cw.services[svc]; !ok {
+					stopCh := make(chan struct{})
+					cw.services[svc] = serviceWatch{stopCh: stopCh}
+					go cw.watchForServiceUpdates(svc, stopCh)
+				}
+			}
+			// stop watch for removed services.
+			for svc, s := range cw.services {
+				if _, ok := m[svc]; !ok {
+					close(s.stopCh)
+					delete(cw.services, svc)
+				}
+			}
+			cw.servicesLock.Unlock()
+			index = newIndex
+		}
+	}
+
+}
+
+// start watching for consul service changes.
+func (cw *consulWatch) watchForServiceUpdates(svc string, stopCh chan struct{}) {
+	ticker := time.NewTicker(*SDCheckInterval)
+	defer ticker.Stop()
+	updateServiceState := func(index uint64) uint64 {
+		sns, newIndex, err := getServiceState(cw.client, svc, cw.baseQueryArgs, index)
+		if err != nil {
+			logger.Errorf("failed update service state, service_name=%q, err=%v", svc, err)
+			return index
+		}
+		if newIndex == index {
+			return index
+		}
+		cw.servicesLock.Lock()
+		if s, ok := cw.services[svc]; ok {
+			s.serviceNodes = sns
+			cw.services[svc] = s
+		}
+		cw.servicesLock.Unlock()
+		return newIndex
+	}
+	watchIndex := updateServiceState(0)
+	go func() {
+		for {
+			select {
+			case <-ticker.C:
+				watchIndex = updateServiceState(watchIndex)
+			case <-stopCh:
+				return
+			}
+		}
+	}()
+}
+
+// returns ServiceNodes.
+func (cw *consulWatch) getServiceNodes() []ServiceNode {
+	var sns []ServiceNode
+	cw.servicesLock.Lock()
+	for _, v := range cw.services {
+		sns = append(sns, v.serviceNodes...)
+	}
+	cw.servicesLock.Unlock()
+	cw.lastAccessTime.Store(time.Now())
+	return sns
+}
+
+func shouldCollectServiceByName(filterServices []string, service string) bool {
+	if len(filterServices) == 0 {
+		return true
+	}
+	for _, filterService := range filterServices {
+		if filterService == service {
+			return true
+		}
+	}
+	return false
+}
+
+func shouldCollectServiceByTags(filterTags, tags []string) bool {
+	if len(filterTags) == 0 {
+		return true
+	}
+	for _, filterTag := range filterTags {
+		hasTag := false
+		for _, tag := range tags {
+			if tag == filterTag {
+				hasTag = true
+				break
+			}
+		}
+		if !hasTag {
+			return false
+		}
+	}
+	return true
+}
diff --git a/lib/promscrape/discovery/dockerswarm/api.go b/lib/promscrape/discovery/dockerswarm/api.go
index 25dffe207..3853600a4 100644
--- a/lib/promscrape/discovery/dockerswarm/api.go
+++ b/lib/promscrape/discovery/dockerswarm/api.go
@@ -1,8 +1,12 @@
 package dockerswarm
 
 import (
+	"encoding/json"
 	"fmt"
+	"net/url"
+	"strings"
 
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
 )
@@ -12,6 +16,9 @@ var configMap = discoveryutils.NewConfigMap()
 type apiConfig struct {
 	client *discoveryutils.Client
 	port   int
+
+	// filtersQueryArg contains escaped `filters` query arg to add to each request to Docker Swarm API.
+	filtersQueryArg string
 }
 
 func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
@@ -24,7 +31,8 @@ func getAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
 
 func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
 	cfg := &apiConfig{
-		port: sdc.Port,
+		port:            sdc.Port,
+		filtersQueryArg: getFiltersQueryArg(sdc.Filters),
 	}
 	ac, err := promauth.NewConfig(baseDir, sdc.BasicAuth, sdc.BearerToken, sdc.BearerTokenFile, sdc.TLSConfig)
 	if err != nil {
@@ -37,3 +45,36 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
 	cfg.client = client
 	return cfg, nil
 }
+
+func (cfg *apiConfig) getAPIResponse(path string) ([]byte, error) {
+	if len(cfg.filtersQueryArg) > 0 {
+		separator := "?"
+		if strings.Contains(path, "?") {
+			separator = "&"
+		}
+		path += separator + "filters=" + cfg.filtersQueryArg
+	}
+	return cfg.client.GetAPIResponse(path)
+}
+
+func getFiltersQueryArg(filters []Filter) string {
+	if len(filters) == 0 {
+		return ""
+	}
+	m := make(map[string]map[string]bool)
+	for _, f := range filters {
+		x := m[f.Name]
+		if x == nil {
+			x = make(map[string]bool)
+			m[f.Name] = x
+		}
+		for _, value := range f.Values {
+			x[value] = true
+		}
+	}
+	buf, err := json.Marshal(m)
+	if err != nil {
+		logger.Panicf("BUG: unexpected error in json.Marshal: %s", err)
+	}
+	return url.QueryEscape(string(buf))
+}
diff --git a/lib/promscrape/discovery/dockerswarm/dockerswarm.go b/lib/promscrape/discovery/dockerswarm/dockerswarm.go
index 3d49365a3..61b4b02f6 100644
--- a/lib/promscrape/discovery/dockerswarm/dockerswarm.go
+++ b/lib/promscrape/discovery/dockerswarm/dockerswarm.go
@@ -10,17 +10,25 @@ import (
 //
 // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dockerswarm_sd_config
 type SDConfig struct {
-	Host string `yaml:"host"`
+	Host    string   `yaml:"host"`
+	Role    string   `yaml:"role"`
+	Port    int      `yaml:"port,omitempty"`
+	Filters []Filter `yaml:"filters,omitempty"`
+
 	// TODO: add support for proxy_url
 	TLSConfig *promauth.TLSConfig `yaml:"tls_config,omitempty"`
-	Role      string              `yaml:"role"`
-	Port      int                 `yaml:"port,omitempty"`
 	// refresh_interval is obtained from `-promscrape.dockerswarmSDCheckInterval` command-line option
 	BasicAuth       *promauth.BasicAuthConfig `yaml:"basic_auth,omitempty"`
 	BearerToken     string                    `yaml:"bearer_token,omitempty"`
 	BearerTokenFile string                    `yaml:"bearer_token_file,omitempty"`
 }
 
+// Filter is a filter, which can be passed to SDConfig.
+type Filter struct {
+	Name   string   `yaml:"name"`
+	Values []string `yaml:"values"`
+}
+
 // GetLabels returns dockerswarm labels according to sdc.
 func GetLabels(sdc *SDConfig, baseDir string) ([]map[string]string, error) {
 	cfg, err := getAPIConfig(sdc, baseDir)
diff --git a/lib/promscrape/discovery/dockerswarm/network.go b/lib/promscrape/discovery/dockerswarm/network.go
index bcf21dd0a..27bb748cb 100644
--- a/lib/promscrape/discovery/dockerswarm/network.go
+++ b/lib/promscrape/discovery/dockerswarm/network.go
@@ -27,7 +27,7 @@ func getNetworksLabelsByNetworkID(cfg *apiConfig) (map[string]map[string]string,
 }
 
 func getNetworks(cfg *apiConfig) ([]network, error) {
-	resp, err := cfg.client.GetAPIResponse("/networks")
+	resp, err := cfg.getAPIResponse("/networks")
 	if err != nil {
 		return nil, fmt.Errorf("cannot query dockerswarm api for networks: %w", err)
 	}
diff --git a/lib/promscrape/discovery/dockerswarm/nodes.go b/lib/promscrape/discovery/dockerswarm/nodes.go
index d5eec44cc..c6db715f2 100644
--- a/lib/promscrape/discovery/dockerswarm/nodes.go
+++ b/lib/promscrape/discovery/dockerswarm/nodes.go
@@ -46,7 +46,7 @@ func getNodesLabels(cfg *apiConfig) ([]map[string]string, error) {
 }
 
 func getNodes(cfg *apiConfig) ([]node, error) {
-	resp, err := cfg.client.GetAPIResponse("/nodes")
+	resp, err := cfg.getAPIResponse("/nodes")
 	if err != nil {
 		return nil, fmt.Errorf("cannot query dockerswarm api for nodes: %w", err)
 	}
diff --git a/lib/promscrape/discovery/dockerswarm/services.go b/lib/promscrape/discovery/dockerswarm/services.go
index 147c610cc..29e4f0dcb 100644
--- a/lib/promscrape/discovery/dockerswarm/services.go
+++ b/lib/promscrape/discovery/dockerswarm/services.go
@@ -59,7 +59,7 @@ func getServicesLabels(cfg *apiConfig) ([]map[string]string, error) {
 }
 
 func getServices(cfg *apiConfig) ([]service, error) {
-	data, err := cfg.client.GetAPIResponse("/services")
+	data, err := cfg.getAPIResponse("/services")
 	if err != nil {
 		return nil, fmt.Errorf("cannot query dockerswarm api for services: %w", err)
 	}
diff --git a/lib/promscrape/discovery/dockerswarm/tasks.go b/lib/promscrape/discovery/dockerswarm/tasks.go
index e8724c2cb..ecf69e4a5 100644
--- a/lib/promscrape/discovery/dockerswarm/tasks.go
+++ b/lib/promscrape/discovery/dockerswarm/tasks.go
@@ -58,7 +58,7 @@ func getTasksLabels(cfg *apiConfig) ([]map[string]string, error) {
 }
 
 func getTasks(cfg *apiConfig) ([]task, error) {
-	resp, err := cfg.client.GetAPIResponse("/tasks")
+	resp, err := cfg.getAPIResponse("/tasks")
 	if err != nil {
 		return nil, fmt.Errorf("cannot query dockerswarm api for tasks: %w", err)
 	}
diff --git a/lib/promscrape/discovery/ec2/instance.go b/lib/promscrape/discovery/ec2/instance.go
index 7dbf6203a..45ccfb0cf 100644
--- a/lib/promscrape/discovery/ec2/instance.go
+++ b/lib/promscrape/discovery/ec2/instance.go
@@ -104,7 +104,13 @@ type NetworkInterfaceSet struct {
 
 // NetworkInterface represents NetworkInterface from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_InstanceNetworkInterface.html
 type NetworkInterface struct {
-	SubnetID string `xml:"subnetId"`
+	SubnetID         string           `xml:"subnetId"`
+	IPv6AddressesSet Ipv6AddressesSet `xml:"ipv6AddressesSet"`
+}
+
+// Ipv6AddressesSet represents ipv6AddressesSet from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_InstanceNetworkInterface.html
+type Ipv6AddressesSet struct {
+	Items []string `xml:"item"`
 }
 
 // TagSet represents TagSet from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Instance.html
@@ -151,21 +157,27 @@ func (inst *Instance) appendTargetLabels(ms []map[string]string, ownerID string,
 		"__meta_ec2_vpc_id":             inst.VPCID,
 	}
 	if len(inst.VPCID) > 0 {
-		// Deduplicate VPC Subnet IDs maintaining the order of the network interfaces returned by EC2.
 		subnets := make([]string, 0, len(inst.NetworkInterfaceSet.Items))
 		seenSubnets := make(map[string]bool, len(inst.NetworkInterfaceSet.Items))
+		var ipv6Addrs []string
 		for _, ni := range inst.NetworkInterfaceSet.Items {
 			if len(ni.SubnetID) == 0 {
 				continue
 			}
+			// Deduplicate VPC Subnet IDs maintaining the order of the network interfaces returned by EC2.
 			if !seenSubnets[ni.SubnetID] {
 				seenSubnets[ni.SubnetID] = true
 				subnets = append(subnets, ni.SubnetID)
 			}
+			// Collect ipv6 addresses
+			ipv6Addrs = append(ipv6Addrs, ni.IPv6AddressesSet.Items...)
 		}
 		// We surround the separated list with the separator as well. This way regular expressions
 		// in relabeling rules don't have to consider tag positions.
 		m["__meta_ec2_subnet_id"] = "," + strings.Join(subnets, ",") + ","
+		if len(ipv6Addrs) > 0 {
+			m["__meta_ec2_ipv6_addresses"] = "," + strings.Join(ipv6Addrs, ",") + ","
+		}
 	}
 	for _, t := range inst.TagSet.Items {
 		if len(t.Key) == 0 || len(t.Value) == 0 {
diff --git a/lib/promscrape/discoveryutils/client.go b/lib/promscrape/discoveryutils/client.go
index 014edced4..ab772dc1c 100644
--- a/lib/promscrape/discoveryutils/client.go
+++ b/lib/promscrape/discoveryutils/client.go
@@ -33,10 +33,12 @@ func GetHTTPClient() *http.Client {
 
 // Client is http client, which talks to the given apiServer.
 type Client struct {
-	hc        *fasthttp.HostClient
-	ac        *promauth.Config
-	apiServer string
-	hostPort  string
+	hc *fasthttp.HostClient
+	// blockingClient is used for performing long-polling requests.
+	blockingClient *fasthttp.HostClient
+	ac             *promauth.Config
+	apiServer      string
+	hostPort       string
 }
 
 // NewClient returns new Client for the given apiServer and the given ac.
@@ -80,11 +82,24 @@ func NewClient(apiServer string, ac *promauth.Config) (*Client, error) {
 		MaxConns:            2 * *maxConcurrency,
 		Dial:                dialFunc,
 	}
+	wc := &fasthttp.HostClient{
+		Addr:                hostPort,
+		Name:                "vm_promscrape/discovery",
+		DialDualStack:       netutil.TCP6Enabled(),
+		IsTLS:               isTLS,
+		TLSConfig:           tlsCfg,
+		ReadTimeout:         time.Minute * 3,
+		WriteTimeout:        10 * time.Second,
+		MaxResponseBodySize: 300 * 1024 * 1024,
+		MaxConns:            20 * *maxConcurrency,
+		Dial:                dialFunc,
+	}
 	return &Client{
-		hc:        hc,
-		ac:        ac,
-		apiServer: apiServer,
-		hostPort:  hostPort,
+		hc:             hc,
+		blockingClient: wc,
+		ac:             ac,
+		apiServer:      apiServer,
+		hostPort:       hostPort,
 	}, nil
 }
 
@@ -97,6 +112,12 @@ func concurrencyLimitChInit() {
 	concurrencyLimitCh = make(chan struct{}, *maxConcurrency)
 }
 
+// APIRequestParams modifies api request with given params.
+type APIRequestParams struct {
+	FetchFromResponse func(resp *fasthttp.Response)
+	SetToRequest      func(req *fasthttp.Request)
+}
+
 // GetAPIResponse returns response for the given absolute path.
 func (c *Client) GetAPIResponse(path string) ([]byte, error) {
 	// Limit the number of concurrent API requests.
@@ -111,7 +132,17 @@ func (c *Client) GetAPIResponse(path string) ([]byte, error) {
 			c.apiServer, *maxWaitTime, *maxConcurrency)
 	}
 	defer func() { <-concurrencyLimitCh }()
+	return c.getAPIResponseWithParamsAndClient(c.hc, path, nil)
+}
 
+// GetBlockingAPIResponse returns response for given absolute path with blocking client and optional callback for api response,
+// inspectResponse - should never reference data from response.
+func (c *Client) GetBlockingAPIResponse(path string, inspectResponse func(resp *fasthttp.Response)) ([]byte, error) {
+	return c.getAPIResponseWithParamsAndClient(c.blockingClient, path, inspectResponse)
+}
+
+// getAPIResponseWithParamsAndClient returns response for the given absolute path with optional callback for response.
+func (c *Client) getAPIResponseWithParamsAndClient(client *fasthttp.HostClient, path string, inspectResponse func(resp *fasthttp.Response)) ([]byte, error) {
 	requestURL := c.apiServer + path
 	var u fasthttp.URI
 	u.Update(requestURL)
@@ -122,9 +153,10 @@ func (c *Client) GetAPIResponse(path string) ([]byte, error) {
 	if c.ac != nil && c.ac.Authorization != "" {
 		req.Header.Set("Authorization", c.ac.Authorization)
 	}
+
 	var resp fasthttp.Response
-	deadline := time.Now().Add(c.hc.ReadTimeout)
-	if err := doRequestWithPossibleRetry(c.hc, &req, &resp, deadline); err != nil {
+	deadline := time.Now().Add(client.ReadTimeout)
+	if err := doRequestWithPossibleRetry(client, &req, &resp, deadline); err != nil {
 		return nil, fmt.Errorf("cannot fetch %q: %w", requestURL, err)
 	}
 	var data []byte
@@ -137,6 +169,9 @@ func (c *Client) GetAPIResponse(path string) ([]byte, error) {
 	} else {
 		data = append(data[:0], resp.Body()...)
 	}
+	if inspectResponse != nil {
+		inspectResponse(&resp)
+	}
 	statusCode := resp.StatusCode()
 	if statusCode != fasthttp.StatusOK {
 		return nil, fmt.Errorf("unexpected status code returned from %q: %d; expecting %d; response body: %q",
diff --git a/lib/promscrape/scraper.go b/lib/promscrape/scraper.go
index 40f82860a..ccb0d4aaf 100644
--- a/lib/promscrape/scraper.go
+++ b/lib/promscrape/scraper.go
@@ -11,6 +11,7 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/consul"
 	"github.com/VictoriaMetrics/metrics"
 )
 
@@ -25,9 +26,6 @@ var (
 	openstackSDCheckInterval = flag.Duration("promscrape.openstackSDCheckInterval", 30*time.Second, "Interval for checking for changes in openstack API server. "+
 		"This works only if `openstack_sd_configs` is configured in '-promscrape.config' file. "+
 		"See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config for details")
-	consulSDCheckInterval = flag.Duration("promscrape.consulSDCheckInterval", 30*time.Second, "Interval for checking for changes in consul. "+
-		"This works only if `consul_sd_configs` is configured in '-promscrape.config' file. "+
-		"See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config for details")
 	eurekaSDCheckInterval = flag.Duration("promscrape.eurekaSDCheckInterval", 30*time.Second, "Interval for checking for changes in eureka. "+
 		"This works only if `eureka_sd_configs` is configured in '-promscrape.config' file. "+
 		"See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#eureka_sd_config for details")
@@ -101,7 +99,7 @@ func runScraper(configFile string, pushData func(wr *prompbmarshal.WriteRequest)
 	scs.add("file_sd_configs", *fileSDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getFileSDScrapeWork(swsPrev) })
 	scs.add("kubernetes_sd_configs", *kubernetesSDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getKubernetesSDScrapeWork(swsPrev) })
 	scs.add("openstack_sd_configs", *openstackSDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getOpenStackSDScrapeWork(swsPrev) })
-	scs.add("consul_sd_configs", *consulSDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getConsulSDScrapeWork(swsPrev) })
+	scs.add("consul_sd_configs", *consul.SDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getConsulSDScrapeWork(swsPrev) })
 	scs.add("eureka_sd_configs", *eurekaSDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getEurekaSDScrapeWork(swsPrev) })
 	scs.add("dns_sd_configs", *dnsSDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getDNSSDScrapeWork(swsPrev) })
 	scs.add("ec2_sd_configs", *ec2SDCheckInterval, func(cfg *Config, swsPrev []ScrapeWork) []ScrapeWork { return cfg.getEC2SDScrapeWork(swsPrev) })
diff --git a/lib/promscrape/scrapework.go b/lib/promscrape/scrapework.go
index 30b0dea59..864b2cbe0 100644
--- a/lib/promscrape/scrapework.go
+++ b/lib/promscrape/scrapework.go
@@ -249,7 +249,7 @@ func (sw *scrapeWork) scrapeInternal(scrapeTimestamp, realTimestamp int64) error
 
 	// Common case: read all the data from scrape target to memory (body) and then process it.
 	// This case should work more optimally for than stream parse code above for common case when scrape target exposes
-	// up to a few thouthand metrics.
+	// up to a few thousand metrics.
 	body := leveledbytebufferpool.Get(sw.prevBodyLen)
 	var err error
 	body.B, err = sw.ReadData(body.B[:0])
diff --git a/lib/protoparser/csvimport/parser.go b/lib/protoparser/csvimport/parser.go
index 3e5126c1e..004d581b6 100644
--- a/lib/protoparser/csvimport/parser.go
+++ b/lib/protoparser/csvimport/parser.go
@@ -83,7 +83,7 @@ func parseRows(sc *scanner, dst []Row, tags []Tag, metrics []metric, cds []Colum
 		tagsLen := len(tags)
 		for sc.NextColumn() {
 			if col >= uint(len(cds)) {
-				// Skip superflouous column.
+				// Skip superfluous column.
 				continue
 			}
 			cd := &cds[col]
diff --git a/lib/protoparser/prometheus/parser.go b/lib/protoparser/prometheus/parser.go
index 4972c7bd6..9938ba229 100644
--- a/lib/protoparser/prometheus/parser.go
+++ b/lib/protoparser/prometheus/parser.go
@@ -70,6 +70,14 @@ func (r *Row) reset() {
 	r.Timestamp = 0
 }
 
+func skipTrailingComment(s string) string {
+	n := strings.IndexByte(s, '#')
+	if n < 0 {
+		return s
+	}
+	return s[:n]
+}
+
 func skipLeadingWhitespace(s string) string {
 	// Prometheus treats ' ' and '\t' as whitespace
 	// according to https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md#text-format-details
@@ -133,6 +141,7 @@ func (r *Row) unmarshal(s string, tagsPool []Tag, noEscapes bool) ([]Tag, error)
 		return tagsPool, fmt.Errorf("metric cannot be empty")
 	}
 	s = skipLeadingWhitespace(s)
+	s = skipTrailingComment(s)
 	if len(s) == 0 {
 		return tagsPool, fmt.Errorf("value cannot be empty")
 	}
@@ -146,18 +155,29 @@ func (r *Row) unmarshal(s string, tagsPool []Tag, noEscapes bool) ([]Tag, error)
 		r.Value = v
 		return tagsPool, nil
 	}
-	// There is timestamp.
+	// There is a timestamp.
 	v, err := fastfloat.Parse(s[:n])
 	if err != nil {
 		return tagsPool, fmt.Errorf("cannot parse value %q: %w", s[:n], err)
 	}
+	r.Value = v
 	s = skipLeadingWhitespace(s[n+1:])
-	ts, err := fastfloat.ParseInt64(s)
+	if len(s) == 0 {
+		// There is no timestamp - just a whitespace after the value.
+		return tagsPool, nil
+	}
+	ts, err := fastfloat.Parse(s)
 	if err != nil {
 		return tagsPool, fmt.Errorf("cannot parse timestamp %q: %w", s, err)
 	}
-	r.Value = v
-	r.Timestamp = ts
+	if ts >= -1<<31 && ts < 1<<31 {
+		// This looks like OpenMetrics timestamp in Unix seconds.
+		// Convert it to milliseconds.
+		//
+		// See https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md#timestamps
+		ts *= 1000
+	}
+	r.Timestamp = int64(ts)
 	return tagsPool, nil
 }
 
diff --git a/lib/storage/metric_name.go b/lib/storage/metric_name.go
index f70b83c2a..563bd1d5f 100644
--- a/lib/storage/metric_name.go
+++ b/lib/storage/metric_name.go
@@ -343,17 +343,17 @@ func hasTag(tags []string, key []byte) bool {
 }
 
 // String returns user-readable representation of the metric name.
-//
-// Use this function only for debug logging.
 func (mn *MetricName) String() string {
-	mn.sortTags()
+	var mnCopy MetricName
+	mnCopy.CopyFrom(mn)
+	mnCopy.sortTags()
 	var tags []string
-	for i := range mn.Tags {
-		t := &mn.Tags[i]
-		tags = append(tags, fmt.Sprintf("%q=%q", t.Key, t.Value))
+	for i := range mnCopy.Tags {
+		t := &mnCopy.Tags[i]
+		tags = append(tags, fmt.Sprintf("%s=%q", t.Key, t.Value))
 	}
-	tagsStr := strings.Join(tags, ", ")
-	return fmt.Sprintf("MetricGroup=%q, tags=[%s]", mn.MetricGroup, tagsStr)
+	tagsStr := strings.Join(tags, ",")
+	return fmt.Sprintf("%s{%s}", mnCopy.MetricGroup, tagsStr)
 }
 
 // Marshal appends marshaled mn to dst and returns the result.
@@ -419,7 +419,7 @@ var maxLabelsPerTimeseries = 30
 // SetMaxLabelsPerTimeseries sets the limit on the number of labels
 // per each time series.
 //
-// Superfouos labels are dropped.
+// Superfluous labels are dropped.
 func SetMaxLabelsPerTimeseries(maxLabels int) {
 	if maxLabels <= 0 {
 		logger.Panicf("BUG: maxLabels must be positive; got %d", maxLabels)
diff --git a/lib/storage/part.go b/lib/storage/part.go
index 2eb62b180..653a19311 100644
--- a/lib/storage/part.go
+++ b/lib/storage/part.go
@@ -60,27 +60,15 @@ func openFilePart(path string) (*part, error) {
 	}
 
 	timestampsPath := path + "/timestamps.bin"
-	timestampsFile, err := fs.OpenReaderAt(timestampsPath)
-	if err != nil {
-		return nil, fmt.Errorf("cannot open timestamps file: %w", err)
-	}
+	timestampsFile := fs.MustOpenReaderAt(timestampsPath)
 	timestampsSize := fs.MustFileSize(timestampsPath)
 
 	valuesPath := path + "/values.bin"
-	valuesFile, err := fs.OpenReaderAt(valuesPath)
-	if err != nil {
-		timestampsFile.MustClose()
-		return nil, fmt.Errorf("cannot open values file: %w", err)
-	}
+	valuesFile := fs.MustOpenReaderAt(valuesPath)
 	valuesSize := fs.MustFileSize(valuesPath)
 
 	indexPath := path + "/index.bin"
-	indexFile, err := fs.OpenReaderAt(indexPath)
-	if err != nil {
-		timestampsFile.MustClose()
-		valuesFile.MustClose()
-		return nil, fmt.Errorf("cannot open index file: %w", err)
-	}
+	indexFile := fs.MustOpenReaderAt(indexPath)
 	indexSize := fs.MustFileSize(indexPath)
 
 	metaindexPath := path + "/metaindex.bin"
@@ -261,7 +249,7 @@ func (ibc *indexBlockCache) Get(k uint64) *indexBlock {
 func (ibc *indexBlockCache) Put(k uint64, ib *indexBlock) {
 	ibc.mu.Lock()
 
-	// Clean superflouos cache entries.
+	// Clean superfluous cache entries.
 	if overflow := len(ibc.m) - getMaxCachedIndexBlocksPerPart(); overflow > 0 {
 		// Remove 10% of items from the cache.
 		overflow = int(float64(len(ibc.m)) * 0.1)
diff --git a/lib/storage/storage.go b/lib/storage/storage.go
index e474a0d20..07d8686b0 100644
--- a/lib/storage/storage.go
+++ b/lib/storage/storage.go
@@ -1238,9 +1238,10 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
 		if mr.Timestamp < minTimestamp {
 			// Skip rows with too small timestamps outside the retention.
 			if firstWarn == nil {
+				metricName := getUserReadableMetricName(mr.MetricNameRaw)
 				firstWarn = fmt.Errorf("cannot insert row with too small timestamp %d outside the retention; minimum allowed timestamp is %d; "+
-					"probably you need updating -retentionPeriod command-line flag",
-					mr.Timestamp, minTimestamp)
+					"probably you need updating -retentionPeriod command-line flag; metricName: %s",
+					mr.Timestamp, minTimestamp, metricName)
 			}
 			atomic.AddUint64(&s.tooSmallTimestampRows, 1)
 			continue
@@ -1248,9 +1249,9 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
 		if mr.Timestamp > maxTimestamp {
 			// Skip rows with too big timestamps significantly exceeding the current time.
 			if firstWarn == nil {
-				firstWarn = fmt.Errorf("cannot insert row with too big timestamp %d exceeding the current time; maximum allowd timestamp is %d; "+
-					"propbably you need updating -retentionPeriod command-line flag",
-					mr.Timestamp, maxTimestamp)
+				metricName := getUserReadableMetricName(mr.MetricNameRaw)
+				firstWarn = fmt.Errorf("cannot insert row with too big timestamp %d exceeding the current time; maximum allowed timestamp is %d; metricName: %s",
+					mr.Timestamp, maxTimestamp, metricName)
 			}
 			atomic.AddUint64(&s.tooBigTimestampRows, 1)
 			continue
@@ -1359,6 +1360,14 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
 	return rows, nil
 }
 
+func getUserReadableMetricName(metricNameRaw []byte) string {
+	var mn MetricName
+	if err := mn.unmarshalRaw(metricNameRaw); err != nil {
+		return fmt.Sprintf("cannot unmarshal metricNameRaw %q: %s", metricNameRaw, err)
+	}
+	return mn.String()
+}
+
 type pendingMetricRow struct {
 	MetricName []byte
 	mr         MetricRow
diff --git a/vendor/github.com/VictoriaMetrics/fasthttp/fs.go b/vendor/github.com/VictoriaMetrics/fasthttp/fs.go
index 0d83b6090..268021438 100644
--- a/vendor/github.com/VictoriaMetrics/fasthttp/fs.go
+++ b/vendor/github.com/VictoriaMetrics/fasthttp/fs.go
@@ -591,8 +591,8 @@ func (r *fsSmallFileReader) WriteTo(w io.Writer) (int64, error) {
 	}
 
 	curPos := r.startPos
-	bufv := copyBufPool.Get()
-	buf := bufv.([]byte)
+	bufv := copyBufPool.Get().(*copyBuf)
+	buf := bufv.b[:]
 	for err == nil {
 		tailLen := r.endPos - curPos
 		if tailLen <= 0 {
diff --git a/vendor/github.com/VictoriaMetrics/fasthttp/go.mod b/vendor/github.com/VictoriaMetrics/fasthttp/go.mod
index 46d90f7aa..e738de9b0 100644
--- a/vendor/github.com/VictoriaMetrics/fasthttp/go.mod
+++ b/vendor/github.com/VictoriaMetrics/fasthttp/go.mod
@@ -3,7 +3,7 @@ module github.com/VictoriaMetrics/fasthttp
 go 1.13
 
 require (
-	github.com/klauspost/compress v1.11.0
+	github.com/klauspost/compress v1.11.3
 	github.com/valyala/bytebufferpool v1.0.0
 	github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a
 )
diff --git a/vendor/github.com/VictoriaMetrics/fasthttp/go.sum b/vendor/github.com/VictoriaMetrics/fasthttp/go.sum
index dfeca7270..6fc4f7e42 100644
--- a/vendor/github.com/VictoriaMetrics/fasthttp/go.sum
+++ b/vendor/github.com/VictoriaMetrics/fasthttp/go.sum
@@ -1,5 +1,5 @@
-github.com/klauspost/compress v1.11.0 h1:wJbzvpYMVGG9iTI9VxpnNZfd4DzMPoCWze3GgSqz8yg=
-github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc=
+github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc=
diff --git a/vendor/github.com/VictoriaMetrics/fasthttp/http.go b/vendor/github.com/VictoriaMetrics/fasthttp/http.go
index 5fc3bee11..3c7fc56d1 100644
--- a/vendor/github.com/VictoriaMetrics/fasthttp/http.go
+++ b/vendor/github.com/VictoriaMetrics/fasthttp/http.go
@@ -1476,8 +1476,8 @@ type httpWriter interface {
 }
 
 func writeBodyChunked(w *bufio.Writer, r io.Reader) error {
-	vbuf := copyBufPool.Get()
-	buf := vbuf.([]byte)
+	bufv := copyBufPool.Get().(*copyBuf)
+	buf := bufv.b[:]
 
 	var err error
 	var n int
@@ -1500,7 +1500,7 @@ func writeBodyChunked(w *bufio.Writer, r io.Reader) error {
 		}
 	}
 
-	copyBufPool.Put(vbuf)
+	copyBufPool.Put(bufv)
 	return err
 }
 
@@ -1541,16 +1541,19 @@ func writeBodyFixedSize(w *bufio.Writer, r io.Reader, size int64) error {
 }
 
 func copyZeroAlloc(w io.Writer, r io.Reader) (int64, error) {
-	vbuf := copyBufPool.Get()
-	buf := vbuf.([]byte)
-	n, err := io.CopyBuffer(w, r, buf)
-	copyBufPool.Put(vbuf)
+	buf := copyBufPool.Get().(*copyBuf)
+	n, err := io.CopyBuffer(w, r, buf.b[:])
+	copyBufPool.Put(buf)
 	return n, err
 }
 
+type copyBuf struct {
+	b [4 * 4096]byte
+}
+
 var copyBufPool = sync.Pool{
 	New: func() interface{} {
-		return make([]byte, 4096)
+		return &copyBuf{}
 	},
 }
 
diff --git a/vendor/github.com/VictoriaMetrics/metricsql/lexer.go b/vendor/github.com/VictoriaMetrics/metricsql/lexer.go
index 7d82db2c4..f607e47a9 100644
--- a/vendor/github.com/VictoriaMetrics/metricsql/lexer.go
+++ b/vendor/github.com/VictoriaMetrics/metricsql/lexer.go
@@ -149,8 +149,14 @@ func scanString(s string) (string, error) {
 func scanPositiveNumber(s string) (string, error) {
 	// Scan integer part. It may be empty if fractional part exists.
 	i := 0
-	if n := scanSpecialIntegerPrefix(s); n > 0 {
-		i += n
+	skipChars, isHex := scanSpecialIntegerPrefix(s)
+	i += skipChars
+	if isHex {
+		// Scan integer hex number
+		for i < len(s) && isHexChar(s[i]) {
+			i++
+		}
+		return s[:i], nil
 	}
 	for i < len(s) && isDecimalChar(s[i]) {
 		i++
@@ -370,26 +376,31 @@ func isPositiveNumberPrefix(s string) bool {
 }
 
 func isSpecialIntegerPrefix(s string) bool {
-	return scanSpecialIntegerPrefix(s) > 0
+	skipChars, _ := scanSpecialIntegerPrefix(s)
+	return skipChars > 0
 }
 
-func scanSpecialIntegerPrefix(s string) int {
+func scanSpecialIntegerPrefix(s string) (skipChars int, isHex bool) {
 	if len(s) < 1 || s[0] != '0' {
-		return 0
+		return 0, false
 	}
 	s = strings.ToLower(s[1:])
 	if len(s) == 0 {
-		return 0
+		return 0, false
 	}
 	if isDecimalChar(s[0]) {
 		// octal number: 0123
-		return 1
+		return 1, false
 	}
-	if s[0] == 'x' || s[0] == 'o' || s[0] == 'b' {
+	if s[0] == 'x' {
+		// 0x
+		return 2, true
+	}
+	if s[0] == 'o' || s[0] == 'b' {
 		// 0x, 0o or 0b prefix
-		return 2
+		return 2, false
 	}
-	return 0
+	return 0, false
 }
 
 func isPositiveDuration(s string) bool {
@@ -536,6 +547,10 @@ func isDecimalChar(ch byte) bool {
 	return ch >= '0' && ch <= '9'
 }
 
+func isHexChar(ch byte) bool {
+	return isDecimalChar(ch) || ch >= 'a' && ch <= 'f' || ch >= 'A' && ch <= 'F'
+}
+
 func isIdentPrefix(s string) bool {
 	if len(s) == 0 {
 		return false
diff --git a/vendor/github.com/valyala/fastjson/fastfloat/parse.go b/vendor/github.com/valyala/fastjson/fastfloat/parse.go
index d17d7f2e6..5d4a7c7a7 100644
--- a/vendor/github.com/valyala/fastjson/fastfloat/parse.go
+++ b/vendor/github.com/valyala/fastjson/fastfloat/parse.go
@@ -237,7 +237,9 @@ func ParseBestEffort(s string) float64 {
 		if strings.HasPrefix(s, "+") {
 			s = s[1:]
 		}
-		if strings.EqualFold(s, "inf") {
+		// "infinity" is needed for OpenMetrics support.
+		// See https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md
+		if strings.EqualFold(s, "inf") || strings.EqualFold(s, "infinity") {
 			if minus {
 				return -inf
 			}
@@ -385,7 +387,9 @@ func Parse(s string) (float64, error) {
 		if strings.HasPrefix(ss, "+") {
 			ss = ss[1:]
 		}
-		if strings.EqualFold(ss, "inf") {
+		// "infinity" is needed for OpenMetrics support.
+		// See https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md
+		if strings.EqualFold(ss, "inf") || strings.EqualFold(ss, "infinity") {
 			if minus {
 				return -inf, nil
 			}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 2affcefa9..66637ebc8 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -10,13 +10,13 @@ cloud.google.com/go/internal/version
 cloud.google.com/go/storage
 # github.com/VictoriaMetrics/fastcache v1.5.7
 github.com/VictoriaMetrics/fastcache
-# github.com/VictoriaMetrics/fasthttp v1.0.7
+# github.com/VictoriaMetrics/fasthttp v1.0.9
 github.com/VictoriaMetrics/fasthttp
 github.com/VictoriaMetrics/fasthttp/fasthttputil
 github.com/VictoriaMetrics/fasthttp/stackless
 # github.com/VictoriaMetrics/metrics v1.12.3
 github.com/VictoriaMetrics/metrics
-# github.com/VictoriaMetrics/metricsql v0.7.2
+# github.com/VictoriaMetrics/metricsql v0.7.3
 github.com/VictoriaMetrics/metricsql
 github.com/VictoriaMetrics/metricsql/binaryop
 # github.com/aws/aws-sdk-go v1.35.31
@@ -99,7 +99,7 @@ github.com/klauspost/compress/zstd
 github.com/klauspost/compress/zstd/internal/xxhash
 # github.com/valyala/bytebufferpool v1.0.0
 github.com/valyala/bytebufferpool
-# github.com/valyala/fastjson v1.6.1
+# github.com/valyala/fastjson v1.6.3
 github.com/valyala/fastjson
 github.com/valyala/fastjson/fastfloat
 # github.com/valyala/fastrand v1.0.0


Test patch:
diff --git a/app/vmalert/config/config_test.go b/app/vmalert/config/config_test.go
index a631e6ddc..0539986e3 100644
--- a/app/vmalert/config/config_test.go
+++ b/app/vmalert/config/config_test.go
@@ -323,34 +323,55 @@ func TestHashRule(t *testing.T) {
 }
 
 func TestGroupChecksum(t *testing.T) {
-	data := `
+	f := func(t *testing.T, data, newData string) {
+		t.Helper()
+		var g Group
+		if err := yaml.Unmarshal([]byte(data), &g); err != nil {
+			t.Fatalf("failed to unmarshal: %s", err)
+		}
+		if g.Checksum == "" {
+			t.Fatalf("expected to get non-empty checksum")
+		}
+
+		var ng Group
+		if err := yaml.Unmarshal([]byte(newData), &ng); err != nil {
+			t.Fatalf("failed to unmarshal: %s", err)
+		}
+		if g.Checksum == ng.Checksum {
+			t.Fatalf("expected to get different checksums")
+		}
+	}
+	t.Run("Ok", func(t *testing.T) {
+		f(t, `
 name: TestGroup
 rules:
   - alert: ExampleAlertAlwaysFiring
     expr: sum by(job) (up == 1)
   - record: handler:requests:rate5m
     expr: sum(rate(prometheus_http_requests_total[5m])) by (handler)
-`
-	var g Group
-	if err := yaml.Unmarshal([]byte(data), &g); err != nil {
-		t.Fatalf("failed to unmarshal: %s", err)
-	}
-	if g.Checksum == "" {
-		t.Fatalf("expected to get non-empty checksum")
-	}
-	newData := `
+`, `
 name: TestGroup
 rules:
   - record: handler:requests:rate5m
     expr: sum(rate(prometheus_http_requests_total[5m])) by (handler)
   - alert: ExampleAlertAlwaysFiring
     expr: sum by(job) (up == 1)
-`
-	var ng Group
-	if err := yaml.Unmarshal([]byte(newData), &g); err != nil {
-		t.Fatalf("failed to unmarshal: %s", err)
-	}
-	if g.Checksum == ng.Checksum {
-		t.Fatalf("expected to get different checksums")
-	}
+`)
+	})
+
+	t.Run("Ok, `for` must change cs", func(t *testing.T) {
+		f(t, `
+name: TestGroup
+rules:
+  - alert: ExampleAlertWithFor
+    expr: sum by(job) (up == 1)
+    for: 5m
+`, `
+name: TestGroup
+rules:
+  - alert: ExampleAlertWithFor
+    expr: sum by(job) (up == 1)
+`)
+	})
+
 }
diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go
index 44a9cf70c..1c73109cd 100644
--- a/app/vmselect/promql/exec_test.go
+++ b/app/vmselect/promql/exec_test.go
@@ -526,6 +526,17 @@ func TestExecSuccess(t *testing.T) {
 		resultExpected := []netstorage.Result{r}
 		f(q, resultExpected)
 	})
+	t.Run(`minute(series_with_NaNs)`, func(t *testing.T) {
+		t.Parallel()
+		q := `minute(time() <= 1200 or time() > 1600)`
+		r := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{16, 20, nan, nan, 30, 33},
+			Timestamps: timestampsExpected,
+		}
+		resultExpected := []netstorage.Result{r}
+		f(q, resultExpected)
+	})
 	t.Run("rate({})", func(t *testing.T) {
 		t.Parallel()
 		q := `rate({})`
@@ -1772,10 +1783,32 @@ func TestExecSuccess(t *testing.T) {
 	})
 	t.Run(`time() > scalar`, func(t *testing.T) {
 		t.Parallel()
-		q := `time() > 123`
+		q := `time() > 1234`
 		r := netstorage.Result{
 			MetricName: metricNameExpected,
-			Values:     []float64{1000, 1200, 1400, 1600, 1800, 2000},
+			Values:     []float64{nan, nan, 1400, 1600, 1800, 2000},
+			Timestamps: timestampsExpected,
+		}
+		resultExpected := []netstorage.Result{r}
+		f(q, resultExpected)
+	})
+	t.Run(`time() >bool scalar`, func(t *testing.T) {
+		t.Parallel()
+		q := `time() >bool 1234`
+		r := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{0, 0, 1, 1, 1, 1},
+			Timestamps: timestampsExpected,
+		}
+		resultExpected := []netstorage.Result{r}
+		f(q, resultExpected)
+	})
+	t.Run(`nan >bool scalar1`, func(t *testing.T) {
+		t.Parallel()
+		q := `(time() > 1234) >bool 1450`
+		r := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{nan, nan, 0, 1, 1, 1},
 			Timestamps: timestampsExpected,
 		}
 		resultExpected := []netstorage.Result{r}
diff --git a/lib/fs/reader_at_test.go b/lib/fs/reader_at_test.go
index 10d84c738..4747030e7 100644
--- a/lib/fs/reader_at_test.go
+++ b/lib/fs/reader_at_test.go
@@ -22,10 +22,7 @@ func testReaderAt(t *testing.T, bufSize int) {
 		t.Fatalf("cannot create %q: %s", path, err)
 	}
 	defer MustRemoveAll(path)
-	r, err := OpenReaderAt(path)
-	if err != nil {
-		t.Fatalf("error in OpenReaderAt(%q): %s", path, err)
-	}
+	r := MustOpenReaderAt(path)
 	defer r.MustClose()
 
 	buf := make([]byte, bufSize)
diff --git a/lib/fs/reader_at_timing_test.go b/lib/fs/reader_at_timing_test.go
index 84eb1e65a..87b012e97 100644
--- a/lib/fs/reader_at_timing_test.go
+++ b/lib/fs/reader_at_timing_test.go
@@ -29,10 +29,7 @@ func benchmarkReaderAtMustReadAt(b *testing.B, isMmap bool) {
 		b.Fatalf("cannot create %q: %s", path, err)
 	}
 	defer MustRemoveAll(path)
-	r, err := OpenReaderAt(path)
-	if err != nil {
-		b.Fatalf("error in OpenReaderAt(%q): %s", path, err)
-	}
+	r := MustOpenReaderAt(path)
 	defer r.MustClose()
 
 	b.ResetTimer()
diff --git a/lib/mergeset/table_search_test.go b/lib/mergeset/table_search_test.go
index 56de4794e..18d769e21 100644
--- a/lib/mergeset/table_search_test.go
+++ b/lib/mergeset/table_search_test.go
@@ -136,7 +136,7 @@ func testTableSearchSerial(tb *Table, items []string) error {
 			n++
 		}
 		if ts.NextItem() {
-			return fmt.Errorf("superflouos item found at position %d when searching for %q: %q", n, key, ts.Item)
+			return fmt.Errorf("superfluous item found at position %d when searching for %q: %q", n, key, ts.Item)
 		}
 		if err := ts.Error(); err != nil {
 			return fmt.Errorf("unexpected error when searching for %q: %w", key, err)
diff --git a/lib/promscrape/discovery/dockerswarm/api_test.go b/lib/promscrape/discovery/dockerswarm/api_test.go
new file mode 100644
index 000000000..fc8888ee5
--- /dev/null
+++ b/lib/promscrape/discovery/dockerswarm/api_test.go
@@ -0,0 +1,26 @@
+package dockerswarm
+
+import (
+	"testing"
+)
+
+func TestGetFiltersQueryArg(t *testing.T) {
+	f := func(filters []Filter, queryArgExpected string) {
+		t.Helper()
+		queryArg := getFiltersQueryArg(filters)
+		if queryArg != queryArgExpected {
+			t.Fatalf("unexpected query arg; got %s; want %s", queryArg, queryArgExpected)
+		}
+	}
+	f(nil, "")
+	f([]Filter{
+		{
+			Name:   "name",
+			Values: []string{"foo", "bar"},
+		},
+		{
+			Name:   "xxx",
+			Values: []string{"aa"},
+		},
+	}, "%7B%22name%22%3A%7B%22bar%22%3Atrue%2C%22foo%22%3Atrue%7D%2C%22xxx%22%3A%7B%22aa%22%3Atrue%7D%7D")
+}
diff --git a/lib/promscrape/scrapework_test.go b/lib/promscrape/scrapework_test.go
index 9d1e54f92..861b77f39 100644
--- a/lib/promscrape/scrapework_test.go
+++ b/lib/promscrape/scrapework_test.go
@@ -65,7 +65,7 @@ func TestScrapeWorkScrapeInternalFailure(t *testing.T) {
 		pushDataCalls++
 	}
 
-	timestamp := int64(123)
+	timestamp := int64(123000)
 	if err := sw.scrapeInternal(timestamp, timestamp); err == nil {
 		t.Fatalf("expecting non-nil error")
 	}
@@ -112,7 +112,7 @@ func TestScrapeWorkScrapeInternalSuccess(t *testing.T) {
 			}
 		}
 
-		timestamp := int64(123)
+		timestamp := int64(123000)
 		if err := sw.scrapeInternal(timestamp, timestamp); err != nil {
 			t.Fatalf("unexpected error: %s", err)
 		}
diff --git a/lib/protoparser/csvimport/parser_test.go b/lib/protoparser/csvimport/parser_test.go
index 086588bbd..ea1fbcf2b 100644
--- a/lib/protoparser/csvimport/parser_test.go
+++ b/lib/protoparser/csvimport/parser_test.go
@@ -165,7 +165,7 @@ func TestRowsUnmarshalSuccess(t *testing.T) {
 			},
 		})
 
-	// Superflouos columns
+	// Superfluous columns
 	f("1:metric:foo", `123,456,foo,bar`, []Row{
 		{
 			Metric: "foo",
diff --git a/lib/protoparser/prometheus/parser_test.go b/lib/protoparser/prometheus/parser_test.go
index dafa3f447..fb5a78365 100644
--- a/lib/protoparser/prometheus/parser_test.go
+++ b/lib/protoparser/prometheus/parser_test.go
@@ -1,6 +1,7 @@
 package prometheus
 
 import (
+	"math"
 	"reflect"
 	"testing"
 )
@@ -177,14 +178,14 @@ func TestRowsUnmarshalSuccess(t *testing.T) {
 		Rows: []Row{{
 			Metric:    "foobar",
 			Value:     123.456,
-			Timestamp: 789,
+			Timestamp: 789000,
 		}},
 	})
-	f("foobar{} 123.456 789\n", &Rows{
+	f("foobar{} 123.456 789.4354\n", &Rows{
 		Rows: []Row{{
 			Metric:    "foobar",
 			Value:     123.456,
-			Timestamp: 789,
+			Timestamp: 789435,
 		}},
 	})
 	f(`#                                    _                                            _
@@ -202,7 +203,79 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 		}},
 	})
 
-	// Timestamp bigger than 1<<31
+	// Exemplars - see https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md#exemplars-1
+	f(`foo_bucket{le="10",a="#b"} 17 # {trace_id="oHg5SJ#YRHA0"} 9.8 1520879607.789
+	   abc 123 456#foobar
+	   foo   344#bar`, &Rows{
+		Rows: []Row{
+			{
+				Metric: "foo_bucket",
+				Tags: []Tag{
+					{
+						Key:   "le",
+						Value: "10",
+					},
+					{
+						Key:   "a",
+						Value: "#b",
+					},
+				},
+				Value: 17,
+			},
+			{
+				Metric:    "abc",
+				Value:     123,
+				Timestamp: 456000,
+			},
+			{
+				Metric: "foo",
+				Value:  344,
+			},
+		},
+	})
+
+	// "Infinity" word - this has been added in OpenMetrics.
+	// See https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md
+	// Checks for https://github.com/VictoriaMetrics/VictoriaMetrics/issues/924
+	inf := math.Inf(1)
+	f(`
+		foo Infinity
+		bar +Infinity
+		baz -infinity
+		aaa +inf
+		bbb -INF
+		ccc INF
+	`, &Rows{
+		Rows: []Row{
+			{
+				Metric: "foo",
+				Value:  inf,
+			},
+			{
+				Metric: "bar",
+				Value:  inf,
+			},
+			{
+				Metric: "baz",
+				Value:  -inf,
+			},
+			{
+				Metric: "aaa",
+				Value:  inf,
+			},
+			{
+				Metric: "bbb",
+				Value:  -inf,
+			},
+			{
+				Metric: "ccc",
+				Value:  inf,
+			},
+		},
+	})
+
+	// Timestamp bigger than 1<<31.
+	// It should be parsed in milliseconds.
 	f("aaa 1123 429496729600", &Rows{
 		Rows: []Row{{
 			Metric:    "aaa",
@@ -211,6 +284,15 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 		}},
 	})
 
+	// Floating-point timestamps in OpenMetric format.
+	f("aaa 1123 42949.567", &Rows{
+		Rows: []Row{{
+			Metric:    "aaa",
+			Value:     1123,
+			Timestamp: 42949567,
+		}},
+	})
+
 	// Tags
 	f(`foo{bar="baz"} 1 2`, &Rows{
 		Rows: []Row{{
@@ -220,7 +302,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 				Value: "baz",
 			}},
 			Value:     1,
-			Timestamp: 2,
+			Timestamp: 2000,
 		}},
 	})
 	f(`foo{bar="b\"a\\z"} -1.2`, &Rows{
@@ -252,7 +334,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 				},
 			},
 			Value:     1,
-			Timestamp: 2,
+			Timestamp: 2000,
 		}},
 	})
 
@@ -266,7 +348,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 				Value: "baz",
 			}},
 			Value:     1,
-			Timestamp: 2,
+			Timestamp: 2000,
 		}},
 	})
 
@@ -276,7 +358,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 			{
 				Metric:    "foo",
 				Value:     0.3,
-				Timestamp: 2,
+				Timestamp: 2000,
 			},
 			{
 				Metric: "aaa",
@@ -285,7 +367,7 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 			{
 				Metric:    "bar.baz",
 				Value:     0.34,
-				Timestamp: 43,
+				Timestamp: 43000,
 			},
 		},
 	})
@@ -296,12 +378,12 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
 			{
 				Metric:    "foo",
 				Value:     0.3,
-				Timestamp: 2,
+				Timestamp: 2000,
 			},
 			{
 				Metric:    "bar.baz",
 				Value:     0.34,
-				Timestamp: 43,
+				Timestamp: 43000,
 			},
 		},
 	})
diff --git a/lib/protoparser/prometheus/streamparser_test.go b/lib/protoparser/prometheus/streamparser_test.go
index 6d60cb5ea..d2b88ede6 100644
--- a/lib/protoparser/prometheus/streamparser_test.go
+++ b/lib/protoparser/prometheus/streamparser_test.go
@@ -82,13 +82,13 @@ func TestParseStream(t *testing.T) {
 	f("foo 123 456", []Row{{
 		Metric:    "foo",
 		Value:     123,
-		Timestamp: 456,
+		Timestamp: 456000,
 	}})
 	f(`foo{bar="baz"} 1 2`+"\n"+`aaa{} 3 4`, []Row{
 		{
 			Metric:    "aaa",
 			Value:     3,
-			Timestamp: 4,
+			Timestamp: 4000,
 		},
 		{
 			Metric: "foo",
@@ -97,7 +97,7 @@ func TestParseStream(t *testing.T) {
 				Value: "baz",
 			}},
 			Value:     1,
-			Timestamp: 2,
+			Timestamp: 2000,
 		},
 	})
 	f("foo 23", []Row{{
diff --git a/lib/storage/dedup_test.go b/lib/storage/dedup_test.go
index 9ac7ba0d5..8c7fd2348 100644
--- a/lib/storage/dedup_test.go
+++ b/lib/storage/dedup_test.go
@@ -44,7 +44,7 @@ func TestDeduplicateSamples(t *testing.T) {
 			}
 		}
 		if j != len(timestampsCopy) {
-			t.Fatalf("superflouos timestamps found starting from index %d: %v", j, timestampsCopy[j:])
+			t.Fatalf("superfluous timestamps found starting from index %d: %v", j, timestampsCopy[j:])
 		}
 	}
 	f(time.Millisecond, nil, []int64{})
@@ -94,7 +94,7 @@ func TestDeduplicateSamplesDuringMerge(t *testing.T) {
 			}
 		}
 		if j != len(timestampsCopy) {
-			t.Fatalf("superflouos timestamps found starting from index %d: %v", j, timestampsCopy[j:])
+			t.Fatalf("superfluous timestamps found starting from index %d: %v", j, timestampsCopy[j:])
 		}
 	}
 	f(time.Millisecond, nil, []int64{})
diff --git a/lib/storage/metric_name_test.go b/lib/storage/metric_name_test.go
index 432509091..18d1cda8f 100644
--- a/lib/storage/metric_name_test.go
+++ b/lib/storage/metric_name_test.go
@@ -6,6 +6,32 @@ import (
 	"testing"
 )
 
+func TestMetricNameString(t *testing.T) {
+	f := func(mn *MetricName, resultExpected string) {
+		t.Helper()
+		result := mn.String()
+		if result != resultExpected {
+			t.Fatalf("unexpected result\ngot\n%s\nwant\n%s", result, resultExpected)
+		}
+	}
+	f(&MetricName{
+		MetricGroup: []byte("foobar"),
+	}, "foobar{}")
+	f(&MetricName{
+		MetricGroup: []byte("abc"),
+		Tags: []Tag{
+			{
+				Key:   []byte("foo"),
+				Value: []byte("bar"),
+			},
+			{
+				Key:   []byte("baz"),
+				Value: []byte("123"),
+			},
+		},
+	}, `abc{baz="123",foo="bar"}`)
+}
+
 func TestMetricNameSortTags(t *testing.T) {
 	testMetricNameSortTags(t, []string{}, []string{})
 	testMetricNameSortTags(t, []string{"foo"}, []string{"foo"})