Repository: VictoriaMetrics/VictoriaMetrics
Pull request #4629 (2023-07-13 16:30:09)

vmalert: fix evalTS after modify group interval
Need to reset evalTS after modify group interval. Otherwise there could be latency on rule evaluation time all the time when (modifyTS-LastestEvalTS)%NewGroupInterval >0, eg: will have 29m latency below.
<img width="1129" alt="image" src="https://github.com/VictoriaMetrics/VictoriaMetrics/assets/39937150/035a755b-7209-4134-8da9-6d02715be23c">






codecov[bot]: ## [Codecov](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4629?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) Report
Patch coverage has no change and project coverage change: **`+0.04`** :tada:
> Comparison is base [(`c76084b`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/c76084b529421df7821424c57773c0785db3d1e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 61.97% compared to head [(`d1c7b68`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4629?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 62.01%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4629      +/-   ##
==========================================
+ Coverage   61.97%   62.01%   +0.04%     
==========================================
  Files         373      373              
  Lines       69238    69239       +1     
==========================================
+ Hits        42908    42937      +29     
+ Misses      24041    24019      -22     
+ Partials     2289     2283       -6     
```


| [Impacted Files](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4629?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) | Coverage Δ | |
|---|---|---|
| [app/vmalert/group.go](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4629?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-YXBwL3ZtYWxlcnQvZ3JvdXAuZ28=) | `81.86% <0.00%> (-0.21%)` | :arrow_down: |

... and [8 files with indirect coverage changes](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4629/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4629?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).


Patch:
diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml
new file mode 100644
index 000000000..f4c42686b
--- /dev/null
+++ b/.github/workflows/sync-docs.yml
@@ -0,0 +1,53 @@
+name: publish-docs
+on:
+  push:
+    branches:
+      - 'master'
+    paths:
+      - 'docs/**'
+  workflow_dispatch: {}
+permissions:
+  contents: read  # This is required for actions/checkout and to commit back image update
+  deployments: write
+jobs:
+  build:
+    name: Build
+    runs-on: ubuntu-latest
+    steps:
+      - name: Code checkout
+        uses: actions/checkout@v3
+        with:
+          path: main
+      - name: Checkout private code
+        uses: actions/checkout@v3
+        with:
+          repository: VictoriaMetrics/vmdocs
+          token: ${{ secrets.VM_BOT_GH_TOKEN }}
+          path: docs
+
+      - name: Import GPG key
+        uses: crazy-max/ghaction-import-gpg@v5
+        with:
+          gpg_private_key: ${{ secrets.VM_BOT_GPG_PRIVATE_KEY }}
+          passphrase: ${{ secrets.VM_BOT_PASSPHRASE }}
+          git_user_signingkey: true
+          git_commit_gpgsign: true
+          workdir: docs
+      - name: Set short git commit SHA
+        id: vars
+        run: |
+          calculatedSha=$(git rev-parse --short ${{ github.sha }})
+          echo "short_sha=$calculatedSha" >> $GITHUB_OUTPUT
+        working-directory: main
+
+      - name: update code and commit
+        run: |
+          rm -rf content
+          cp -r ../main/docs content
+          make clean-after-copy
+          git config --global user.name "${{ steps.import-gpg.outputs.email }}"
+          git config --global user.email "${{ steps.import-gpg.outputs.email }}"
+          git add .
+          git commit -S -m "sync docs with VictoriaMetrics/VictoriaMetrics commit: ${{ steps.vars.outputs.short_sha }}"
+          git push
+        working-directory: docs
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 8a24846df..45ecabafc 100644
--- a/Makefile
+++ b/Makefile
@@ -409,17 +409,7 @@ vet:
 	go vet ./lib/...
 	go vet ./app/...
 
-# Set variables by using target specific variables to avoid running git diff for each make execution
-# See: https://www.gnu.org/software/make/manual/html_node/Target_002dspecific.html
-goimports: GO_CHANGED_FILES+=$(shell git diff --name-only --diff-filter=ACMR HEAD -- 'lib/*.go')
-goimports: GO_CHANGED_FILES+=$(shell git diff --name-only --diff-filter=ACMR HEAD -- 'app/*.go')
-goimports: install-goimports
-	# GO_CHANGED_FILES will contain a single space if there are no changed files
-	if [ "$(GO_CHANGED_FILES)" != " " ]; then \
-		goimports -local github.com/VictoriaMetrics/VictoriaMetrics -w $(GO_CHANGED_FILES); \
-	fi
-
-check-all: fmt goimports vet golangci-lint govulncheck
+check-all: fmt vet golangci-lint govulncheck
 
 test:
 	go test ./lib/... ./app/...
@@ -484,9 +474,6 @@ install-govulncheck:
 install-wwhrd:
 	which wwhrd || go install github.com/frapposelli/wwhrd@latest
 
-install-goimports:
-	which goimports || go install golang.org/x/tools/cmd/goimports@latest
-
 check-licenses: install-wwhrd
 	wwhrd check -f .wwhrd.yml
 
diff --git a/app/vmalert/group.go b/app/vmalert/group.go
index 10581b1fc..3ccc01269 100644
--- a/app/vmalert/group.go
+++ b/app/vmalert/group.go
@@ -384,6 +384,7 @@ func (g *Group) start(ctx context.Context, nts func() []notifier.Notifier, rw *r
 				g.Interval = ng.Interval
 				t.Stop()
 				t = time.NewTicker(g.Interval)
+				evalTS = time.Now()
 			}
 			g.mu.Unlock()
 			logger.Infof("group %q re-started; interval=%v; concurrency=%d", g.Name, g.Interval, g.Concurrency)
diff --git a/app/vmctl/README.md b/app/vmctl/README.md
index a040ccd5d..d51b490a9 100644
--- a/app/vmctl/README.md
+++ b/app/vmctl/README.md
@@ -799,7 +799,12 @@ Or manually specify addresses according to [URL format](https://docs.victoriamet
     --vm-native-src-addr=http://<src-vmselect>:8481/select/0/prometheus
     --vm-native-dst-addr=http://<dst-vminsert>:8480/insert/0/prometheus
     ```
-6. Migration speed can be adjusted via `--vm-concurrency` cmd-line flag, which controls the number of concurrent 
+6. Migrating data from VM cluster which had replication (`-replicationFactor` > 1) enabled won't produce the same amount
+of data copies for the destination database, and will result only in creating duplicates. To remove duplicates,
+destination database need to be configured with `-dedup.minScrapeInterval=1ms`. To restore the replication factor
+the destination `vminsert` component need to be configured with the according `-replicationFactor` value. 
+See more about replication [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#replication-and-data-safety).
+7. Migration speed can be adjusted via `--vm-concurrency` cmd-line flag, which controls the number of concurrent 
 workers busy with processing. Please note, that each worker can load up to a single vCPU core on VictoriaMetrics. 
 So try to set it according to allocated CPU resources of your VictoriaMetrics destination installation.
 7. Migration is a backfilling process, so it is recommended to read
diff --git a/app/vmselect/graphite/tags_api.go b/app/vmselect/graphite/tags_api.go
index 35cb9dbc3..75da7878d 100644
--- a/app/vmselect/graphite/tags_api.go
+++ b/app/vmselect/graphite/tags_api.go
@@ -138,9 +138,7 @@ func registerMetrics(startTime time.Time, w http.ResponseWriter, r *http.Request
 		mr.MetricNameRaw = storage.MarshalMetricNameRaw(mr.MetricNameRaw[:0], labels)
 		mr.Timestamp = ct
 	}
-	if err := vmstorage.RegisterMetricNames(nil, mrs); err != nil {
-		return fmt.Errorf("cannot register paths: %w", err)
-	}
+	vmstorage.RegisterMetricNames(nil, mrs)
 
 	// Return response
 	contentType := "text/plain; charset=utf-8"
diff --git a/app/vmstorage/main.go b/app/vmstorage/main.go
index ce26c2ea1..08151f3b5 100644
--- a/app/vmstorage/main.go
+++ b/app/vmstorage/main.go
@@ -155,11 +155,10 @@ func AddRows(mrs []storage.MetricRow) error {
 var errReadOnly = errors.New("the storage is in read-only mode; check -storage.minFreeDiskSpaceBytes command-line flag value")
 
 // RegisterMetricNames registers all the metrics from mrs in the storage.
-func RegisterMetricNames(qt *querytracer.Tracer, mrs []storage.MetricRow) error {
+func RegisterMetricNames(qt *querytracer.Tracer, mrs []storage.MetricRow) {
 	WG.Add(1)
-	err := Storage.RegisterMetricNames(qt, mrs)
+	Storage.RegisterMetricNames(qt, mrs)
 	WG.Done()
-	return err
 }
 
 // DeleteSeries deletes series matching tfss.
@@ -546,12 +545,6 @@ func registerStorageMetrics(strg *storage.Storage) {
 		return float64(idbm().PartsRefCount)
 	})
 
-	metrics.NewGauge(`vm_new_timeseries_created_total`, func() float64 {
-		return float64(idbm().NewTimeseriesCreated)
-	})
-	metrics.NewGauge(`vm_timeseries_repopulated_total`, func() float64 {
-		return float64(idbm().TimeseriesRepopulated)
-	})
 	metrics.NewGauge(`vm_missing_tsids_for_metric_id_total`, func() float64 {
 		return float64(idbm().MissingTSIDsForMetricID)
 	})
@@ -666,6 +659,12 @@ func registerStorageMetrics(strg *storage.Storage) {
 		return float64(m().TooSmallTimestampRows)
 	})
 
+	metrics.NewGauge(`vm_timeseries_repopulated_total`, func() float64 {
+		return float64(m().TimeseriesRepopulated)
+	})
+	metrics.NewGauge(`vm_new_timeseries_created_total`, func() float64 {
+		return float64(m().NewTimeseriesCreated)
+	})
 	metrics.NewGauge(`vm_slow_row_inserts_total`, func() float64 {
 		return float64(m().SlowRowInserts)
 	})
diff --git a/app/vmui/packages/vmui/package-lock.json b/app/vmui/packages/vmui/package-lock.json
index 8bcbd6cd8..987dc8f77 100644
--- a/app/vmui/packages/vmui/package-lock.json
+++ b/app/vmui/packages/vmui/package-lock.json
@@ -18804,9 +18804,9 @@
       }
     },
     "node_modules/tough-cookie": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
-      "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
+      "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
       "dev": true,
       "peer": true,
       "dependencies": {
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index b758b643a..979a835c6 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -27,7 +27,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
 * SECURITY: upgrade base docker image (alpine) from 3.18.0 to 3.18.2. See [alpine 3.18.2 release notes](https://alpinelinux.org/posts/Alpine-3.15.9-3.16.6-3.17.4-3.18.2-released.html).
 * SECURITY: upgrade Go builder from Go1.20.5 to Go1.20.6. See [the list of issues addressed in Go1.20.6](https://github.com/golang/go/issues?q=milestone%3AGo1.20.6+label%3ACherryPickApproved).
 
-
+* FETURE: reduce memory usage by up to 5x for setups with [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate) and long [retention](https://docs.victoriametrics.com/#retention). See [description for this change](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/7094fa38bc207c7bd7330ea8a834310a310ce5e3) for details.
 * FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add verbose output for docker installations or when TTY isn't available. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4081).
 * FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): interrupt backoff retries when import process is cancelled. The change makes vmctl more responsive in case of errors during the import. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4442).
 * FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): update backoff policy on retries to reduce probability of overloading for `source` or `destination` databases. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4402).
@@ -52,9 +52,10 @@ The following tip changes can be tested by building VictoriaMetrics components f
 * BUGFIX: add validation for invalid [partial RFC3339 timestamp formats](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#timestamp-formats) in query and export APIs.
 * BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl.html): interrupt explore procedure in influx mode if vmctl found no numeric fields.
 * BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl.html): fix panic in case `--remote-read-filter-time-start` flag is not set for remote-read mode. This flag is now required to use remote-read mode. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4553).
-* BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl.html): fix issue with adding additional character if length of the if the length of the string has become less than the previous length. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4555).
+* BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl.html): fix formatting issue, which could add superflouos `s` characters at the end of `samples/s` output during data migration. For example, it could write `samples/ssssss`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4555).
 * BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): use RFC3339 time format in query args instead of unix timestamp for all issued queries to Prometheus-like datasources.
 * BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): correctly calculate evaluation time for rules. Before, there was a low probability for discrepancy between actual time and rules evaluation time if evaluation interval was lower than the execution time for rules within the group.
+* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): reset evaluation timestamp after modifying group interval. Before, there could have latency on rule evaluation time.
 * BUGFIX: vmselect: fix timestamp alignment for Prometheus querying API if time argument is less than 10m from the beginning of Unix epoch.
 
 ## [v1.91.3](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.91.3)
diff --git a/docs/VictoriaLogs/FAQ.md b/docs/VictoriaLogs/FAQ.md
index 788294d7e..b78c1720b 100644
--- a/docs/VictoriaLogs/FAQ.md
+++ b/docs/VictoriaLogs/FAQ.md
@@ -1,3 +1,15 @@
+---
+sort: 1
+weight: 1
+title: VictoriaLogs FAQ
+menu:
+  docs:
+    parent: "victorialogs"
+    weight: 1
+aliases:
+- /VictoriaLogs/FAQ.html
+---
+
 # VictoriaLogs FAQ
 
 ## What is the difference between VictoriaLogs and Elasticsearch (OpenSearch)?
diff --git a/docs/VictoriaLogs/LogsQL.md b/docs/VictoriaLogs/LogsQL.md
index 35b758a1e..eefa21138 100644
--- a/docs/VictoriaLogs/LogsQL.md
+++ b/docs/VictoriaLogs/LogsQL.md
@@ -1,3 +1,15 @@
+---
+sort: 2
+weight: 2
+title: LogsQL
+menu:
+  docs:
+    parent: "victorialogs"
+    weight: 2
+aliases:
+- /VictoriaLogs/LogsQL.html
+---
+
 # LogsQL
 
 LogsQL is a simple yet powerful query language for [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/).
diff --git a/docs/VictoriaLogs/QuickStart.md b/docs/VictoriaLogs/QuickStart.md
index ca7359350..63887d5f2 100644
--- a/docs/VictoriaLogs/QuickStart.md
+++ b/docs/VictoriaLogs/QuickStart.md
@@ -1,3 +1,15 @@
+---
+sort: 3
+weight: 3
+title: VictoriaLogs Quick Start
+menu:
+  docs:
+    parent: "victorialogs"
+    weight: 3
+aliases:
+- /VictoriaLogs/QuickStart.html
+---
+
 # VictoriaLogs Quick Start
 
 It is recommended to read [README](https://docs.victoriametrics.com/VictoriaLogs/)
diff --git a/docs/VictoriaLogs/README.md b/docs/VictoriaLogs/README.md
index 506861bfb..a71cf68fa 100644
--- a/docs/VictoriaLogs/README.md
+++ b/docs/VictoriaLogs/README.md
@@ -6,7 +6,7 @@ from [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/).
 VictoriaLogs provides the following key features:
 
 - VictoriaLogs can accept logs from popular log collectors. See [these docs](https://docs.victoriametrics.com/VictoriaLogs/data-ingestion/).
-- VictoriaLogs is much easier to setup and operate comparing to ElasticSearch and Grafana Loki.
+- VictoriaLogs is much easier to set up and operate compared to Elasticsearch and Grafana Loki.
   See [these docs](https://docs.victoriametrics.com/VictoriaLogs/QuickStart.html).
 - VictoriaLogs provides easy yet powerful query language with full-text search capabilities across
   all the [log fields](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#data-model) -
@@ -15,16 +15,16 @@ VictoriaLogs provides the following key features:
   See [these docs](https://docs.victoriametrics.com/VictoriaLogs/querying/#command-line) for details.
 - VictoriaLogs capacity and performance scales linearly with the available resources (CPU, RAM, disk IO, disk space).
   It runs smoothly on both Raspberry PI and a server with hundreds of CPU cores and terabytes of RAM.
-- VictoriaLogs can handle much bigger data volumes than ElasticSearch and Grafana Loki when running on comparable hardware.
+- VictoriaLogs can handle much bigger data volumes than Elasticsearch and Grafana Loki when running on comparable hardware.
   See [these docs](#benchmarks).
 - VictoriaLogs supports fast full-text search over high-cardinality [log fields](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#data-model)
   such as `trace_id`, `user_id` and `ip`.
 - VictoriaLogs supports multitenancy - see [these docs](#multitenancy).
-- VictoriaLogs supports out of order logs' ingestion aka backfilling.
-- VictoriaLogs provides simple web UI for querying logs - see [these docs](https://docs.victoriametrics.com/VictoriaLogs/querying/#web-ui).
+- VictoriaLogs supports out-of-order logs' ingestion aka backfilling.
+- VictoriaLogs provides a simple web UI for querying logs - see [these docs](https://docs.victoriametrics.com/VictoriaLogs/querying/#web-ui).
 
-VictoriaLogs is at Preview stage now. It is ready for evaluation in production and verifying claims given above.
-It isn't recommended migrating from existing logging solutions to VictoriaLogs Preview in general case yet.
+VictoriaLogs is at the Preview stage now. It is ready for evaluation in production and verifying the claims given above.
+It isn't recommended to migrate from existing logging solutions to VictoriaLogs Preview in general cases yet.
 See the [Roadmap](https://docs.victoriametrics.com/VictoriaLogs/Roadmap.html) for details.
 
 If you have questions about VictoriaLogs, then read [this FAQ](https://docs.victoriametrics.com/VictoriaLogs/FAQ.html).
@@ -39,15 +39,15 @@ It is recommended to set up monitoring of these metrics via VictoriaMetrics
 (see [these docs](https://docs.victoriametrics.com/#how-to-scrape-prometheus-exporters-such-as-node-exporter)),
 vmagent (see [these docs](https://docs.victoriametrics.com/vmagent.html#how-to-collect-metrics-in-prometheus-format)) or via Prometheus.
 
-VictoriaLogs emits own logs to stdout. It is recommended investigating these logs during troubleshooting.
+VictoriaLogs emits its own logs to stdout. It is recommended to investigate these logs during troubleshooting.
 
 ## Upgrading
 
 It is safe upgrading VictoriaLogs to new versions unless [release notes](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) say otherwise.
-It is safe skipping multiple versions during the upgrade unless [release notes](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) say otherwise.
-It is recommended performing regular upgrades to the latest version, since it may contain important bug fixes, performance optimizations or new features.
+It is safe to skip multiple versions during the upgrade unless [release notes](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) say otherwise.
+It is recommended to perform regular upgrades to the latest version, since it may contain important bug fixes, performance optimizations or new features.
 
-It is also safe downgrading to older versions unless [release notes](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) say otherwise.
+It is also safe to downgrade to older versions unless [release notes](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) say otherwise.
 
 The following steps must be performed during the upgrade / downgrade procedure:
 
@@ -75,14 +75,14 @@ It automatically drops partition directories outside the configured retention.
 VictoriaLogs automatically drops logs at [data ingestion](https://docs.victoriametrics.com/VictoriaLogs/data-ingestion/) stage
 if they have timestamps outside the configured retention. A sample of dropped logs is logged with `WARN` message in order to simplify troubleshooting.
 The `vl_rows_dropped_total` [metric](#monitoring) is incremented each time an ingested log entry is dropped because of timestamp outside the retention.
-It is recommended setting up the following alerting rule at [vmalert](https://docs.victoriametrics.com/vmalert.html) in order to be notified
+It is recommended to set up the following alerting rule at [vmalert](https://docs.victoriametrics.com/vmalert.html) in order to be notified
 when logs with wrong timestamps are ingested into VictoriaLogs:
 
 ```metricsql
 rate(vl_rows_dropped_total[5m]) > 0
 ```
 
-By default VictoriaLogs doesn't accept log entries with timestamps bigger than `now+2d`, e.g. 2 days in the future.
+By default, VictoriaLogs doesn't accept log entries with timestamps bigger than `now+2d`, e.g. 2 days in the future.
 If you need accepting logs with bigger timestamps, then specify the desired "future retention" via `-futureRetention` command-line flag.
 This flag accepts values starting from `1d`. See [these docs](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-durations)
 for the supported duration formats.
@@ -106,7 +106,7 @@ VictoriaLogs automatically creates the `-storageDataPath` directory on the first
 
 ## Multitenancy
 
-VictoriaLogs supports multitenancy. A tenant is identified by `(AccountID, ProjectID)` pair, where `AccountID` and `ProjectID` are arbitrary 32-bit unsigned integeres.
+VictoriaLogs supports multitenancy. A tenant is identified by `(AccountID, ProjectID)` pair, where `AccountID` and `ProjectID` are arbitrary 32-bit unsigned integers.
 The `AccountID` and `ProjectID` fields can be set during [data ingestion](https://docs.victoriametrics.com/VictoriaLogs/data-ingestion/)
 and [querying](https://docs.victoriametrics.com/VictoriaLogs/querying/) via `AccountID` and `ProjectID` request headers.
 
@@ -122,7 +122,7 @@ Here is a [benchmark suite](https://github.com/VictoriaMetrics/VictoriaMetrics/t
 and resource usage between VictoriaLogs and Elasticsearch.
 
 It is recommended [setting up VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/QuickStart.html) in production alongside the existing
-log management systems and comparing resource usage + query performance between VictoriaLogs and your system such as ElasticSearch or Grafana Loki.
+log management systems and comparing resource usage + query performance between VictoriaLogs and your system such as Elasticsearch or Grafana Loki.
 
 Please share benchmark results and ideas on how to improve benchmarks / VictoriaLogs
 via [VictoriaMetrics community channels](https://docs.victoriametrics.com/#community-and-contributions).
@@ -135,9 +135,9 @@ Pass `-help` to VictoriaLogs in order to see the list of supported command-line
   -cacheExpireDuration duration
     	Items are removed from in-memory caches after they aren't accessed for this duration. Lower values may reduce memory usage at the cost of higher CPU usage. See also -prevCacheRemovalPercent (default 30m0s)
   -enableTCP6
-    	Whether to enable IPv6 for listening and dialing. By default, only IPv4 TCP and UDP is used
+    	Whether to enable IPv6 for listening and dialling. By default, only IPv4 TCP and UDP are used
   -envflag.enable
-    	Whether to enable reading flags from environment variables additionally to command line. Command line flag values have priority over values from environment vars. Flags are read only from command line if this flag isn't set. See https://docs.victoriametrics.com/#environment-variables for more details
+    	Whether to enable reading flags from environment variables in additionally to the command line. Command line flag values have priority over values from environment vars. Flags are read only from the command line if this flag isn't set. See https://docs.victoriametrics.com/#environment-variables for more details
   -envflag.prefix string
     	Prefix for environment variables if -envflag.enable is set
   -flagsAuthKey string
@@ -146,7 +146,7 @@ Pass `-help` to VictoriaLogs in order to see the list of supported command-line
     	Whether to use pread() instead of mmap() for reading data files. By default, mmap() is used for 64-bit arches and pread() is used for 32-bit arches, since they cannot read data files bigger than 2^32 bytes in memory. mmap() is usually faster for reading small data chunks than pread()
   -futureRetention value
     	Log entries with timestamps bigger than now+futureRetention are rejected during data ingestion; see https://docs.victoriametrics.com/VictoriaLogs/#retention
-    	The following optional suffixes are supported: h (hour), d (day), w (week), y (year). If suffix isn't set, then the duration is counted in months (default 2d)
+    	The following optional suffixes are supported: h (hour), d (day), w (week), y (year). If the suffix isn't set, then the duration is counted in months (default 2d)
   -gogc int
     	GOGC to use. See https://tip.golang.org/doc/gc-guide (default 100)
   -http.connTimeout duration
@@ -170,7 +170,7 @@ Pass `-help` to VictoriaLogs in order to see the list of supported command-line
   -httpListenAddr.useProxyProtocol
     	Whether to use proxy protocol for connections accepted at -httpListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt . With enabled proxy protocol http server cannot serve regular /metrics endpoint. Use -pushmetrics.url for metrics pushing
   -inmemoryDataFlushInterval duration
-    	The interval for guaranteed saving of in-memory data to disk. The saved data survives unclean shutdown such as OOM crash, hardware reset, SIGKILL, etc. Bigger intervals may help increasing lifetime of flash storage with limited write cycles (e.g. Raspberry PI). Smaller intervals increase disk IO load. Minimum supported value is 1s (default 5s)
+    	The interval for guaranteed saving of in-memory data to disk. The saved data survives unclean shutdowns such as OOM crashes, hardware reset, SIGKILL, etc. Bigger intervals may help increase the lifetime of flash storage with limited write cycles (e.g. Raspberry PI). Smaller intervals increase disk IO load. Minimum supported value is 1s (default 5s)
   -insert.maxLineSizeBytes size
     	The maximum size of a single line, which can be read by /insert/* handlers
     	Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 262144)
@@ -181,7 +181,7 @@ Pass `-help` to VictoriaLogs in order to see the list of supported command-line
   -internStringDisableCache
     	Whether to disable caches for interned strings. This may reduce memory usage at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning . See also -internStringCacheExpireDuration and -internStringMaxLen
   -internStringMaxLen int
-    	The maximum length for strings to intern. Lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning . See also -internStringDisableCache and -internStringCacheExpireDuration (default 500)
+    	The maximum length for strings to intern. A lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning . See also -internStringDisableCache and -internStringCacheExpireDuration (default 500)
   -logIngestedRows
     	Whether to log all the ingested log entries; this can be useful for debugging of data ingestion; see https://docs.victoriametrics.com/VictoriaLogs/data-ingestion/ ; see also -logNewStreams
   -logNewStreams
@@ -203,12 +203,12 @@ Pass `-help` to VictoriaLogs in order to see the list of supported command-line
   -loggerWarnsPerSecondLimit int
     	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 values disable the rate limit
   -maxConcurrentInserts int
-    	The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 12)
+    	The maximum number of concurrent insert requests. The default value should work for most cases, since it minimizes memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 12)
   -memory.allowedBytes size
     	Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to a non-zero value. Too low a value may increase the cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache resulting in higher disk IO usage
     	Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
   -memory.allowedPercent float
-    	Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low a value may increase cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache which will result in higher disk IO usage (default 60)
+    	Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low a value may increase cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from the OS page cache which will result in higher disk IO usage (default 60)
   -metricsAuthKey string
     	Auth key for /metrics endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
   -pprofAuthKey string
@@ -225,7 +225,7 @@ Pass `-help` to VictoriaLogs in order to see the list of supported command-line
     	Supports an array of values separated by comma or specified via multiple flags.
   -retentionPeriod value
     	Log entries with timestamps older than now-retentionPeriod are automatically deleted; log entries with timestamps outside the retention are also rejected during data ingestion; the minimum supported retention is 1d (one day); see https://docs.victoriametrics.com/VictoriaLogs/#retention
-    	The following optional suffixes are supported: h (hour), d (day), w (week), y (year). If suffix isn't set, then the duration is counted in months (default 7d)
+    	The following optional suffixes are supported: h (hour), d (day), w (week), y (year). If the suffix isn't set, then the duration is counted in months (default 7d)
   -search.maxConcurrentRequests int
     	The maximum number of concurrent search requests. It shouldn't be high, since a single request can saturate all the CPU cores, while many concurrently executed requests may require high amounts of memory. See also -search.maxQueueDuration (default 6)
   -search.maxQueryDuration duration
@@ -233,7 +233,7 @@ Pass `-help` to VictoriaLogs in order to see the list of supported command-line
   -search.maxQueueDuration duration
     	The maximum time the search request waits for execution when -search.maxConcurrentRequests limit is reached; see also -search.maxQueryDuration (default 10s)
   -select.maxSortBufferSize size
-    	Query results from /select/logsql/query are automatically sorted by _time if their summary size doesn't exceed this value; otherwise query results are streamed in the response without sorting; too big value for this flag may result in high memory usage, since the sorting is performed in memory
+    	Query results from /select/logsql/query are automatically sorted by _time if their summary size doesn't exceed this value; otherwise, query results are streamed in the response without sorting; too big value for this flag may result in high memory usage since the sorting is performed in memory
     	Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 1048576)
   -storageDataPath string
     	Path to directory with the VictoriaLogs data; see https://docs.victoriametrics.com/VictoriaLogs/#storage (default "victoria-logs-data")
diff --git a/docs/VictoriaLogs/Roadmap.md b/docs/VictoriaLogs/Roadmap.md
index 799502b96..40feafa48 100644
--- a/docs/VictoriaLogs/Roadmap.md
+++ b/docs/VictoriaLogs/Roadmap.md
@@ -1,3 +1,15 @@
+---
+sort: 4
+weight: 4
+title: VictoriaLogs roadmap
+menu:
+  docs:
+    parent: "victorialogs"
+    weight: 4
+aliases:
+- /VictoriaLogs/Roadmap.html
+---
+
 # VictoriaLogs roadmap
 
 The [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/) Preview is ready for evaluation in production.
diff --git a/docs/VictoriaLogs/data-ingestion/README.md b/docs/VictoriaLogs/data-ingestion/README.md
index 5faeef494..458f3d7a1 100644
--- a/docs/VictoriaLogs/data-ingestion/README.md
+++ b/docs/VictoriaLogs/data-ingestion/README.md
@@ -1,3 +1,8 @@
+---
+sort: 6
+title: Data ingestion
+---
+
 # Data ingestion
 
 [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/) can accept logs from the following log collectors:
diff --git a/docs/VictoriaLogs/keyConcepts.md b/docs/VictoriaLogs/keyConcepts.md
index ac2db9bfb..381d37848 100644
--- a/docs/VictoriaLogs/keyConcepts.md
+++ b/docs/VictoriaLogs/keyConcepts.md
@@ -1,3 +1,15 @@
+---
+sort: 5
+weight: 5
+title: VictoriaLogs key concepts
+menu:
+  docs:
+    parent: "victorialogs"
+    weight: 5
+aliases:
+- /VictoriaLogs/Roadmap.html
+---
+
 # VictoriaLogs key concepts
 
 ## Data model
diff --git a/docs/VictoriaLogs/querying/README.md b/docs/VictoriaLogs/querying/README.md
index 1fded3c76..25501c9d7 100644
--- a/docs/VictoriaLogs/querying/README.md
+++ b/docs/VictoriaLogs/querying/README.md
@@ -1,3 +1,8 @@
+---
+sort: 7
+title: Querying
+---
+
 # Querying
 
 [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/) can be queried with [LogsQL](https://docs.victoriametrics.com/VictoriaLogs/LogsQL.html)
diff --git a/docs/vmctl.md b/docs/vmctl.md
index 7ee213912..75893d311 100644
--- a/docs/vmctl.md
+++ b/docs/vmctl.md
@@ -810,7 +810,12 @@ Or manually specify addresses according to [URL format](https://docs.victoriamet
     --vm-native-src-addr=http://<src-vmselect>:8481/select/0/prometheus
     --vm-native-dst-addr=http://<dst-vminsert>:8480/insert/0/prometheus
     ```
-6. Migration speed can be adjusted via `--vm-concurrency` cmd-line flag, which controls the number of concurrent 
+6. Migrating data from VM cluster which had replication (`-replicationFactor` > 1) enabled won't produce the same amount
+of data copies for the destination database, and will result only in creating duplicates. To remove duplicates,
+destination database need to be configured with `-dedup.minScrapeInterval=1ms`. To restore the replication factor
+the destination `vminsert` component need to be configured with the according `-replicationFactor` value. 
+See more about replication [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#replication-and-data-safety).
+7. Migration speed can be adjusted via `--vm-concurrency` cmd-line flag, which controls the number of concurrent 
 workers busy with processing. Please note, that each worker can load up to a single vCPU core on VictoriaMetrics. 
 So try to set it according to allocated CPU resources of your VictoriaMetrics destination installation.
 7. Migration is a backfilling process, so it is recommended to read
diff --git a/lib/mergeset/part_search.go b/lib/mergeset/part_search.go
index 65504ec15..8ee302a3c 100644
--- a/lib/mergeset/part_search.go
+++ b/lib/mergeset/part_search.go
@@ -138,16 +138,7 @@ func (ps *partSearch) Seek(k []byte) {
 	items := ps.ib.items
 	data := ps.ib.data
 	cpLen := commonPrefixLen(ps.ib.commonPrefix, k)
-	if cpLen > 0 {
-		keySuffix := k[cpLen:]
-		ps.ibItemIdx = sort.Search(len(items), func(i int) bool {
-			it := items[i]
-			it.Start += uint32(cpLen)
-			return string(keySuffix) <= it.String(data)
-		})
-	} else {
-		ps.ibItemIdx = binarySearchKey(data, items, k)
-	}
+	ps.ibItemIdx = binarySearchKey(data, items, k, cpLen)
 	if ps.ibItemIdx < len(items) {
 		// The item has been found.
 		return
@@ -165,14 +156,18 @@ func (ps *partSearch) tryFastSeek(k []byte) bool {
 	if ps.ib == nil {
 		return false
 	}
-	data := ps.ib.data
 	items := ps.ib.items
 	idx := ps.ibItemIdx
 	if idx >= len(items) {
 		// The ib is exhausted.
 		return false
 	}
-	if string(k) > items[len(items)-1].String(data) {
+	cpLen := commonPrefixLen(ps.ib.commonPrefix, k)
+	suffix := k[cpLen:]
+	it := items[len(items)-1]
+	it.Start += uint32(cpLen)
+	data := ps.ib.data
+	if string(suffix) > it.String(data) {
 		// The item is located in next blocks.
 		return false
 	}
@@ -181,8 +176,16 @@ func (ps *partSearch) tryFastSeek(k []byte) bool {
 	if idx > 0 {
 		idx--
 	}
-	if string(k) < items[idx].String(data) {
-		if string(k) < items[0].String(data) {
+	it = items[idx]
+	it.Start += uint32(cpLen)
+	if string(suffix) < it.String(data) {
+		items = items[:idx]
+		if len(items) == 0 {
+			return false
+		}
+		it = items[0]
+		it.Start += uint32(cpLen)
+		if string(suffix) < it.String(data) {
 			// The item is located in previous blocks.
 			return false
 		}
@@ -190,7 +193,7 @@ func (ps *partSearch) tryFastSeek(k []byte) bool {
 	}
 
 	// The item is located in the current block
-	ps.ibItemIdx = idx + binarySearchKey(data, items[idx:], k)
+	ps.ibItemIdx = idx + binarySearchKey(data, items[idx:], k, cpLen)
 	return true
 }
 
@@ -330,11 +333,14 @@ func (ps *partSearch) readInmemoryBlock(bh *blockHeader) (*inmemoryBlock, error)
 	return ib, nil
 }
 
-func binarySearchKey(data []byte, items []Item, key []byte) int {
+func binarySearchKey(data []byte, items []Item, k []byte, cpLen int) int {
 	if len(items) == 0 {
 		return 0
 	}
-	if string(key) <= items[0].String(data) {
+	suffix := k[cpLen:]
+	it := items[0]
+	it.Start += uint32(cpLen)
+	if string(suffix) <= it.String(data) {
 		// Fast path - the item is the first.
 		return 0
 	}
@@ -346,7 +352,9 @@ func binarySearchKey(data []byte, items []Item, key []byte) int {
 	i, j := uint(0), n
 	for i < j {
 		h := uint(i+j) >> 1
-		if h >= 0 && h < uint(len(items)) && string(key) > items[h].String(data) {
+		it := items[h]
+		it.Start += uint32(cpLen)
+		if h >= 0 && h < uint(len(items)) && string(suffix) > it.String(data) {
 			i = h + 1
 		} else {
 			j = h
diff --git a/lib/mergeset/table.go b/lib/mergeset/table.go
index 1a72f1e2b..21df81c72 100644
--- a/lib/mergeset/table.go
+++ b/lib/mergeset/table.go
@@ -661,12 +661,11 @@ func (tb *Table) flushInmemoryParts(isFinal bool) {
 
 func (riss *rawItemsShards) flush(tb *Table, dst []*inmemoryBlock, isFinal bool) []*inmemoryBlock {
 	tb.rawItemsPendingFlushesWG.Add(1)
-	defer tb.rawItemsPendingFlushesWG.Done()
-
 	for i := range riss.shards {
 		dst = riss.shards[i].appendBlocksToFlush(dst, tb, isFinal)
 	}
 	tb.flushBlocksToParts(dst, isFinal)
+	tb.rawItemsPendingFlushesWG.Done()
 	return dst
 }
 
diff --git a/lib/storage/index_db.go b/lib/storage/index_db.go
index 036198ee8..515470611 100644
--- a/lib/storage/index_db.go
+++ b/lib/storage/index_db.go
@@ -31,7 +31,17 @@ import (
 
 const (
 	// Prefix for MetricName->TSID entries.
-	nsPrefixMetricNameToTSID = 0
+	//
+	// This index was substituted with nsPrefixDateMetricNameToTSID,
+	// since the MetricName->TSID index may require big amounts of memory for indexdb/dataBlocks cache
+	// when it grows big on the configured retention under high churn rate
+	// (e.g. when new time series are constantly registered).
+	//
+	// It is much more efficient from memory usage PoV to query per-day MetricName->TSID index
+	// (aka nsPrefixDateMetricNameToTSID) when the TSID must be obtained for the given MetricName
+	// during data ingestion under high churn rate and big retention.
+	//
+	// nsPrefixMetricNameToTSID = 0
 
 	// Prefix for Tag->MetricID entries.
 	nsPrefixTagToMetricIDs = 1
@@ -50,6 +60,9 @@ const (
 
 	// Prefix for (Date,Tag)->MetricID entries.
 	nsPrefixDateTagToMetricIDs = 6
+
+	// Prefix for (Date,MetricName)->TSID entries.
+	nsPrefixDateMetricNameToTSID = 7
 )
 
 // indexDB represents an index db.
@@ -59,12 +72,6 @@ type indexDB struct {
 
 	refCount uint64
 
-	// The counter for newly created time series. It can be used for determining time series churn rate.
-	newTimeseriesCreated uint64
-
-	// The counter for time series which were re-populated from previous indexDB after the rotation.
-	timeseriesRepopulated uint64
-
 	// The number of missing MetricID -> TSID entries.
 	// High rate for this value means corrupted indexDB.
 	missingTSIDsForMetricID uint64
@@ -177,8 +184,6 @@ type IndexDBMetrics struct {
 
 	IndexDBRefCount uint64
 
-	NewTimeseriesCreated    uint64
-	TimeseriesRepopulated   uint64
 	MissingTSIDsForMetricID uint64
 
 	RecentHourMetricIDsSearchCalls uint64
@@ -219,8 +224,6 @@ func (db *indexDB) UpdateMetrics(m *IndexDBMetrics) {
 	m.DeletedMetricsCount += uint64(db.s.getDeletedMetricIDs().Len())
 
 	m.IndexDBRefCount += atomic.LoadUint64(&db.refCount)
-	m.NewTimeseriesCreated += atomic.LoadUint64(&db.newTimeseriesCreated)
-	m.TimeseriesRepopulated += atomic.LoadUint64(&db.timeseriesRepopulated)
 	m.MissingTSIDsForMetricID += atomic.LoadUint64(&db.missingTSIDsForMetricID)
 
 	m.DateRangeSearchCalls += atomic.LoadUint64(&db.dateRangeSearchCalls)
@@ -377,33 +380,6 @@ func (db *indexDB) putMetricNameToCache(metricID uint64, metricName []byte) {
 	db.s.metricNameCache.Set(key[:], metricName)
 }
 
-// maybeCreateIndexes probabilistically creates global and per-day indexes for the given (tsid, metricNameRaw, date) at db.
-//
-// The probability increases from 0 to 100% during the first hour since db rotation.
-//
-// It returns true if new index entry was created, and false if it was skipped.
-func (is *indexSearch) maybeCreateIndexes(tsid *TSID, metricNameRaw []byte, date uint64) (bool, error) {
-	pMin := float64(fasttime.UnixTimestamp()-is.db.rotationTimestamp) / 3600
-	if pMin < 1 {
-		p := float64(uint32(fastHashUint64(tsid.MetricID))) / (1 << 32)
-		if p > pMin {
-			// Fast path: there is no need creating indexes for metricNameRaw yet.
-			return false, nil
-		}
-	}
-	// Slow path: create indexes for (tsid, metricNameRaw) at db.
-	mn := GetMetricName()
-	if err := mn.UnmarshalRaw(metricNameRaw); err != nil {
-		return false, fmt.Errorf("cannot unmarshal metricNameRaw %q: %w", metricNameRaw, err)
-	}
-	mn.sortTags()
-	is.createGlobalIndexes(tsid, mn)
-	is.createPerDayIndexes(date, tsid.MetricID, mn)
-	PutMetricName(mn)
-	atomic.AddUint64(&is.db.timeseriesRepopulated, 1)
-	return true, nil
-}
-
 func marshalTagFiltersKey(dst []byte, tfss []*TagFilters, tr TimeRange, versioned bool) []byte {
 	prefix := ^uint64(0)
 	if versioned {
@@ -473,25 +449,28 @@ func unmarshalMetricIDs(dst []uint64, src []byte) ([]uint64, error) {
 	return dst, nil
 }
 
-// getTSIDByNameNoCreate fills the dst with TSID for the given metricName.
+// getTSIDByMetricName fills the dst with TSID for the given metricName at the given date.
 //
-// It returns io.EOF if the given mn isn't found locally.
-func (db *indexDB) getTSIDByNameNoCreate(dst *TSID, metricName []byte) error {
-	is := db.getIndexSearch(noDeadline)
-	err := is.getTSIDByMetricName(dst, metricName)
-	db.putIndexSearch(is)
-	if err == nil {
-		return nil
-	}
-	if err != io.EOF {
-		return fmt.Errorf("cannot search TSID by MetricName %q: %w", metricName, err)
+// It returns false if the given metricName isn't found in the indexdb.
+func (is *indexSearch) getTSIDByMetricName(dst *generationTSID, metricName []byte, date uint64) bool {
+	if is.getTSIDByMetricNameNoExtDB(&dst.TSID, metricName, date) {
+		// Fast path - the TSID is found in the current indexdb.
+		dst.generation = is.db.generation
+		return true
 	}
 
-	// Do not search for the TSID in the external storage,
-	// since this function is already called by another indexDB instance.
-
-	// The TSID for the given mn wasn't found.
-	return io.EOF
+	// Slow path - search for the TSID in the previous indexdb
+	ok := false
+	deadline := is.deadline
+	is.db.doExtDB(func(extDB *indexDB) {
+		is := extDB.getIndexSearch(deadline)
+		ok = is.getTSIDByMetricNameNoExtDB(&dst.TSID, metricName, date)
+		extDB.putIndexSearch(is)
+		if ok {
+			dst.generation = extDB.generation
+		}
+	})
+	return ok
 }
 
 type indexSearch struct {
@@ -502,55 +481,6 @@ type indexSearch struct {
 
 	// deadline in unix timestamp seconds for the given search.
 	deadline uint64
-
-	// tsidByNameMisses and tsidByNameSkips is used for a performance
-	// hack in GetOrCreateTSIDByName. See the comment there.
-	tsidByNameMisses int
-	tsidByNameSkips  int
-}
-
-// GetOrCreateTSIDByName fills the dst with TSID for the given metricName.
-//
-// It also registers the metricName in global and per-day indexes
-// for the given date if the metricName->TSID entry is missing in the index.
-func (is *indexSearch) GetOrCreateTSIDByName(dst *TSID, metricName, metricNameRaw []byte, date uint64) error {
-	// A hack: skip searching for the TSID after many serial misses.
-	// This should improve insertion performance for big batches
-	// of new time series.
-	if is.tsidByNameMisses < 100 {
-		err := is.getTSIDByMetricName(dst, metricName)
-		if err == nil {
-			// Fast path - the TSID for the given metricName has been found in the index.
-			is.tsidByNameMisses = 0
-			if err = is.db.s.registerSeriesCardinality(dst.MetricID, metricNameRaw); err != nil {
-				return err
-			}
-			// There is no need in checking whether the TSID is present in the per-day index for the given date,
-			// since this check must be performed by the caller in an optimized way.
-			// See storage.updatePerDateData() function.
-			return nil
-		}
-		if err != io.EOF {
-			userReadableMetricName := getUserReadableMetricName(metricNameRaw)
-			return fmt.Errorf("cannot search TSID by MetricName %s: %w", userReadableMetricName, err)
-		}
-		is.tsidByNameMisses++
-	} else {
-		is.tsidByNameSkips++
-		if is.tsidByNameSkips > 10000 {
-			is.tsidByNameSkips = 0
-			is.tsidByNameMisses = 0
-		}
-	}
-
-	// TSID for the given name wasn't found. Create it.
-	// It is OK if duplicate TSID for mn is created by concurrent goroutines.
-	// Metric results will be merged by mn after TableSearch.
-	if err := is.createTSIDByName(dst, metricName, metricNameRaw, date); err != nil {
-		userReadableMetricName := getUserReadableMetricName(metricNameRaw)
-		return fmt.Errorf("cannot create TSID by MetricName %s: %w", userReadableMetricName, err)
-	}
-	return nil
 }
 
 func (db *indexDB) getIndexSearch(deadline uint64) *indexSearch {
@@ -572,75 +502,9 @@ func (db *indexDB) putIndexSearch(is *indexSearch) {
 	is.mp.Reset()
 	is.deadline = 0
 
-	// Do not reset tsidByNameMisses and tsidByNameSkips,
-	// since they are used in GetOrCreateTSIDByName across call boundaries.
-
 	db.indexSearchPool.Put(is)
 }
 
-func (is *indexSearch) createTSIDByName(dst *TSID, metricName, metricNameRaw []byte, date uint64) error {
-	mn := GetMetricName()
-	defer PutMetricName(mn)
-	if err := mn.Unmarshal(metricName); err != nil {
-		return fmt.Errorf("cannot unmarshal metricName %q: %w", metricName, err)
-	}
-
-	created, err := is.db.getOrCreateTSID(dst, metricName, mn)
-	if err != nil {
-		return fmt.Errorf("cannot generate TSID: %w", err)
-	}
-	if err := is.db.s.registerSeriesCardinality(dst.MetricID, metricNameRaw); err != nil {
-		return err
-	}
-	is.createGlobalIndexes(dst, mn)
-	is.createPerDayIndexes(date, dst.MetricID, mn)
-
-	// There is no need in invalidating tag cache, since it is invalidated
-	// on db.tb flush via invalidateTagFiltersCache flushCallback passed to mergeset.MustOpenTable.
-
-	if created {
-		// Increase the newTimeseriesCreated counter only if tsid wasn't found in indexDB
-		atomic.AddUint64(&is.db.newTimeseriesCreated, 1)
-		if logNewSeries {
-			logger.Infof("new series created: %s", mn.String())
-		}
-	}
-	return nil
-}
-
-// SetLogNewSeries updates new series logging.
-//
-// This function must be called before any calling any storage functions.
-func SetLogNewSeries(ok bool) {
-	logNewSeries = ok
-}
-
-var logNewSeries = false
-
-// getOrCreateTSID looks for existing TSID for the given metricName in db.extDB or creates a new TSID if nothing was found.
-//
-// Returns true if TSID was created or false if TSID was in extDB
-func (db *indexDB) getOrCreateTSID(dst *TSID, metricName []byte, mn *MetricName) (bool, error) {
-	// Search the TSID in the external storage.
-	// This is usually the db from the previous period.
-	var err error
-	if db.doExtDB(func(extDB *indexDB) {
-		err = extDB.getTSIDByNameNoCreate(dst, metricName)
-	}) {
-		if err == nil {
-			// The TSID has been found in the external storage.
-			return false, nil
-		}
-		if err != io.EOF {
-			return false, fmt.Errorf("external search failed: %w", err)
-		}
-	}
-	// The TSID wasn't found in the external storage.
-	// Generate it locally.
-	generateTSID(dst, mn)
-	return true, nil
-}
-
 func generateTSID(dst *TSID, mn *MetricName) {
 	dst.MetricGroupID = xxhash.Sum64(mn.MetricGroup)
 	// Assume that the job-like metric is put at mn.Tags[0], while instance-like metric is put at mn.Tags[1]
@@ -664,13 +528,6 @@ func (is *indexSearch) createGlobalIndexes(tsid *TSID, mn *MetricName) {
 	ii := getIndexItems()
 	defer putIndexItems(ii)
 
-	// Create MetricName -> TSID index.
-	ii.B = append(ii.B, nsPrefixMetricNameToTSID)
-	ii.B = mn.Marshal(ii.B)
-	ii.B = append(ii.B, kvSeparatorChar)
-	ii.B = tsid.Marshal(ii.B)
-	ii.Next()
-
 	// Create MetricID -> MetricName index.
 	ii.B = marshalCommonPrefix(ii.B, nsPrefixMetricIDToMetricName)
 	ii.B = encoding.MarshalUint64(ii.B, tsid.MetricID)
@@ -1935,26 +1792,27 @@ func (db *indexDB) getTSIDsFromMetricIDs(qt *querytracer.Tracer, metricIDs []uin
 
 var tagFiltersKeyBufPool bytesutil.ByteBufferPool
 
-func (is *indexSearch) getTSIDByMetricName(dst *TSID, metricName []byte) error {
+func (is *indexSearch) getTSIDByMetricNameNoExtDB(dst *TSID, metricName []byte, date uint64) bool {
 	dmis := is.db.s.getDeletedMetricIDs()
 	ts := &is.ts
 	kb := &is.kb
-	kb.B = append(kb.B[:0], nsPrefixMetricNameToTSID)
+	kb.B = marshalCommonPrefix(kb.B[:0], nsPrefixDateMetricNameToTSID)
+	kb.B = encoding.MarshalUint64(kb.B, date)
 	kb.B = append(kb.B, metricName...)
 	kb.B = append(kb.B, kvSeparatorChar)
 	ts.Seek(kb.B)
 	for ts.NextItem() {
 		if !bytes.HasPrefix(ts.Item, kb.B) {
 			// Nothing found.
-			return io.EOF
+			return false
 		}
 		v := ts.Item[len(kb.B):]
 		tail, err := dst.Unmarshal(v)
 		if err != nil {
-			return fmt.Errorf("cannot unmarshal TSID: %w", err)
+			logger.Panicf("FATAL: cannot unmarshal TSID: %s", err)
 		}
 		if len(tail) > 0 {
-			return fmt.Errorf("unexpected non-empty tail left after unmarshaling TSID: %X", tail)
+			logger.Panicf("FATAL: unexpected non-empty tail left after unmarshaling TSID: %X", tail)
 		}
 		if dmis.Len() > 0 {
 			// Verify whether the dst is marked as deleted.
@@ -1964,13 +1822,13 @@ func (is *indexSearch) getTSIDByMetricName(dst *TSID, metricName []byte) error {
 			}
 		}
 		// Found valid dst.
-		return nil
+		return true
 	}
 	if err := ts.Error(); err != nil {
-		return fmt.Errorf("error when searching TSID by metricName; searchPrefix %q: %w", kb.B, err)
+		logger.Panicf("FATAL: error when searching TSID by metricName; searchPrefix %q: %s", kb.B, err)
 	}
 	// Nothing found
-	return io.EOF
+	return false
 }
 
 func (is *indexSearch) searchMetricNameWithCache(dst []byte, metricID uint64) ([]byte, error) {
@@ -2821,13 +2679,21 @@ const (
 	int64Max = int64((1 << 63) - 1)
 )
 
-func (is *indexSearch) createPerDayIndexes(date, metricID uint64, mn *MetricName) {
+func (is *indexSearch) createPerDayIndexes(date uint64, tsid *TSID, mn *MetricName) {
 	ii := getIndexItems()
 	defer putIndexItems(ii)
 
 	ii.B = marshalCommonPrefix(ii.B, nsPrefixDateToMetricID)
 	ii.B = encoding.MarshalUint64(ii.B, date)
-	ii.B = encoding.MarshalUint64(ii.B, metricID)
+	ii.B = encoding.MarshalUint64(ii.B, tsid.MetricID)
+	ii.Next()
+
+	// Create per-day inverted index entries for TSID.
+	ii.B = marshalCommonPrefix(ii.B, nsPrefixDateMetricNameToTSID)
+	ii.B = encoding.MarshalUint64(ii.B, date)
+	ii.B = mn.Marshal(ii.B)
+	ii.B = append(ii.B, kvSeparatorChar)
+	ii.B = tsid.Marshal(ii.B)
 	ii.Next()
 
 	// Create per-day inverted index entries for metricID.
@@ -2835,9 +2701,8 @@ func (is *indexSearch) createPerDayIndexes(date, metricID uint64, mn *MetricName
 	defer kbPool.Put(kb)
 	kb.B = marshalCommonPrefix(kb.B[:0], nsPrefixDateTagToMetricIDs)
 	kb.B = encoding.MarshalUint64(kb.B, date)
-	ii.registerTagIndexes(kb.B, mn, metricID)
+	ii.registerTagIndexes(kb.B, mn, tsid.MetricID)
 	is.db.tb.AddItems(ii.Items)
-	is.db.s.dateMetricIDCache.Set(date, metricID)
 }
 
 func (ii *indexItems) registerTagIndexes(prefix []byte, mn *MetricName, metricID uint64) {
@@ -2947,22 +2812,24 @@ func reverseBytes(dst, src []byte) []byte {
 	return dst
 }
 
-func (is *indexSearch) hasDateMetricID(date, metricID uint64) (bool, error) {
+func (is *indexSearch) hasDateMetricIDNoExtDB(date, metricID uint64) bool {
 	ts := &is.ts
 	kb := &is.kb
 	kb.B = marshalCommonPrefix(kb.B[:0], nsPrefixDateToMetricID)
 	kb.B = encoding.MarshalUint64(kb.B, date)
 	kb.B = encoding.MarshalUint64(kb.B, metricID)
-	if err := ts.FirstItemWithPrefix(kb.B); err != nil {
-		if err == io.EOF {
-			return false, nil
+	err := ts.FirstItemWithPrefix(kb.B)
+	if err == nil {
+		if string(ts.Item) != string(kb.B) {
+			logger.Panicf("FATAL: unexpected entry for (date=%s, metricID=%d); got %q; want %q", dateToString(date), metricID, ts.Item, kb.B)
 		}
-		return false, fmt.Errorf("error when searching for (date=%s, metricID=%d) entry: %w", dateToString(date), metricID, err)
+		// Fast path - the (date, metricID) entry is found in the current indexdb.
+		return true
 	}
-	if string(ts.Item) != string(kb.B) {
-		return false, fmt.Errorf("unexpected entry for (date=%s, metricID=%d); got %q; want %q", dateToString(date), metricID, ts.Item, kb.B)
+	if err != io.EOF {
+		logger.Panicf("FATAL: unexpected error when searching for (date=%s, metricID=%d) entry: %s", dateToString(date), metricID, err)
 	}
-	return true, nil
+	return false
 }
 
 func (is *indexSearch) getMetricIDsForDateTagFilter(qt *querytracer.Tracer, tf *tagFilter, date uint64, commonPrefix []byte,
diff --git a/lib/storage/storage.go b/lib/storage/storage.go
index c9b945225..5188b2def 100644
--- a/lib/storage/storage.go
+++ b/lib/storage/storage.go
@@ -2,7 +2,6 @@ package storage
 
 import (
 	"bytes"
-	"errors"
 	"fmt"
 	"io"
 	"math"
@@ -45,6 +44,8 @@ type Storage struct {
 	tooSmallTimestampRows uint64
 	tooBigTimestampRows   uint64
 
+	timeseriesRepopulated  uint64
+	newTimeseriesCreated   uint64
 	slowRowInserts         uint64
 	slowPerDayIndexInserts uint64
 	slowMetricNameLoads    uint64
@@ -293,7 +294,11 @@ func (s *Storage) updateDeletedMetricIDs(metricIDs *uint64set.Set) {
 // since it may slow down data ingestion when used frequently.
 func (s *Storage) DebugFlush() {
 	s.tb.flushPendingRows()
-	s.idb().tb.DebugFlush()
+	idb := s.idb()
+	idb.tb.DebugFlush()
+	idb.doExtDB(func(extDB *indexDB) {
+		extDB.tb.DebugFlush()
+	})
 }
 
 // CreateSnapshot creates snapshot for s and returns the snapshot name.
@@ -440,6 +445,8 @@ type Metrics struct {
 	TooSmallTimestampRows uint64
 	TooBigTimestampRows   uint64
 
+	TimeseriesRepopulated  uint64
+	NewTimeseriesCreated   uint64
 	SlowRowInserts         uint64
 	SlowPerDayIndexInserts uint64
 	SlowMetricNameLoads    uint64
@@ -509,6 +516,8 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
 	m.TooSmallTimestampRows += atomic.LoadUint64(&s.tooSmallTimestampRows)
 	m.TooBigTimestampRows += atomic.LoadUint64(&s.tooBigTimestampRows)
 
+	m.TimeseriesRepopulated += atomic.LoadUint64(&s.timeseriesRepopulated)
+	m.NewTimeseriesCreated += atomic.LoadUint64(&s.newTimeseriesCreated)
 	m.SlowRowInserts += atomic.LoadUint64(&s.slowRowInserts)
 	m.SlowPerDayIndexInserts += atomic.LoadUint64(&s.slowPerDayIndexInserts)
 	m.SlowMetricNameLoads += atomic.LoadUint64(&s.slowMetricNameLoads)
@@ -723,8 +732,9 @@ func (s *Storage) mustRotateIndexDB() {
 	// Persist changes on the file system.
 	fs.MustSyncPath(s.path)
 
-	// Do not flush tsidCache to avoid read/write path slowdown
-	// and slowly re-populate new idb with entries from the cache via maybeCreateIndexes().
+	// Do not flush tsidCache to avoid read/write path slowdown.
+	// The cache is automatically re-populated with new TSID entries
+	// with the updated indexdb generation.
 	// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1401
 
 	// Flush metric id caches for the current and the previous hour,
@@ -738,7 +748,7 @@ func (s *Storage) mustRotateIndexDB() {
 	//    These series are already registered in prevHourMetricIDs, so VM doesn't add per-day entries to the current indexdb.
 	// 4. Stop adding new samples for these series just before 5 UTC.
 	// 5. The next indexdb rotation is performed at 4 UTC next day.
-	//    The information about the series from step 5 disappears from indexdb, since the old indexdb from step 1 is deleted,
+	//    The information about the series from step 3 disappears from indexdb, since the old indexdb from step 1 is deleted,
 	//    while the current indexdb doesn't contain information about the series.
 	//    So queries for the last 24 hours stop returning samples added at step 3.
 	// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2698
@@ -815,7 +825,6 @@ func (s *Storage) mustLoadNextDayMetricIDs(date uint64) *byDateMetricIDEntry {
 	name := "next_day_metric_ids"
 	path := filepath.Join(s.cachePath, name)
 	if !fs.IsPathExist(path) {
-		logger.Infof("nothing to load from %q", path)
 		return e
 	}
 	src, err := os.ReadFile(path)
@@ -855,7 +864,6 @@ func (s *Storage) mustLoadHourMetricIDs(hour uint64, name string) *hourMetricIDs
 	}
 	path := filepath.Join(s.cachePath, name)
 	if !fs.IsPathExist(path) {
-		logger.Infof("nothing to load from %q", path)
 		return hm
 	}
 	src, err := os.ReadFile(path)
@@ -1176,8 +1184,7 @@ func (s *Storage) DeleteSeries(qt *querytracer.Tracer, tfss []*TagFilters) (int,
 	if err != nil {
 		return deletedCount, fmt.Errorf("cannot delete tsids: %w", err)
 	}
-	// Do not reset MetricName->TSID cache in order to prevent from adding new data points
-	// to deleted time series in Storage.add, since it is already reset inside DeleteTSIDs.
+	// Do not reset MetricName->TSID cache, since it is already reset inside DeleteTSIDs.
 
 	// Do not reset MetricID->MetricName cache, since it must be used only
 	// after filtering out deleted metricIDs.
@@ -1531,61 +1538,129 @@ var metricRowsInsertCtxPool sync.Pool
 
 const maxMetricRowsPerBlock = 8000
 
-// RegisterMetricNames registers all the metric names from mns in the indexdb, so they can be queried later.
+// RegisterMetricNames registers all the metric names from mrs in the indexdb, so they can be queried later.
 //
-// The the MetricRow.Timestamp is used for registering the metric name starting from the given timestamp.
+// The the MetricRow.Timestamp is used for registering the metric name at the given day according to the timestamp.
 // Th MetricRow.Value field is ignored.
-func (s *Storage) RegisterMetricNames(qt *querytracer.Tracer, mrs []MetricRow) error {
+func (s *Storage) RegisterMetricNames(qt *querytracer.Tracer, mrs []MetricRow) {
 	qt = qt.NewChild("registering %d series", len(mrs))
 	defer qt.Done()
-	var metricName []byte
+	var metricNameBuf []byte
 	var genTSID generationTSID
 	mn := GetMetricName()
 	defer PutMetricName(mn)
 
+	var seriesRepopulated uint64
+
 	idb := s.idb()
 	is := idb.getIndexSearch(noDeadline)
 	defer idb.putIndexSearch(is)
+	var firstWarn error
 	for i := range mrs {
 		mr := &mrs[i]
+		date := uint64(mr.Timestamp) / msecPerDay
 		if s.getTSIDFromCache(&genTSID, mr.MetricNameRaw) {
-			if err := s.registerSeriesCardinality(genTSID.TSID.MetricID, mr.MetricNameRaw); err != nil {
+			// Fast path - mr.MetricNameRaw has been already registered in the current idb.
+			if !s.registerSeriesCardinality(genTSID.TSID.MetricID, mr.MetricNameRaw) {
+				// Skip row, since it exceeds cardinality limit
 				continue
 			}
-			if genTSID.generation == idb.generation {
-				// Fast path - mr.MetricNameRaw has been already registered in the current idb.
-				continue
+			if genTSID.generation != idb.generation {
+				// The found TSID is from the previous indexdb. Create it in the current indexdb.
+
+				if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
+					// Do not stop adding rows on error - just skip invalid row.
+					// This guarantees that invalid rows don't prevent
+					// from adding valid rows into the storage.
+					if firstWarn == nil {
+						firstWarn = fmt.Errorf("cannot umarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err)
+					}
+					continue
+				}
+				mn.sortTags()
+
+				createAllIndexesForMetricName(is, mn, &genTSID.TSID, date)
+				genTSID.generation = idb.generation
+				s.putSeriesToCache(mr.MetricNameRaw, &genTSID, date)
+				seriesRepopulated++
 			}
+			continue
 		}
-		// Slow path - register mr.MetricNameRaw.
+
+		// Slow path - search TSID for the given metricName in indexdb.
+
+		// Construct canonical metric name - it is used below.
 		if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
-			return fmt.Errorf("cannot unmarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err)
+			// Do not stop adding rows on error - just skip invalid row.
+			// This guarantees that invalid rows don't prevent
+			// from adding valid rows into the storage.
+			if firstWarn == nil {
+				firstWarn = fmt.Errorf("cannot umarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err)
+			}
+			continue
 		}
 		mn.sortTags()
-		metricName = mn.Marshal(metricName[:0])
-		date := uint64(mr.Timestamp) / msecPerDay
-		if err := is.GetOrCreateTSIDByName(&genTSID.TSID, metricName, mr.MetricNameRaw, date); err != nil {
-			if errors.Is(err, errSeriesCardinalityExceeded) {
+		metricNameBuf = mn.Marshal(metricNameBuf[:0])
+
+		if is.getTSIDByMetricName(&genTSID, metricNameBuf, date) {
+			// Slower path - the TSID has been found in indexdb.
+
+			if !s.registerSeriesCardinality(genTSID.TSID.MetricID, mr.MetricNameRaw) {
+				// Skip the row, since it exceeds the configured cardinality limit.
 				continue
 			}
-			return fmt.Errorf("cannot create TSID for metricName %q: %w", metricName, err)
+
+			if genTSID.generation != idb.generation {
+				// The found TSID is from the previous indexdb. Create it in the current indexdb.
+				createAllIndexesForMetricName(is, mn, &genTSID.TSID, date)
+				genTSID.generation = idb.generation
+				seriesRepopulated++
+			}
+			s.putSeriesToCache(mr.MetricNameRaw, &genTSID, date)
+			continue
+		}
+
+		// Slowest path - there is no TSID in indexdb for the given mr.MetricNameRaw. Create it.
+		generateTSID(&genTSID.TSID, mn)
+
+		if !s.registerSeriesCardinality(genTSID.TSID.MetricID, mr.MetricNameRaw) {
+			// Skip the row, since it exceeds the configured cardinality limit.
+			continue
 		}
+
+		// Schedule creating TSID indexes instead of creating them synchronously.
+		// This should keep stable the ingestion rate when new time series are ingested.
+		createAllIndexesForMetricName(is, mn, &genTSID.TSID, date)
 		genTSID.generation = idb.generation
-		s.putTSIDToCache(&genTSID, mr.MetricNameRaw)
+		s.putSeriesToCache(mr.MetricNameRaw, &genTSID, date)
+	}
+
+	atomic.AddUint64(&s.timeseriesRepopulated, seriesRepopulated)
+
+	if firstWarn != nil {
+		logger.Warnf("cannot create some metrics: %s", firstWarn)
 	}
-	return nil
 }
 
 func (s *Storage) add(rows []rawRow, dstMrs []*MetricRow, mrs []MetricRow, precisionBits uint8) error {
 	idb := s.idb()
 	is := idb.getIndexSearch(noDeadline)
 	defer idb.putIndexSearch(is)
+
+	mn := GetMetricName()
+	defer PutMetricName(mn)
+
 	var (
 		// These vars are used for speeding up bulk imports of multiple adjacent rows for the same metricName.
 		prevTSID          TSID
 		prevMetricNameRaw []byte
 	)
-	var pmrs *pendingMetricRows
+	var metricNameBuf []byte
+
+	var slowInsertsCount uint64
+	var newSeriesCount uint64
+	var seriesRepopulated uint64
+
 	minTimestamp, maxTimestamp := s.tb.getMinMaxTimestamps()
 
 	var genTSID generationTSID
@@ -1629,6 +1704,8 @@ func (s *Storage) add(rows []rawRow, dstMrs []*MetricRow, mrs []MetricRow, preci
 		r.Timestamp = mr.Timestamp
 		r.Value = mr.Value
 		r.PrecisionBits = precisionBits
+
+		// Search for TSID for the given mr.MetricNameRaw and store it at r.TSID.
 		if string(mr.MetricNameRaw) == string(prevMetricNameRaw) {
 			// Fast path - the current mr contains the same metric name as the previous mr, so it contains the same TSID.
 			// This path should trigger on bulk imports when many rows contain the same MetricNameRaw.
@@ -1636,99 +1713,109 @@ func (s *Storage) add(rows []rawRow, dstMrs []*MetricRow, mrs []MetricRow, preci
 			continue
 		}
 		if s.getTSIDFromCache(&genTSID, mr.MetricNameRaw) {
-			if err := s.registerSeriesCardinality(r.TSID.MetricID, mr.MetricNameRaw); err != nil {
+			// Fast path - the TSID for the given mr.MetricNameRaw has been found in cache and isn't deleted.
+			// There is no need in checking whether r.TSID.MetricID is deleted, since tsidCache doesn't
+			// contain MetricName->TSID entries for deleted time series.
+			// See Storage.DeleteSeries code for details.
+
+			if !s.registerSeriesCardinality(r.TSID.MetricID, mr.MetricNameRaw) {
+				// Skip row, since it exceeds cardinality limit
 				j--
 				continue
 			}
 			r.TSID = genTSID.TSID
-			// Fast path - the TSID for the given MetricNameRaw has been found in cache and isn't deleted.
-			// There is no need in checking whether r.TSID.MetricID is deleted, since tsidCache doesn't
-			// contain MetricName->TSID entries for deleted time series.
-			// See Storage.DeleteSeries code for details.
 			prevTSID = r.TSID
 			prevMetricNameRaw = mr.MetricNameRaw
 
 			if genTSID.generation != idb.generation {
-				// The found entry is from the previous cache generation,
-				// so attempt to re-populate the current generation with this entry.
-				// This is needed for https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1401
+				// The found TSID is from the previous indexdb. Create it in the current indexdb.
 				date := uint64(r.Timestamp) / msecPerDay
-				created, err := is.maybeCreateIndexes(&genTSID.TSID, mr.MetricNameRaw, date)
-				if err != nil {
-					return fmt.Errorf("cannot create indexes: %w", err)
-				}
-				if created {
-					genTSID.generation = idb.generation
-					s.putTSIDToCache(&genTSID, mr.MetricNameRaw)
+
+				if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
+					if firstWarn == nil {
+						firstWarn = fmt.Errorf("cannot unmarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err)
+					}
+					j--
+					continue
 				}
+				mn.sortTags()
+
+				createAllIndexesForMetricName(is, mn, &genTSID.TSID, date)
+				genTSID.generation = idb.generation
+				s.putSeriesToCache(mr.MetricNameRaw, &genTSID, date)
+				seriesRepopulated++
+				slowInsertsCount++
 			}
 			continue
 		}
 
-		// Slow path - the TSID is missing in the cache.
-		// Postpone its search in the loop below.
-		j--
-		if pmrs == nil {
-			pmrs = getPendingMetricRows()
-		}
-		if err := pmrs.addRow(mr); err != nil {
-			// Do not stop adding rows on error - just skip invalid row.
-			// This guarantees that invalid rows don't prevent
-			// from adding valid rows into the storage.
+		// Slow path - the TSID for the given mr.MetricNameRaw is missing in the cache.
+		slowInsertsCount++
+
+		date := uint64(r.Timestamp) / msecPerDay
+
+		// Construct canonical metric name - it is used below.
+		if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
 			if firstWarn == nil {
-				firstWarn = err
+				firstWarn = fmt.Errorf("cannot unmarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err)
 			}
+			j--
 			continue
 		}
-	}
-	if pmrs != nil {
-		// Sort pendingMetricRows by canonical metric name in order to speed up search via `is` in the loop below.
-		pendingMetricRows := pmrs.pmrs
-		sort.Slice(pendingMetricRows, func(i, j int) bool {
-			return string(pendingMetricRows[i].MetricName) < string(pendingMetricRows[j].MetricName)
-		})
-		prevMetricNameRaw = nil
-		var slowInsertsCount uint64
-		for i := range pendingMetricRows {
-			pmr := &pendingMetricRows[i]
-			mr := pmr.mr
-			dstMrs[j] = mr
-			r := &rows[j]
-			j++
-			r.Timestamp = mr.Timestamp
-			r.Value = mr.Value
-			r.PrecisionBits = precisionBits
-			if string(mr.MetricNameRaw) == string(prevMetricNameRaw) {
-				// Fast path - the current mr contains the same metric name as the previous mr, so it contains the same TSID.
-				// This path should trigger on bulk imports when many rows contain the same MetricNameRaw.
-				r.TSID = prevTSID
-				continue
-			}
-			slowInsertsCount++
-			date := uint64(r.Timestamp) / msecPerDay
-			if err := is.GetOrCreateTSIDByName(&r.TSID, pmr.MetricName, mr.MetricNameRaw, date); err != nil {
+		mn.sortTags()
+		metricNameBuf = mn.Marshal(metricNameBuf[:0])
+
+		// Search for TSID for the given mr.MetricNameRaw in the indexdb.
+		if is.getTSIDByMetricName(&genTSID, metricNameBuf, date) {
+			// Slower path - the TSID has been found in indexdb.
+
+			if !s.registerSeriesCardinality(genTSID.TSID.MetricID, mr.MetricNameRaw) {
+				// Skip the row, since it exceeds the configured cardinality limit.
 				j--
-				if errors.Is(err, errSeriesCardinalityExceeded) {
-					continue
-				}
-				// Do not stop adding rows on error - just skip invalid row.
-				// This guarantees that invalid rows don't prevent
-				// from adding valid rows into the storage.
-				if firstWarn == nil {
-					firstWarn = fmt.Errorf("cannot obtain or create TSID for MetricName %q: %w", pmr.MetricName, err)
-				}
 				continue
 			}
-			genTSID.generation = idb.generation
-			genTSID.TSID = r.TSID
-			s.putTSIDToCache(&genTSID, mr.MetricNameRaw)
 
-			prevTSID = r.TSID
+			if genTSID.generation != idb.generation {
+				// The found TSID is from the previous indexdb. Create it in the current indexdb.
+				createAllIndexesForMetricName(is, mn, &genTSID.TSID, date)
+				genTSID.generation = idb.generation
+				seriesRepopulated++
+			}
+			s.putSeriesToCache(mr.MetricNameRaw, &genTSID, date)
+
+			r.TSID = genTSID.TSID
+			prevTSID = genTSID.TSID
 			prevMetricNameRaw = mr.MetricNameRaw
+			continue
+		}
+
+		// Slowest path - the TSID for the given mr.MetricNameRaw isn't found in indexdb. Create it.
+		generateTSID(&genTSID.TSID, mn)
+
+		if !s.registerSeriesCardinality(genTSID.TSID.MetricID, mr.MetricNameRaw) {
+			// Skip the row, since it exceeds the configured cardinality limit.
+			j--
+			continue
+		}
+
+		createAllIndexesForMetricName(is, mn, &genTSID.TSID, date)
+		genTSID.generation = idb.generation
+		s.putSeriesToCache(mr.MetricNameRaw, &genTSID, date)
+		newSeriesCount++
+
+		r.TSID = genTSID.TSID
+		prevTSID = r.TSID
+		prevMetricNameRaw = mr.MetricNameRaw
+
+		if logNewSeries {
+			logger.Infof("new series created: %s", mn.String())
 		}
-		putPendingMetricRows(pmrs)
-		atomic.AddUint64(&s.slowRowInserts, slowInsertsCount)
 	}
+
+	atomic.AddUint64(&s.slowRowInserts, slowInsertsCount)
+	atomic.AddUint64(&s.newTimeseriesCreated, newSeriesCount)
+	atomic.AddUint64(&s.timeseriesRepopulated, seriesRepopulated)
+
 	if firstWarn != nil {
 		storageAddRowsLogger.Warnf("warn occurred during rows addition: %s", firstWarn)
 	}
@@ -1749,22 +1836,44 @@ func (s *Storage) add(rows []rawRow, dstMrs []*MetricRow, mrs []MetricRow, preci
 
 var storageAddRowsLogger = logger.WithThrottler("storageAddRows", 5*time.Second)
 
-func (s *Storage) registerSeriesCardinality(metricID uint64, metricNameRaw []byte) error {
+// SetLogNewSeries updates new series logging.
+//
+// This function must be called before any calling any storage functions.
+func SetLogNewSeries(ok bool) {
+	logNewSeries = ok
+}
+
+var logNewSeries = false
+
+func createAllIndexesForMetricName(is *indexSearch, mn *MetricName, tsid *TSID, date uint64) {
+	is.createGlobalIndexes(tsid, mn)
+	is.createPerDayIndexes(date, tsid, mn)
+}
+
+func (s *Storage) putSeriesToCache(metricNameRaw []byte, genTSID *generationTSID, date uint64) {
+	// Store the TSID for for the current indexdb into cache,
+	// so future rows for that TSID are ingested via fast path.
+	s.putTSIDToCache(genTSID, metricNameRaw)
+
+	// Register the (date, metricID) entry in the cache,
+	// so next time the entry is found there instead of searching for it in the indexdb.
+	s.dateMetricIDCache.Set(date, genTSID.TSID.MetricID)
+}
+
+func (s *Storage) registerSeriesCardinality(metricID uint64, metricNameRaw []byte) bool {
 	if sl := s.hourlySeriesLimiter; sl != nil && !sl.Add(metricID) {
 		atomic.AddUint64(&s.hourlySeriesLimitRowsDropped, 1)
 		logSkippedSeries(metricNameRaw, "-storage.maxHourlySeries", sl.MaxItems())
-		return errSeriesCardinalityExceeded
+		return false
 	}
 	if sl := s.dailySeriesLimiter; sl != nil && !sl.Add(metricID) {
 		atomic.AddUint64(&s.dailySeriesLimitRowsDropped, 1)
 		logSkippedSeries(metricNameRaw, "-storage.maxDailySeries", sl.MaxItems())
-		return errSeriesCardinalityExceeded
+		return false
 	}
-	return nil
+	return true
 }
 
-var errSeriesCardinalityExceeded = fmt.Errorf("cannot create series because series cardinality limit exceeded")
-
 func logSkippedSeries(metricNameRaw []byte, flagName string, flagValue int) {
 	select {
 	case <-logSkippedSeriesTicker.C:
@@ -1787,75 +1896,6 @@ func getUserReadableMetricName(metricNameRaw []byte) string {
 	return mn.String()
 }
 
-type pendingMetricRow struct {
-	MetricName []byte
-	mr         *MetricRow
-}
-
-type pendingMetricRows struct {
-	pmrs           []pendingMetricRow
-	metricNamesBuf []byte
-
-	lastMetricNameRaw []byte
-	lastMetricName    []byte
-	mn                MetricName
-}
-
-func (pmrs *pendingMetricRows) reset() {
-	mrs := pmrs.pmrs
-	for i := range mrs {
-		pmr := &mrs[i]
-		pmr.MetricName = nil
-		pmr.mr = nil
-	}
-	pmrs.pmrs = mrs[:0]
-	pmrs.metricNamesBuf = pmrs.metricNamesBuf[:0]
-	pmrs.lastMetricNameRaw = nil
-	pmrs.lastMetricName = nil
-	pmrs.mn.Reset()
-}
-
-func (pmrs *pendingMetricRows) addRow(mr *MetricRow) error {
-	// Do not spend CPU time on re-calculating canonical metricName during bulk import
-	// of many rows for the same metric.
-	if string(mr.MetricNameRaw) != string(pmrs.lastMetricNameRaw) {
-		if err := pmrs.mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
-			return fmt.Errorf("cannot unmarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err)
-		}
-		pmrs.mn.sortTags()
-		metricNamesBufLen := len(pmrs.metricNamesBuf)
-		pmrs.metricNamesBuf = pmrs.mn.Marshal(pmrs.metricNamesBuf)
-		pmrs.lastMetricName = pmrs.metricNamesBuf[metricNamesBufLen:]
-		pmrs.lastMetricNameRaw = mr.MetricNameRaw
-	}
-	mrs := pmrs.pmrs
-	if cap(mrs) > len(mrs) {
-		mrs = mrs[:len(mrs)+1]
-	} else {
-		mrs = append(mrs, pendingMetricRow{})
-	}
-	pmrs.pmrs = mrs
-	pmr := &mrs[len(mrs)-1]
-	pmr.MetricName = pmrs.lastMetricName
-	pmr.mr = mr
-	return nil
-}
-
-func getPendingMetricRows() *pendingMetricRows {
-	v := pendingMetricRowsPool.Get()
-	if v == nil {
-		v = &pendingMetricRows{}
-	}
-	return v.(*pendingMetricRows)
-}
-
-func putPendingMetricRows(pmrs *pendingMetricRows) {
-	pmrs.reset()
-	pendingMetricRowsPool.Put(pmrs)
-}
-
-var pendingMetricRowsPool sync.Pool
-
 func (s *Storage) updatePerDateData(rows []rawRow, mrs []*MetricRow) error {
 	var date uint64
 	var hour uint64
@@ -1875,9 +1915,9 @@ func (s *Storage) updatePerDateData(rows []rawRow, mrs []*MetricRow) error {
 	// pMin linearly increases from 0 to 1 during the last hour of the day.
 	pMin := (float64(ts%(3600*24)) / 3600) - 23
 	type pendingDateMetricID struct {
-		date     uint64
-		metricID uint64
-		mr       *MetricRow
+		date uint64
+		tsid *TSID
+		mr   *MetricRow
 	}
 	var pendingDateMetricIDs []pendingDateMetricID
 	var pendingNextDayMetricIDs []uint64
@@ -1897,7 +1937,7 @@ func (s *Storage) updatePerDateData(rows []rawRow, mrs []*MetricRow) error {
 		prevDate = date
 		prevMetricID = metricID
 		if hour == hm.hour {
-			// The r belongs to the current hour. Check for the current hour cache.
+			// The row belongs to the current hour. Check for the current hour cache.
 			if hm.m.Has(metricID) {
 				// Fast path: the metricID is in the current hour cache.
 				// This means the metricID has been already added to per-day inverted index.
@@ -1910,9 +1950,9 @@ func (s *Storage) updatePerDateData(rows []rawRow, mrs []*MetricRow) error {
 					p := float64(uint32(fastHashUint64(metricID))) / (1 << 32)
 					if p < pMin && !nextDayMetricIDs.Has(metricID) {
 						pendingDateMetricIDs = append(pendingDateMetricIDs, pendingDateMetricID{
-							date:     date + 1,
-							metricID: metricID,
-							mr:       mrs[i],
+							date: date + 1,
+							tsid: &r.TSID,
+							mr:   mrs[i],
 						})
 						pendingNextDayMetricIDs = append(pendingNextDayMetricIDs, metricID)
 					}
@@ -1932,9 +1972,9 @@ func (s *Storage) updatePerDateData(rows []rawRow, mrs []*MetricRow) error {
 		}
 		// Slow path: store the (date, metricID) entry in the indexDB.
 		pendingDateMetricIDs = append(pendingDateMetricIDs, pendingDateMetricID{
-			date:     date,
-			metricID: metricID,
-			mr:       mrs[i],
+			date: date,
+			tsid: &r.TSID,
+			mr:   mrs[i],
 		})
 	}
 	if len(pendingNextDayMetricIDs) > 0 {
@@ -1948,7 +1988,7 @@ func (s *Storage) updatePerDateData(rows []rawRow, mrs []*MetricRow) error {
 		s.pendingHourEntriesLock.Unlock()
 	}
 	if len(pendingDateMetricIDs) == 0 {
-		// Fast path - there are no new (date, metricID) entries in rows.
+		// Fast path - there are no new (date, metricID) entries.
 		return nil
 	}
 
@@ -1962,27 +2002,22 @@ func (s *Storage) updatePerDateData(rows []rawRow, mrs []*MetricRow) error {
 		if a.date != b.date {
 			return a.date < b.date
 		}
-		return a.metricID < b.metricID
+		return a.tsid.MetricID < b.tsid.MetricID
 	})
+
 	idb := s.idb()
 	is := idb.getIndexSearch(noDeadline)
 	defer idb.putIndexSearch(is)
+
 	var firstError error
 	dateMetricIDsForCache := make([]dateMetricID, 0, len(pendingDateMetricIDs))
 	mn := GetMetricName()
 	for _, dmid := range pendingDateMetricIDs {
 		date := dmid.date
-		metricID := dmid.metricID
-		ok, err := is.hasDateMetricID(date, metricID)
-		if err != nil {
-			if firstError == nil {
-				firstError = fmt.Errorf("error when locating (date=%s, metricID=%d) in database: %w", dateToString(date), metricID, err)
-			}
-			continue
-		}
-		if !ok {
+		metricID := dmid.tsid.MetricID
+		if !is.hasDateMetricIDNoExtDB(date, metricID) {
 			// The (date, metricID) entry is missing in the indexDB. Add it there together with per-day indexes.
-			// It is OK if the (date, metricID) entry is added multiple times to db
+			// It is OK if the (date, metricID) entry is added multiple times to indexdb
 			// by concurrent goroutines.
 			if err := mn.UnmarshalRaw(dmid.mr.MetricNameRaw); err != nil {
 				if firstError == nil {
@@ -1991,7 +2026,7 @@ func (s *Storage) updatePerDateData(rows []rawRow, mrs []*MetricRow) error {
 				continue
 			}
 			mn.sortTags()
-			is.createPerDayIndexes(date, metricID, mn)
+			is.createPerDayIndexes(date, dmid.tsid, mn)
 		}
 		dateMetricIDsForCache = append(dateMetricIDsForCache, dateMetricID{
 			date:     date,


Test patch:
diff --git a/app/vmalert/alerting_test.go b/app/vmalert/alerting_test.go
index 61e39c371..d05305c90 100644
--- a/app/vmalert/alerting_test.go
+++ b/app/vmalert/alerting_test.go
@@ -866,8 +866,8 @@ func TestAlertingRule_Template(t *testing.T) {
 			for hash, expAlert := range tc.expAlerts {
 				gotAlert := tc.rule.alerts[hash]
 				if gotAlert == nil {
-					t.Fatalf("alert %d is missing; labels: %v; annotations: %v",
-						hash, expAlert.Labels, expAlert.Annotations)
+					t.Fatalf("alert %d is missing; labels: %v; annotations: %v", hash, expAlert.Labels, expAlert.Annotations)
+					break
 				}
 				if !reflect.DeepEqual(expAlert.Annotations, gotAlert.Annotations) {
 					t.Fatalf("expected to have annotations %#v; got %#v", expAlert.Annotations, gotAlert.Annotations)
diff --git a/lib/logstorage/values_encoder_test.go b/lib/logstorage/values_encoder_test.go
index 718d03658..c1a7c5409 100644
--- a/lib/logstorage/values_encoder_test.go
+++ b/lib/logstorage/values_encoder_test.go
@@ -55,27 +55,27 @@ func TestValuesEncoder(t *testing.T) {
 
 	// uint8 values
 	for i := range values {
-		values[i] = fmt.Sprintf("%d", i+1)
+		values[i] = fmt.Sprintf("%d", uint64(i+1))
 	}
 	f(values, valueTypeUint8, 1, uint64(len(values)))
 
 	// uint16 values
 	for i := range values {
-		values[i] = fmt.Sprintf("%d", (i+1)<<8)
+		values[i] = fmt.Sprintf("%d", uint64(i+1)<<8)
 	}
-	f(values, valueTypeUint16, 1<<8, uint64(len(values)<<8))
+	f(values, valueTypeUint16, 1<<8, uint64(len(values))<<8)
 
 	// uint32 values
 	for i := range values {
-		values[i] = fmt.Sprintf("%d", (i+1)<<16)
+		values[i] = fmt.Sprintf("%d", uint64(i+1)<<16)
 	}
-	f(values, valueTypeUint32, 1<<16, uint64(len(values)<<16))
+	f(values, valueTypeUint32, 1<<16, uint64(len(values))<<16)
 
 	// uint64 values
 	for i := range values {
-		values[i] = fmt.Sprintf("%d", (i+1)<<32)
+		values[i] = fmt.Sprintf("%d", uint64(i+1)<<32)
 	}
-	f(values, valueTypeUint64, 1<<32, uint64(len(values)<<32))
+	f(values, valueTypeUint64, 1<<32, uint64(len(values))<<32)
 
 	// ipv4 values
 	for i := range values {
diff --git a/lib/storage/index_db_test.go b/lib/storage/index_db_test.go
index 6b75eb1f5..58185d7ca 100644
--- a/lib/storage/index_db_test.go
+++ b/lib/storage/index_db_test.go
@@ -491,12 +491,11 @@ func TestRemoveDuplicateMetricIDs(t *testing.T) {
 }
 
 func TestIndexDBOpenClose(t *testing.T) {
-	s := newTestStorage()
-	defer stopTestStorage(s)
+	var s Storage
 	tableName := nextIndexDBTableName()
 	for i := 0; i < 5; i++ {
 		var isReadOnly uint32
-		db := mustOpenIndexDB(tableName, s, 0, &isReadOnly)
+		db := mustOpenIndexDB(tableName, &s, 0, &isReadOnly)
 		db.MustClose()
 	}
 	if err := os.RemoveAll(tableName); err != nil {
@@ -508,19 +507,10 @@ func TestIndexDB(t *testing.T) {
 	const metricGroups = 10
 
 	t.Run("serial", func(t *testing.T) {
-		s := newTestStorage()
-		defer stopTestStorage(s)
-
-		dbName := nextIndexDBTableName()
-		var isReadOnly uint32
-		db := mustOpenIndexDB(dbName, s, 0, &isReadOnly)
-		defer func() {
-			db.MustClose()
-			if err := os.RemoveAll(dbName); err != nil {
-				t.Fatalf("cannot remove indexDB: %s", err)
-			}
-		}()
+		const path = "TestIndexDB-serial"
+		s := MustOpenStorage(path, maxRetentionMsecs, 0, 0)
 
+		db := s.idb()
 		mns, tsids, err := testIndexDBGetOrCreateTSIDByName(db, metricGroups)
 		if err != nil {
 			t.Fatalf("unexpected error: %s", err)
@@ -529,27 +519,23 @@ func TestIndexDB(t *testing.T) {
 			t.Fatalf("unexpected error: %s", err)
 		}
 
-		// Re-open the db and verify it works as expected.
-		db.MustClose()
-		db = mustOpenIndexDB(dbName, s, 0, &isReadOnly)
+		// Re-open the storage and verify it works as expected.
+		s.MustClose()
+		s = MustOpenStorage(path, maxRetentionMsecs, 0, 0)
+
+		db = s.idb()
 		if err := testIndexDBCheckTSIDByName(db, mns, tsids, false); err != nil {
 			t.Fatalf("unexpected error: %s", err)
 		}
+
+		s.MustClose()
+		fs.MustRemoveAll(path)
 	})
 
 	t.Run("concurrent", func(t *testing.T) {
-		s := newTestStorage()
-		defer stopTestStorage(s)
-
-		dbName := nextIndexDBTableName()
-		var isReadOnly uint32
-		db := mustOpenIndexDB(dbName, s, 0, &isReadOnly)
-		defer func() {
-			db.MustClose()
-			if err := os.RemoveAll(dbName); err != nil {
-				t.Fatalf("cannot remove indexDB: %s", err)
-			}
-		}()
+		const path = "TestIndexDB-concurrent"
+		s := MustOpenStorage(path, maxRetentionMsecs, 0, 0)
+		db := s.idb()
 
 		ch := make(chan error, 3)
 		for i := 0; i < cap(ch); i++ {
@@ -566,20 +552,20 @@ func TestIndexDB(t *testing.T) {
 				ch <- nil
 			}()
 		}
-		var errors []error
+		deadlineCh := time.After(30 * time.Second)
 		for i := 0; i < cap(ch); i++ {
 			select {
 			case err := <-ch:
 				if err != nil {
-					errors = append(errors, fmt.Errorf("unexpected error: %w", err))
+					t.Fatalf("unexpected error: %s", err)
 				}
-			case <-time.After(30 * time.Second):
+			case <-deadlineCh:
 				t.Fatalf("timeout")
 			}
 		}
-		if len(errors) > 0 {
-			t.Fatal(errors[0])
-		}
+
+		s.MustClose()
+		fs.MustRemoveAll(path)
 	})
 }
 
@@ -590,11 +576,11 @@ func testIndexDBGetOrCreateTSIDByName(db *indexDB, metricGroups int) ([]MetricNa
 	var tsids []TSID
 
 	is := db.getIndexSearch(noDeadline)
-	defer db.putIndexSearch(is)
+
+	date := uint64(timestampFromTime(time.Now())) / msecPerDay
 
 	var metricNameBuf []byte
-	var metricNameRawBuf []byte
-	for i := 0; i < 4e2+1; i++ {
+	for i := 0; i < 401; i++ {
 		var mn MetricName
 
 		// Init MetricGroup.
@@ -609,27 +595,21 @@ func testIndexDBGetOrCreateTSIDByName(db *indexDB, metricGroups int) ([]MetricNa
 		}
 		mn.sortTags()
 		metricNameBuf = mn.Marshal(metricNameBuf[:0])
-		metricNameRawBuf = mn.marshalRaw(metricNameRawBuf[:0])
 
 		// Create tsid for the metricName.
-		var tsid TSID
-		if err := is.GetOrCreateTSIDByName(&tsid, metricNameBuf, metricNameRawBuf, 0); err != nil {
-			return nil, nil, fmt.Errorf("unexpected error when creating tsid for mn:\n%s: %w", &mn, err)
+		var genTSID generationTSID
+		if !is.getTSIDByMetricName(&genTSID, metricNameBuf, date) {
+			generateTSID(&genTSID.TSID, &mn)
+			createAllIndexesForMetricName(is, &mn, &genTSID.TSID, date)
 		}
 
 		mns = append(mns, mn)
-		tsids = append(tsids, tsid)
-	}
-
-	// fill Date -> MetricID cache
-	date := uint64(timestampFromTime(time.Now())) / msecPerDay
-	for i := range tsids {
-		tsid := &tsids[i]
-		is.createPerDayIndexes(date, tsid.MetricID, &mns[i])
+		tsids = append(tsids, genTSID.TSID)
 	}
+	db.putIndexSearch(is)
 
 	// Flush index to disk, so it becomes visible for search
-	db.tb.DebugFlush()
+	db.s.DebugFlush()
 
 	return mns, tsids, nil
 }
@@ -644,8 +624,9 @@ func testIndexDBCheckTSIDByName(db *indexDB, mns []MetricName, tsids []TSID, isC
 		return false
 	}
 
+	currentTime := timestampFromTime(time.Now())
 	timeseriesCounters := make(map[uint64]bool)
-	var tsidCopy TSID
+	var genTSID generationTSID
 	var metricNameCopy []byte
 	allLabelNames := make(map[string]bool)
 	for i := range mns {
@@ -658,26 +639,29 @@ func testIndexDBCheckTSIDByName(db *indexDB, mns []MetricName, tsids []TSID, isC
 		mn.sortTags()
 		metricName := mn.Marshal(nil)
 
-		if err := db.getTSIDByNameNoCreate(&tsidCopy, metricName); err != nil {
-			return fmt.Errorf("cannot obtain tsid #%d for mn %s: %w", i, mn, err)
+		is := db.getIndexSearch(noDeadline)
+		if !is.getTSIDByMetricName(&genTSID, metricName, uint64(currentTime)/msecPerDay) {
+			return fmt.Errorf("cannot obtain tsid #%d for mn %s", i, mn)
 		}
+		db.putIndexSearch(is)
+
 		if isConcurrent {
 			// Copy tsid.MetricID, since multiple TSIDs may match
 			// the same mn in concurrent mode.
-			tsidCopy.MetricID = tsid.MetricID
+			genTSID.TSID.MetricID = tsid.MetricID
 		}
-		if !reflect.DeepEqual(tsid, &tsidCopy) {
-			return fmt.Errorf("unexpected tsid for mn:\n%s\ngot\n%+v\nwant\n%+v", mn, &tsidCopy, tsid)
+		if !reflect.DeepEqual(tsid, &genTSID.TSID) {
+			return fmt.Errorf("unexpected tsid for mn:\n%s\ngot\n%+v\nwant\n%+v", mn, &genTSID.TSID, tsid)
 		}
 
 		// Search for metric name for the given metricID.
 		var err error
-		metricNameCopy, err = db.searchMetricNameWithCache(metricNameCopy[:0], tsidCopy.MetricID)
+		metricNameCopy, err = db.searchMetricNameWithCache(metricNameCopy[:0], genTSID.TSID.MetricID)
 		if err != nil {
-			return fmt.Errorf("error in searchMetricNameWithCache for metricID=%d; i=%d: %w", tsidCopy.MetricID, i, err)
+			return fmt.Errorf("error in searchMetricNameWithCache for metricID=%d; i=%d: %w", genTSID.TSID.MetricID, i, err)
 		}
 		if !bytes.Equal(metricName, metricNameCopy) {
-			return fmt.Errorf("unexpected mn for metricID=%d;\ngot\n%q\nwant\n%q", tsidCopy.MetricID, metricNameCopy, metricName)
+			return fmt.Errorf("unexpected mn for metricID=%d;\ngot\n%q\nwant\n%q", genTSID.TSID.MetricID, metricNameCopy, metricName)
 		}
 
 		// Try searching metric name for non-existent MetricID.
@@ -738,7 +722,6 @@ func testIndexDBCheckTSIDByName(db *indexDB, mns []MetricName, tsids []TSID, isC
 	}
 
 	// Try tag filters.
-	currentTime := timestampFromTime(time.Now())
 	tr := TimeRange{
 		MinTimestamp: currentTime - msecPerDay,
 		MaxTimestamp: currentTime + msecPerDay,
@@ -1447,12 +1430,6 @@ func TestIndexDBRepopulateAfterRotation(t *testing.T) {
 	r := rand.New(rand.NewSource(1))
 	path := "TestIndexRepopulateAfterRotation"
 	s := MustOpenStorage(path, msecsPerMonth, 1e5, 1e5)
-	defer func() {
-		s.MustClose()
-		if err := os.RemoveAll(path); err != nil {
-			t.Fatalf("cannot remove %q: %s", path, err)
-		}
-	}()
 
 	db := s.idb()
 	if db.generation == 0 {
@@ -1460,9 +1437,11 @@ func TestIndexDBRepopulateAfterRotation(t *testing.T) {
 	}
 
 	const metricRowsN = 1000
-	// use min-max timestamps of 1month range to create smaller number of partitions
-	timeMin, timeMax := time.Now().Add(-730*time.Hour), time.Now()
-	mrs := testGenerateMetricRows(r, metricRowsN, timeMin.UnixMilli(), timeMax.UnixMilli())
+
+	currentDayTimestamp := (time.Now().UnixMilli() / msecPerDay) * msecPerDay
+	timeMin := currentDayTimestamp - 24*3600*1000
+	timeMax := currentDayTimestamp + 24*3600*1000
+	mrs := testGenerateMetricRows(r, metricRowsN, timeMin, timeMax)
 	if err := s.AddRows(mrs, defaultPrecisionBits); err != nil {
 		t.Fatalf("unexpected error when adding mrs: %s", err)
 	}
@@ -1476,7 +1455,7 @@ func TestIndexDBRepopulateAfterRotation(t *testing.T) {
 	}
 
 	// check new series were registered in indexDB
-	added := atomic.LoadUint64(&db.newTimeseriesCreated)
+	added := atomic.LoadUint64(&db.s.newTimeseriesCreated)
 	if added != metricRowsN {
 		t.Fatalf("expected indexDB to contain %d rows; got %d", metricRowsN, added)
 	}
@@ -1516,47 +1495,31 @@ func TestIndexDBRepopulateAfterRotation(t *testing.T) {
 		t.Fatalf("expected new indexDB generation %d to be different from prev indexDB", dbNew.generation)
 	}
 
-	// Re-insert rows again and verify that entries belong prevGeneration and dbNew.generation,
-	// while the majority of entries remain at prevGeneration.
+	// Re-insert rows again and verify that all the entries belong to new generation
 	if err := s.AddRows(mrs, defaultPrecisionBits); err != nil {
 		t.Fatalf("unexpected error when adding mrs: %s", err)
 	}
 	s.DebugFlush()
-	entriesByGeneration := make(map[uint64]int)
+
 	for _, mr := range mrs {
 		s.getTSIDFromCache(&genTSID, mr.MetricNameRaw)
-		entriesByGeneration[genTSID.generation]++
-	}
-	if len(entriesByGeneration) > 2 {
-		t.Fatalf("expecting two generations; got %d", entriesByGeneration)
-	}
-	prevEntries := entriesByGeneration[prevGeneration]
-	currEntries := entriesByGeneration[dbNew.generation]
-	totalEntries := prevEntries + currEntries
-	if totalEntries != metricRowsN {
-		t.Fatalf("unexpected number of entries in tsid cache; got %d; want %d", totalEntries, metricRowsN)
+		if genTSID.generation != dbNew.generation {
+			t.Fatalf("unexpected generation for data after rotation; got %d; want %d", genTSID.generation, dbNew.generation)
+		}
 	}
-	if float64(currEntries)/float64(totalEntries) > 0.1 {
-		t.Fatalf("too big share of entries in the new generation; currEntries=%d, prevEntries=%d", currEntries, prevEntries)
+
+	s.MustClose()
+	if err := os.RemoveAll(path); err != nil {
+		t.Fatalf("cannot remove %q: %s", path, err)
 	}
 }
 
 func TestSearchTSIDWithTimeRange(t *testing.T) {
-	s := newTestStorage()
-	defer stopTestStorage(s)
-
-	dbName := nextIndexDBTableName()
-	var isReadOnly uint32
-	db := mustOpenIndexDB(dbName, s, 0, &isReadOnly)
-	defer func() {
-		db.MustClose()
-		if err := os.RemoveAll(dbName); err != nil {
-			t.Fatalf("cannot remove indexDB: %s", err)
-		}
-	}()
+	const path = "TestSearchTSIDWithTimeRange"
+	s := MustOpenStorage(path, maxRetentionMsecs, 0, 0)
+	db := s.idb()
 
 	is := db.getIndexSearch(noDeadline)
-	defer db.putIndexSearch(is)
 
 	// Create a bunch of per-day time series
 	const days = 5
@@ -1565,7 +1528,6 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
 	now := uint64(timestampFromTime(theDay))
 	baseDate := now / msecPerDay
 	var metricNameBuf []byte
-	var metricNameRawBuf []byte
 	perDayMetricIDs := make(map[uint64]*uint64set.Set)
 	var allMetricIDs uint64set.Set
 	labelNames := []string{
@@ -1576,8 +1538,8 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
 	}
 	sort.Strings(labelNames)
 	for day := 0; day < days; day++ {
-		var tsids []TSID
-		var mns []MetricName
+		date := baseDate - uint64(day)
+		var metricIDs uint64set.Set
 		for metric := 0; metric < metricsPerDay; metric++ {
 			var mn MetricName
 			mn.MetricGroup = []byte("testMetric")
@@ -1600,32 +1562,23 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
 			mn.sortTags()
 
 			metricNameBuf = mn.Marshal(metricNameBuf[:0])
-			metricNameRawBuf = mn.marshalRaw(metricNameRawBuf[:0])
-			var tsid TSID
-			if err := is.GetOrCreateTSIDByName(&tsid, metricNameBuf, metricNameRawBuf, 0); err != nil {
-				t.Fatalf("unexpected error when creating tsid for mn:\n%s: %s", &mn, err)
+			var genTSID generationTSID
+			if !is.getTSIDByMetricName(&genTSID, metricNameBuf, date) {
+				generateTSID(&genTSID.TSID, &mn)
+				createAllIndexesForMetricName(is, &mn, &genTSID.TSID, date)
 			}
-			mns = append(mns, mn)
-			tsids = append(tsids, tsid)
+			metricIDs.Add(genTSID.TSID.MetricID)
 		}
 
-		// Add the metrics to the per-day stores
-		date := baseDate - uint64(day)
-		var metricIDs uint64set.Set
-		for i := range tsids {
-			tsid := &tsids[i]
-			metricIDs.Add(tsid.MetricID)
-			is.createPerDayIndexes(date, tsid.MetricID, &mns[i])
-		}
 		allMetricIDs.Union(&metricIDs)
 		perDayMetricIDs[date] = &metricIDs
 	}
+	db.putIndexSearch(is)
 
 	// Flush index to disk, so it becomes visible for search
-	db.tb.DebugFlush()
+	s.DebugFlush()
 
 	is2 := db.getIndexSearch(noDeadline)
-	defer db.putIndexSearch(is2)
 
 	// Check that all the metrics are found for all the days.
 	for date := baseDate - days + 1; date <= baseDate; date++ {
@@ -1646,6 +1599,7 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
 	if !allMetricIDs.Equal(metricIDs) {
 		t.Fatalf("unexpected metricIDs found;\ngot\n%d\nwant\n%d", metricIDs.AppendTo(nil), allMetricIDs.AppendTo(nil))
 	}
+	db.putIndexSearch(is2)
 
 	// Check SearchLabelNamesWithFiltersOnTimeRange with the specified time range.
 	tr := TimeRange{
@@ -1991,6 +1945,9 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
 	if status.TotalLabelValuePairs != expectedLabelValuePairs {
 		t.Fatalf("unexpected TotalLabelValuePairs; got %d; want %d", status.TotalLabelValuePairs, expectedLabelValuePairs)
 	}
+
+	s.MustClose()
+	fs.MustRemoveAll(path)
 }
 
 func toTFPointers(tfs []tagFilter) []*tagFilter {
@@ -2012,8 +1969,6 @@ func newTestStorage() *Storage {
 		retentionMsecs:    maxRetentionMsecs,
 	}
 	s.setDeletedMetricIDs(&uint64set.Set{})
-	var idb *indexDB
-	s.idbCurr.Store(idb)
 	return s
 }
 
diff --git a/lib/storage/index_db_timing_test.go b/lib/storage/index_db_timing_test.go
index 6c5746bd6..0a2c48c19 100644
--- a/lib/storage/index_db_timing_test.go
+++ b/lib/storage/index_db_timing_test.go
@@ -2,11 +2,12 @@ package storage
 
 import (
 	"fmt"
-	"os"
 	"regexp"
 	"strconv"
 	"testing"
 	"time"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
 )
 
 func BenchmarkRegexpFilterMatch(b *testing.B) {
@@ -38,27 +39,18 @@ func BenchmarkRegexpFilterMismatch(b *testing.B) {
 }
 
 func BenchmarkIndexDBAddTSIDs(b *testing.B) {
-	const recordsPerLoop = 1e3
-
-	s := newTestStorage()
-	defer stopTestStorage(s)
+	const path = "BenchmarkIndexDBAddTSIDs"
+	s := MustOpenStorage(path, maxRetentionMsecs, 0, 0)
+	db := s.idb()
 
-	dbName := nextIndexDBTableName()
-	var isReadOnly uint32
-	db := mustOpenIndexDB(dbName, s, 0, &isReadOnly)
-	defer func() {
-		db.MustClose()
-		if err := os.RemoveAll(dbName); err != nil {
-			b.Fatalf("cannot remove indexDB: %s", err)
-		}
-	}()
+	const recordsPerLoop = 1e3
 
 	b.ReportAllocs()
 	b.SetBytes(recordsPerLoop)
 	b.ResetTimer()
 	b.RunParallel(func(pb *testing.PB) {
 		var mn MetricName
-		var tsid TSID
+		var genTSID generationTSID
 
 		// The most common tags.
 		mn.Tags = []Tag{
@@ -72,16 +64,18 @@ func BenchmarkIndexDBAddTSIDs(b *testing.B) {
 
 		startOffset := 0
 		for pb.Next() {
-			benchmarkIndexDBAddTSIDs(db, &tsid, &mn, startOffset, recordsPerLoop)
+			benchmarkIndexDBAddTSIDs(db, &genTSID, &mn, startOffset, recordsPerLoop)
 			startOffset += recordsPerLoop
 		}
 	})
 	b.StopTimer()
+
+	s.MustClose()
+	fs.MustRemoveAll(path)
 }
 
-func benchmarkIndexDBAddTSIDs(db *indexDB, tsid *TSID, mn *MetricName, startOffset, recordsPerLoop int) {
-	var metricName []byte
-	var metricNameRaw []byte
+func benchmarkIndexDBAddTSIDs(db *indexDB, genTSID *generationTSID, mn *MetricName, startOffset, recordsPerLoop int) {
+	date := uint64(0)
 	is := db.getIndexSearch(noDeadline)
 	defer db.putIndexSearch(is)
 	for i := 0; i < recordsPerLoop; i++ {
@@ -90,48 +84,33 @@ func benchmarkIndexDBAddTSIDs(db *indexDB, tsid *TSID, mn *MetricName, startOffs
 			mn.Tags[j].Value = strconv.AppendUint(mn.Tags[j].Value[:0], uint64(i*j), 16)
 		}
 		mn.sortTags()
-		metricName = mn.Marshal(metricName[:0])
-		metricNameRaw = mn.marshalRaw(metricNameRaw[:0])
-		if err := is.GetOrCreateTSIDByName(tsid, metricName, metricNameRaw, 0); err != nil {
-			panic(fmt.Errorf("cannot insert record: %w", err))
-		}
+
+		generateTSID(&genTSID.TSID, mn)
+		createAllIndexesForMetricName(is, mn, &genTSID.TSID, date)
 	}
 }
 
 func BenchmarkHeadPostingForMatchers(b *testing.B) {
 	// This benchmark is equivalent to https://github.com/prometheus/prometheus/blob/23c0299d85bfeb5d9b59e994861553a25ca578e5/tsdb/head_bench_test.go#L52
 	// See https://www.robustperception.io/evaluating-performance-and-correctness for more details.
-	s := newTestStorage()
-	defer stopTestStorage(s)
-
-	dbName := nextIndexDBTableName()
-	var isReadOnly uint32
-	db := mustOpenIndexDB(dbName, s, 0, &isReadOnly)
-	defer func() {
-		db.MustClose()
-		if err := os.RemoveAll(dbName); err != nil {
-			b.Fatalf("cannot remove indexDB: %s", err)
-		}
-	}()
+	const path = "BenchmarkHeadPostingForMatchers"
+	s := MustOpenStorage(path, maxRetentionMsecs, 0, 0)
+	db := s.idb()
 
 	// Fill the db with data as in https://github.com/prometheus/prometheus/blob/23c0299d85bfeb5d9b59e994861553a25ca578e5/tsdb/head_bench_test.go#L66
-	var mn MetricName
-	var metricName []byte
-	var metricNameRaw []byte
-	var tsid TSID
 	is := db.getIndexSearch(noDeadline)
 	defer db.putIndexSearch(is)
+	var mn MetricName
+	var genTSID generationTSID
+	date := uint64(0)
 	addSeries := func(kvs ...string) {
 		mn.Reset()
 		for i := 0; i < len(kvs); i += 2 {
 			mn.AddTag(kvs[i], kvs[i+1])
 		}
 		mn.sortTags()
-		metricName = mn.Marshal(metricName[:0])
-		metricNameRaw = mn.marshalRaw(metricNameRaw[:0])
-		if err := is.createTSIDByName(&tsid, metricName, metricNameRaw, 0); err != nil {
-			b.Fatalf("cannot insert record: %s", err)
-		}
+		generateTSID(&genTSID.TSID, &mn)
+		createAllIndexesForMetricName(is, &mn, &genTSID.TSID, date)
 	}
 	for n := 0; n < 10; n++ {
 		ns := strconv.Itoa(n)
@@ -147,19 +126,19 @@ func BenchmarkHeadPostingForMatchers(b *testing.B) {
 	}
 
 	// Make sure all the items can be searched.
-	db.tb.DebugFlush()
+	db.s.DebugFlush()
 	b.ResetTimer()
 
 	benchSearch := func(b *testing.B, tfs *TagFilters, expectedMetricIDs int) {
-		is := db.getIndexSearch(noDeadline)
-		defer db.putIndexSearch(is)
 		tfss := []*TagFilters{tfs}
 		tr := TimeRange{
 			MinTimestamp: 0,
 			MaxTimestamp: timestampFromTime(time.Now()),
 		}
 		for i := 0; i < b.N; i++ {
+			is := db.getIndexSearch(noDeadline)
 			metricIDs, err := is.searchMetricIDs(nil, tfss, tr, 2e9)
+			db.putIndexSearch(is)
 			if err != nil {
 				b.Fatalf("unexpected error in searchMetricIDs: %s", err)
 			}
@@ -275,21 +254,15 @@ func BenchmarkHeadPostingForMatchers(b *testing.B) {
 		addTagFilter(tfs, "j", "foo", false, false)
 		benchSearch(b, tfs, 88889)
 	})
+
+	s.MustClose()
+	fs.MustRemoveAll(path)
 }
 
 func BenchmarkIndexDBGetTSIDs(b *testing.B) {
-	s := newTestStorage()
-	defer stopTestStorage(s)
-
-	dbName := nextIndexDBTableName()
-	var isReadOnly uint32
-	db := mustOpenIndexDB(dbName, s, 0, &isReadOnly)
-	defer func() {
-		db.MustClose()
-		if err := os.RemoveAll(dbName); err != nil {
-			b.Fatalf("cannot remove indexDB: %s", err)
-		}
-	}()
+	const path = "BenchmarkIndexDBGetTSIDs"
+	s := MustOpenStorage(path, maxRetentionMsecs, 0, 0)
+	db := s.idb()
 
 	const recordsPerLoop = 1000
 	const recordsCount = 1e5
@@ -302,41 +275,42 @@ func BenchmarkIndexDBGetTSIDs(b *testing.B) {
 		value := fmt.Sprintf("value_%d", i)
 		mn.AddTag(key, value)
 	}
-	var tsid TSID
-	var metricName []byte
-	var metricNameRaw []byte
+	mn.sortTags()
+
+	var genTSID generationTSID
+	date := uint64(12345)
 
 	is := db.getIndexSearch(noDeadline)
 	defer db.putIndexSearch(is)
+
 	for i := 0; i < recordsCount; i++ {
-		mn.sortTags()
-		metricName = mn.Marshal(metricName[:0])
-		metricNameRaw = mn.marshalRaw(metricName[:0])
-		if err := is.GetOrCreateTSIDByName(&tsid, metricName, metricNameRaw, 0); err != nil {
-			b.Fatalf("cannot insert record: %s", err)
-		}
+		generateTSID(&genTSID.TSID, &mn)
+		createAllIndexesForMetricName(is, &mn, &genTSID.TSID, date)
 	}
+	db.s.DebugFlush()
 
 	b.SetBytes(recordsPerLoop)
 	b.ReportAllocs()
 	b.ResetTimer()
 	b.RunParallel(func(pb *testing.PB) {
-		var tsidLocal TSID
+		var genTSIDLocal generationTSID
 		var metricNameLocal []byte
-		var metricNameLocalRaw []byte
-		mnLocal := mn
-		is := db.getIndexSearch(noDeadline)
-		defer db.putIndexSearch(is)
+		var mnLocal MetricName
+		mnLocal.CopyFrom(&mn)
+		mnLocal.sortTags()
 		for pb.Next() {
+			is := db.getIndexSearch(noDeadline)
 			for i := 0; i < recordsPerLoop; i++ {
-				mnLocal.sortTags()
 				metricNameLocal = mnLocal.Marshal(metricNameLocal[:0])
-				metricNameLocalRaw = mnLocal.marshalRaw(metricNameLocalRaw[:0])
-				if err := is.GetOrCreateTSIDByName(&tsidLocal, metricNameLocal, metricNameLocalRaw, 0); err != nil {
-					panic(fmt.Errorf("cannot obtain tsid: %w", err))
+				if !is.getTSIDByMetricName(&genTSIDLocal, metricNameLocal, date) {
+					panic(fmt.Errorf("cannot obtain tsid for row %d", i))
 				}
 			}
+			db.putIndexSearch(is)
 		}
 	})
 	b.StopTimer()
+
+	s.MustClose()
+	fs.MustRemoveAll(path)
 }
diff --git a/lib/storage/merge_test.go b/lib/storage/merge_test.go
index 35a6d7d94..4017e6532 100644
--- a/lib/storage/merge_test.go
+++ b/lib/storage/merge_test.go
@@ -384,6 +384,7 @@ func TestMergeForciblyStop(t *testing.T) {
 	if rowsDeleted != 0 {
 		t.Fatalf("unexpected rowsDeleted; got %d; want %d", rowsDeleted, 0)
 	}
+	stopTestStorage(strg)
 }
 
 func testMergeBlockStreams(t *testing.T, bsrs []*blockStreamReader, expectedBlocksCount, expectedRowsCount int, expectedMinTimestamp, expectedMaxTimestamp int64) {
@@ -399,6 +400,7 @@ func testMergeBlockStreams(t *testing.T, bsrs []*blockStreamReader, expectedBloc
 	if err := mergeBlockStreams(&mp.ph, &bsw, bsrs, nil, strg, 0, &rowsMerged, &rowsDeleted); err != nil {
 		t.Fatalf("unexpected error in mergeBlockStreams: %s", err)
 	}
+	stopTestStorage(strg)
 
 	// Verify written data.
 	if mp.ph.RowsCount != uint64(expectedRowsCount) {
diff --git a/lib/storage/merge_timing_test.go b/lib/storage/merge_timing_test.go
index 34ddde98f..324812d9f 100644
--- a/lib/storage/merge_timing_test.go
+++ b/lib/storage/merge_timing_test.go
@@ -47,6 +47,8 @@ func benchmarkMergeBlockStreams(b *testing.B, mps []*inmemoryPart, rowsPerLoop i
 			}
 		}
 	})
+
+	stopTestStorage(strg)
 }
 
 var benchTwoSourcesWorstCaseMPS = func() []*inmemoryPart {
diff --git a/lib/storage/partition_search_test.go b/lib/storage/partition_search_test.go
index 86d5bd81c..4e37ea6ae 100644
--- a/lib/storage/partition_search_test.go
+++ b/lib/storage/partition_search_test.go
@@ -172,14 +172,6 @@ func testPartitionSearchEx(t *testing.T, ptt int64, tr TimeRange, partsCount, ma
 	pt := mustCreatePartition(ptt, "small-table", "big-table", strg)
 	smallPartsPath := pt.smallPartsPath
 	bigPartsPath := pt.bigPartsPath
-	defer func() {
-		if err := os.RemoveAll("small-table"); err != nil {
-			t.Fatalf("cannot remove small parts directory: %s", err)
-		}
-		if err := os.RemoveAll("big-table"); err != nil {
-			t.Fatalf("cannot remove big parts directory: %s", err)
-		}
-	}()
 	var tmpRows []rawRow
 	for _, rows := range rowss {
 		pt.AddRows(rows)
@@ -194,6 +186,14 @@ func testPartitionSearchEx(t *testing.T, ptt int64, tr TimeRange, partsCount, ma
 	pt = mustOpenPartition(smallPartsPath, bigPartsPath, strg)
 	testPartitionSearch(t, pt, tsids, tr, rbsExpected, rowsCountExpected)
 	pt.MustClose()
+	stopTestStorage(strg)
+
+	if err := os.RemoveAll("small-table"); err != nil {
+		t.Fatalf("cannot remove small parts directory: %s", err)
+	}
+	if err := os.RemoveAll("big-table"); err != nil {
+		t.Fatalf("cannot remove big parts directory: %s", err)
+	}
 }
 
 func testPartitionSearch(t *testing.T, pt *partition, tsids []TSID, tr TimeRange, rbsExpected []rawBlock, rowsCountExpected int64) {
diff --git a/lib/storage/storage_test.go b/lib/storage/storage_test.go
index ca165d4ba..a20b37795 100644
--- a/lib/storage/storage_test.go
+++ b/lib/storage/storage_test.go
@@ -823,9 +823,7 @@ func testStorageRegisterMetricNames(s *Storage) error {
 			}
 			mrs = append(mrs, mr)
 		}
-		if err := s.RegisterMetricNames(nil, mrs); err != nil {
-			return fmt.Errorf("unexpected error in RegisterMetricNames: %w", err)
-		}
+		s.RegisterMetricNames(nil, mrs)
 	}
 	var addIDsExpected []string
 	for k := range addIDsMap {
@@ -988,7 +986,7 @@ func testStorageAddRows(rng *rand.Rand, s *Storage) error {
 	const addsCount = 10
 
 	maxTimestamp := timestampFromTime(time.Now())
-	minTimestamp := maxTimestamp - s.retentionMsecs
+	minTimestamp := maxTimestamp - s.retentionMsecs + 3600*1000
 	for i := 0; i < addsCount; i++ {
 		mrs := testGenerateMetricRows(rng, rowsPerAdd, minTimestamp, maxTimestamp)
 		if err := s.AddRows(mrs, defaultPrecisionBits); err != nil {
@@ -1030,7 +1028,9 @@ func testStorageAddRows(rng *rand.Rand, s *Storage) error {
 		return fmt.Errorf("snapshot %q must contain at least %d rows; got %d", snapshotPath, minRowsExpected, rowsCount)
 	}
 
-	// Verify that force merge for the snapshot leaves only a single part per partition.
+	// Verify that force merge for the snapshot leaves at most a single part per partition.
+	// Zero parts are possible if the snapshot is created just after the partition has been created
+	// by concurrent goroutine, but it didn't put the data into it yet.
 	if err := s1.ForceMergePartitions(""); err != nil {
 		return fmt.Errorf("error when force merging partitions: %w", err)
 	}
@@ -1039,9 +1039,9 @@ func testStorageAddRows(rng *rand.Rand, s *Storage) error {
 		pws := ptw.pt.GetParts(nil, true)
 		numParts := len(pws)
 		ptw.pt.PutParts(pws)
-		if numParts != 1 {
+		if numParts > 1 {
 			s1.tb.PutPartitions(ptws)
-			return fmt.Errorf("unexpected number of parts for partition %q after force merge; got %d; want 1", ptw.pt.name, numParts)
+			return fmt.Errorf("unexpected number of parts for partition %q after force merge; got %d; want at most 1", ptw.pt.name, numParts)
 		}
 	}
 	s1.tb.PutPartitions(ptws)
diff --git a/lib/storage/table_search_test.go b/lib/storage/table_search_test.go
index 20a387061..d92e8f786 100644
--- a/lib/storage/table_search_test.go
+++ b/lib/storage/table_search_test.go
@@ -203,6 +203,7 @@ func testTableSearchEx(t *testing.T, rng *rand.Rand, trData, trSearch TimeRange,
 	tb = mustOpenTable("test-table", strg)
 	testTableSearch(t, tb, tsids, trSearch, rbsExpected, rowsCountExpected)
 	tb.MustClose()
+	stopTestStorage(strg)
 }
 
 func testTableSearch(t *testing.T, tb *table, tsids []TSID, tr TimeRange, rbsExpected []rawBlock, rowsCountExpected int64) {
diff --git a/lib/storage/table_search_timing_test.go b/lib/storage/table_search_timing_test.go
index 1e9a62b48..a0c8fd376 100644
--- a/lib/storage/table_search_timing_test.go
+++ b/lib/storage/table_search_timing_test.go
@@ -38,7 +38,7 @@ func BenchmarkTableSearch(b *testing.B) {
 	}
 }
 
-func openBenchTable(b *testing.B, startTimestamp int64, rowsPerInsert, rowsCount, tsidsCount int) *table {
+func openBenchTable(b *testing.B, startTimestamp int64, rowsPerInsert, rowsCount, tsidsCount int) (*table, *Storage) {
 	b.Helper()
 
 	path := filepath.Join("benchmarkTableSearch", fmt.Sprintf("rows%d_tsids%d", rowsCount, tsidsCount))
@@ -58,7 +58,7 @@ func openBenchTable(b *testing.B, startTimestamp int64, rowsPerInsert, rowsCount
 		b.Fatalf("unexpected rows count in the table %q; got %d; want %d", path, rowsCount, rowsCountExpected)
 	}
 
-	return tb
+	return tb, strg
 }
 
 var createdBenchTables = make(map[string]bool)
@@ -98,13 +98,14 @@ func createBenchTable(b *testing.B, path string, startTimestamp int64, rowsPerIn
 	wg.Wait()
 
 	tb.MustClose()
+	stopTestStorage(strg)
 }
 
 func benchmarkTableSearch(b *testing.B, rowsCount, tsidsCount, tsidsSearch int) {
 	startTimestamp := timestampFromTime(time.Now()) - 365*24*3600*1000
 	rowsPerInsert := getMaxRawRowsPerShard()
 
-	tb := openBenchTable(b, startTimestamp, rowsPerInsert, rowsCount, tsidsCount)
+	tb, strg := openBenchTable(b, startTimestamp, rowsPerInsert, rowsCount, tsidsCount)
 	tr := TimeRange{
 		MinTimestamp: startTimestamp,
 		MaxTimestamp: (1 << 63) - 1,
@@ -135,4 +136,5 @@ func benchmarkTableSearch(b *testing.B, rowsCount, tsidsCount, tsidsSearch int)
 	b.StopTimer()
 
 	tb.MustClose()
+	stopTestStorage(strg)
 }
diff --git a/lib/storage/table_test.go b/lib/storage/table_test.go
index 2e8da8aad..c664a55fe 100644
--- a/lib/storage/table_test.go
+++ b/lib/storage/table_test.go
@@ -29,4 +29,6 @@ func TestTableOpenClose(t *testing.T) {
 		tb := mustOpenTable(path, strg)
 		tb.MustClose()
 	}
+
+	stopTestStorage(strg)
 }
diff --git a/lib/storage/table_timing_test.go b/lib/storage/table_timing_test.go
index 56865900f..d7363105f 100644
--- a/lib/storage/table_timing_test.go
+++ b/lib/storage/table_timing_test.go
@@ -104,4 +104,5 @@ func benchmarkTableAddRows(b *testing.B, rowsPerInsert, tsidsCount int) {
 			b.Fatalf("cannot remove table %q: %s", tablePath, err)
 		}
 	}
+	stopTestStorage(strg)
 }