Repository: prometheus/prometheus
Pull request #12664 (2023-08-08 07:37:12)

Cleanup temporary chunk snapshot dirs
Simlar to cleanup of WAL files on startup, cleanup temporary chunk_snapshot dirs. This prevents storage space leaks due to terminated snapshots on shutdown.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 057342ded..d316e84d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,26 +1,5 @@
 # Changelog
 
-## 2.46.0 / 2023-07-25
-
-* [FEATURE] Promtool: Add PromQL format and label matcher set/delete commands to promtool. #11411
-* [FEATURE] Promtool: Add push metrics command. #12299
-* [ENHANCEMENT] Promtool: Read from stdin if no filenames are provided in check rules. #12225
-* [ENHANCEMENT] Hetzner SD: Support larger ID's that will be used by Hetzner in September. #12569
-* [ENHANCEMENT] Kubernetes SD: Add more labels for endpointslice and endpoints role. #10914
-* [ENHANCEMENT] Kubernetes SD: Do not add pods to target group if the PodIP status is not set. #11642
-* [ENHANCEMENT] OpenStack SD: Include instance image ID in labels. #12502
-* [ENHANCEMENT] Remote Write receiver: Validate the metric names and labels. #11688
-* [ENHANCEMENT] Web: Initialize `prometheus_http_requests_total` metrics with `code` label set to `200`. #12472
-* [ENHANCEMENT] TSDB: Add Zstandard compression option for wlog. #11666
-* [ENHANCEMENT] TSDB: Support native histograms in snapshot on shutdown. #12258
-* [ENHANCEMENT] Labels: Avoid compiling regexes that are literal. #12434
-* [BUGFIX] Histograms: Fix parsing of float histograms without zero bucket. #12577
-* [BUGFIX] Histograms: Fix scraping native and classic histograms missing some histograms. #12554
-* [BUGFIX] Histograms: Enable ingestion of multiple exemplars per sample. 12557
-* [BUGFIX] File SD: Fix path handling in File-SD watcher to allow directory monitoring on Windows. #12488
-* [BUGFIX] Linode SD: Cast `InstanceSpec` values to `int64` to avoid overflows on 386 architecture. #12568
-* [BUGFIX] PromQL Engine: Include query parsing in active-query tracking. #12418
-* [BUGFIX] TSDB: Handle TOC parsing failures. #10623
 
 ## 2.45.0 / 2023-06-23
 
diff --git a/VERSION b/VERSION
index cc8758303..e599014ea 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-2.46.0
+2.45.0
diff --git a/discovery/hetzner/hcloud.go b/discovery/hetzner/hcloud.go
index 6d0599dfa..4bcfde830 100644
--- a/discovery/hetzner/hcloud.go
+++ b/discovery/hetzner/hcloud.go
@@ -91,7 +91,7 @@ func (d *hcloudDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, er
 	targets := make([]model.LabelSet, len(servers))
 	for i, server := range servers {
 		labels := model.LabelSet{
-			hetznerLabelRole:              model.LabelValue(HetznerRoleHcloud),
+			hetznerLabelRole:              model.LabelValue(hetznerRoleHcloud),
 			hetznerLabelServerID:          model.LabelValue(fmt.Sprintf("%d", server.ID)),
 			hetznerLabelServerName:        model.LabelValue(server.Name),
 			hetznerLabelDatacenter:        model.LabelValue(server.Datacenter.Name),
diff --git a/discovery/hetzner/hetzner.go b/discovery/hetzner/hetzner.go
index c3f7ec39c..40b28cc2c 100644
--- a/discovery/hetzner/hetzner.go
+++ b/discovery/hetzner/hetzner.go
@@ -57,7 +57,7 @@ type SDConfig struct {
 
 	RefreshInterval model.Duration `yaml:"refresh_interval"`
 	Port            int            `yaml:"port"`
-	Role            Role           `yaml:"role"`
+	Role            role           `yaml:"role"`
 	hcloudEndpoint  string         // For tests only.
 	robotEndpoint   string         // For tests only.
 }
@@ -74,26 +74,26 @@ type refresher interface {
 	refresh(context.Context) ([]*targetgroup.Group, error)
 }
 
-// Role is the Role of the target within the Hetzner Ecosystem.
-type Role string
+// role is the role of the target within the Hetzner Ecosystem.
+type role string
 
 // The valid options for role.
 const (
 	// Hetzner Robot Role (Dedicated Server)
 	// https://robot.hetzner.com
-	HetznerRoleRobot Role = "robot"
+	hetznerRoleRobot role = "robot"
 	// Hetzner Cloud Role
 	// https://console.hetzner.cloud
-	HetznerRoleHcloud Role = "hcloud"
+	hetznerRoleHcloud role = "hcloud"
 )
 
 // UnmarshalYAML implements the yaml.Unmarshaler interface.
-func (c *Role) UnmarshalYAML(unmarshal func(interface{}) error) error {
+func (c *role) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	if err := unmarshal((*string)(c)); err != nil {
 		return err
 	}
 	switch *c {
-	case HetznerRoleRobot, HetznerRoleHcloud:
+	case hetznerRoleRobot, hetznerRoleHcloud:
 		return nil
 	default:
 		return fmt.Errorf("unknown role %q", *c)
@@ -143,12 +143,12 @@ func NewDiscovery(conf *SDConfig, logger log.Logger) (*refresh.Discovery, error)
 
 func newRefresher(conf *SDConfig, l log.Logger) (refresher, error) {
 	switch conf.Role {
-	case HetznerRoleHcloud:
+	case hetznerRoleHcloud:
 		if conf.hcloudEndpoint == "" {
 			conf.hcloudEndpoint = hcloud.Endpoint
 		}
 		return newHcloudDiscovery(conf, l)
-	case HetznerRoleRobot:
+	case hetznerRoleRobot:
 		if conf.robotEndpoint == "" {
 			conf.robotEndpoint = "https://robot-ws.your-server.de"
 		}
diff --git a/discovery/hetzner/robot.go b/discovery/hetzner/robot.go
index 1d8aa9302..496088028 100644
--- a/discovery/hetzner/robot.go
+++ b/discovery/hetzner/robot.go
@@ -105,7 +105,7 @@ func (d *robotDiscovery) refresh(context.Context) ([]*targetgroup.Group, error)
 	targets := make([]model.LabelSet, len(servers))
 	for i, server := range servers {
 		labels := model.LabelSet{
-			hetznerLabelRole:           model.LabelValue(HetznerRoleRobot),
+			hetznerLabelRole:           model.LabelValue(hetznerRoleRobot),
 			hetznerLabelServerID:       model.LabelValue(strconv.Itoa(server.Server.ServerNumber)),
 			hetznerLabelServerName:     model.LabelValue(server.Server.ServerName),
 			hetznerLabelDatacenter:     model.LabelValue(strings.ToLower(server.Server.Dc)),
diff --git a/docs/querying/basics.md b/docs/querying/basics.md
index a4e17c5c6..9eb95c66e 100644
--- a/docs/querying/basics.md
+++ b/docs/querying/basics.md
@@ -35,7 +35,7 @@ vector is the only type that can be directly graphed.
 _Notes about the experimental native histograms:_
 
 * Ingesting native histograms has to be enabled via a [feature
-  flag](../feature_flags.md#native-histograms).
+  flag](../feature_flags/#native-histograms).
 * Once native histograms have been ingested into the TSDB (and even after
   disabling the feature flag again), both instant vectors and range vectors may
   now contain samples that aren't simple floating point numbers (float samples)
diff --git a/docs/querying/functions.md b/docs/querying/functions.md
index 151a1c10b..bec87a69d 100644
--- a/docs/querying/functions.md
+++ b/docs/querying/functions.md
@@ -14,7 +14,7 @@ vector, which if not provided it will default to the value of the expression
 _Notes about the experimental native histograms:_
 
 * Ingesting native histograms has to be enabled via a [feature
-  flag](../feature_flags.md#native-histograms). As long as no native histograms
+  flag](../feature_flags/#native-histograms). As long as no native histograms
   have been ingested into the TSDB, all functions will behave as usual.
 * Functions that do not explicitly mention native histograms in their
   documentation (see below) will ignore histogram samples.
diff --git a/docs/querying/operators.md b/docs/querying/operators.md
index 2a9376d77..c691a0f1a 100644
--- a/docs/querying/operators.md
+++ b/docs/querying/operators.md
@@ -310,7 +310,7 @@ so `2 ^ 3 ^ 2` is equivalent to `2 ^ (3 ^ 2)`.
 ## Operators for native histograms
 
 Native histograms are an experimental feature. Ingesting native histograms has
-to be enabled via a [feature flag](../feature_flags.md#native-histograms). Once
+to be enabled via a [feature flag](../feature_flags/#native-histograms). Once
 native histograms have been ingested, they can be queried (even after the
 feature flag has been disabled again). However, the operator support for native
 histograms is still very limited.
diff --git a/go.mod b/go.mod
index 3e97cdda0..1ddf77184 100644
--- a/go.mod
+++ b/go.mod
@@ -50,11 +50,11 @@ require (
 	github.com/prometheus/common/assets v0.2.0
 	github.com/prometheus/common/sigv4 v0.1.0
 	github.com/prometheus/exporter-toolkit v0.10.0
-	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.20
+	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.19
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
 	github.com/stretchr/testify v1.8.4
 	github.com/vultr/govultr/v2 v2.17.2
-	go.opentelemetry.io/collector/pdata v1.0.0-rcv0014
+	go.opentelemetry.io/collector/pdata v0.66.0
 	go.opentelemetry.io/collector/semconv v0.81.0
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0
 	go.opentelemetry.io/otel v1.16.0
@@ -66,7 +66,7 @@ require (
 	go.uber.org/atomic v1.11.0
 	go.uber.org/automaxprocs v1.5.2
 	go.uber.org/goleak v1.2.1
-	go.uber.org/multierr v1.11.0
+	go.uber.org/multierr v1.8.0
 	golang.org/x/net v0.12.0
 	golang.org/x/oauth2 v0.10.0
 	golang.org/x/sync v0.3.0
diff --git a/go.sum b/go.sum
index f5661fc9e..63c60005a 100644
--- a/go.sum
+++ b/go.sum
@@ -706,8 +706,8 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.20 h1:a9hSJdJcd16e0HoMsnFvaHvxB3pxSD+SC7+CISp7xY0=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.20/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.19 h1:+1H+N9QFl2Sfvia0FBYfMrHYHYhmpZxhSE0wpPL2lYs=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.19/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/shoenig/test v0.6.6 h1:Oe8TPH9wAbv++YPNDKJWUnI8Q4PPWCx3UbOfH+FxiMU=
@@ -797,8 +797,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/collector/pdata v1.0.0-rcv0014 h1:iT5qH0NLmkGeIdDtnBogYDx7L58t6CaWGL378DEo2QY=
-go.opentelemetry.io/collector/pdata v1.0.0-rcv0014/go.mod h1:BRvDrx43kiSoUx3mr7SoA7h9B8+OY99mUK+CZSQFWW4=
+go.opentelemetry.io/collector/pdata v0.66.0 h1:UdE5U6MsDNzuiWaXdjGx2lC3ElVqWmN/hiUE8vyvSuM=
+go.opentelemetry.io/collector/pdata v0.66.0/go.mod h1:pqyaznLzk21m+1KL6fwOsRryRELL+zNM0qiVSn0MbVc=
 go.opentelemetry.io/collector/semconv v0.81.0 h1:lCYNNo3powDvFIaTPP2jDKIrBiV1T92NK4QgL/aHYXw=
 go.opentelemetry.io/collector/semconv v0.81.0/go.mod h1:TlYPtzvsXyHOgr5eATi43qEMqwSmIziivJB2uctKswo=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
@@ -824,6 +824,7 @@ go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lI
 go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
@@ -832,8 +833,8 @@ go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
 go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
-go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
-go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
+go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
diff --git a/promql/engine.go b/promql/engine.go
index cafbab4e3..e2092a800 100644
--- a/promql/engine.go
+++ b/promql/engine.go
@@ -1387,7 +1387,15 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, storage.Warnings) {
 			unwrapParenExpr(&arg)
 			vs, ok := arg.(*parser.VectorSelector)
 			if ok {
-				return ev.rangeEvalTimestampFunctionOverVectorSelector(vs, call, e)
+				return ev.rangeEval(nil, func(v []parser.Value, _ [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
+					if vs.Timestamp != nil {
+						// This is a special case only for "timestamp" since the offset
+						// needs to be adjusted for every point.
+						vs.Offset = time.Duration(enh.Ts-*vs.Timestamp) * time.Millisecond
+					}
+					val, ws := ev.vectorSelector(vs, enh.Ts)
+					return call([]parser.Value{val}, e.Args, enh), ws
+				})
 			}
 		}
 
@@ -1825,48 +1833,38 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, storage.Warnings) {
 	panic(fmt.Errorf("unhandled expression of type: %T", expr))
 }
 
-func (ev *evaluator) rangeEvalTimestampFunctionOverVectorSelector(vs *parser.VectorSelector, call FunctionCall, e *parser.Call) (parser.Value, storage.Warnings) {
-	ws, err := checkAndExpandSeriesSet(ev.ctx, vs)
+// vectorSelector evaluates a *parser.VectorSelector expression.
+func (ev *evaluator) vectorSelector(node *parser.VectorSelector, ts int64) (Vector, storage.Warnings) {
+	ws, err := checkAndExpandSeriesSet(ev.ctx, node)
 	if err != nil {
 		ev.error(errWithWarnings{fmt.Errorf("expanding series: %w", err), ws})
 	}
+	vec := make(Vector, 0, len(node.Series))
+	it := storage.NewMemoizedEmptyIterator(durationMilliseconds(ev.lookbackDelta))
+	var chkIter chunkenc.Iterator
+	for i, s := range node.Series {
+		chkIter = s.Iterator(chkIter)
+		it.Reset(chkIter)
 
-	seriesIterators := make([]*storage.MemoizedSeriesIterator, len(vs.Series))
-	for i, s := range vs.Series {
-		it := s.Iterator(nil)
-		seriesIterators[i] = storage.NewMemoizedIterator(it, durationMilliseconds(ev.lookbackDelta))
-	}
-
-	return ev.rangeEval(nil, func(v []parser.Value, _ [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
-		if vs.Timestamp != nil {
-			// This is a special case for "timestamp()" when the @ modifier is used, to ensure that
-			// we return a point for each time step in this case.
-			// See https://github.com/prometheus/prometheus/issues/8433.
-			vs.Offset = time.Duration(enh.Ts-*vs.Timestamp) * time.Millisecond
-		}
-
-		vec := make(Vector, 0, len(vs.Series))
-		for i, s := range vs.Series {
-			it := seriesIterators[i]
-			t, f, h, ok := ev.vectorSelectorSingle(it, vs, enh.Ts)
-			if ok {
-				vec = append(vec, Sample{
-					Metric: s.Labels(),
-					T:      t,
-					F:      f,
-					H:      h,
-				})
+		t, f, h, ok := ev.vectorSelectorSingle(it, node, ts)
+		if ok {
+			vec = append(vec, Sample{
+				Metric: node.Series[i].Labels(),
+				T:      t,
+				F:      f,
+				H:      h,
+			})
 
-				ev.currentSamples++
-				ev.samplesStats.IncrementSamplesAtTimestamp(enh.Ts, 1)
-				if ev.currentSamples > ev.maxSamples {
-					ev.error(ErrTooManySamples(env))
-				}
+			ev.currentSamples++
+			ev.samplesStats.IncrementSamplesAtTimestamp(ts, 1)
+			if ev.currentSamples > ev.maxSamples {
+				ev.error(ErrTooManySamples(env))
 			}
 		}
-		ev.samplesStats.UpdatePeak(ev.currentSamples)
-		return call([]parser.Value{vec}, e.Args, enh), ws
-	})
+
+	}
+	ev.samplesStats.UpdatePeak(ev.currentSamples)
+	return vec, ws
 }
 
 // vectorSelectorSingle evaluates an instant vector for the iterator of one time series.
diff --git a/tsdb/agent/db.go b/tsdb/agent/db.go
index 77b77fc23..2ed51e4b8 100644
--- a/tsdb/agent/db.go
+++ b/tsdb/agent/db.go
@@ -42,7 +42,6 @@ import (
 	"github.com/prometheus/prometheus/tsdb/record"
 	"github.com/prometheus/prometheus/tsdb/tsdbutil"
 	"github.com/prometheus/prometheus/tsdb/wlog"
-	"github.com/prometheus/prometheus/util/zeropool"
 )
 
 const (
@@ -412,13 +411,28 @@ func (db *DB) loadWAL(r *wlog.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 		dec     record.Decoder
 		lastRef = chunks.HeadSeriesRef(db.nextRef.Load())
 
-		decoded = make(chan interface{}, 10)
-		errCh   = make(chan error, 1)
-
-		seriesPool          zeropool.Pool[[]record.RefSeries]
-		samplesPool         zeropool.Pool[[]record.RefSample]
-		histogramsPool      zeropool.Pool[[]record.RefHistogramSample]
-		floatHistogramsPool zeropool.Pool[[]record.RefFloatHistogramSample]
+		decoded    = make(chan interface{}, 10)
+		errCh      = make(chan error, 1)
+		seriesPool = sync.Pool{
+			New: func() interface{} {
+				return []record.RefSeries{}
+			},
+		}
+		samplesPool = sync.Pool{
+			New: func() interface{} {
+				return []record.RefSample{}
+			},
+		}
+		histogramsPool = sync.Pool{
+			New: func() interface{} {
+				return []record.RefHistogramSample{}
+			},
+		}
+		floatHistogramsPool = sync.Pool{
+			New: func() interface{} {
+				return []record.RefFloatHistogramSample{}
+			},
+		}
 	)
 
 	go func() {
@@ -428,7 +442,7 @@ func (db *DB) loadWAL(r *wlog.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 			rec := r.Record()
 			switch dec.Type(rec) {
 			case record.Series:
-				series := seriesPool.Get()[:0]
+				series := seriesPool.Get().([]record.RefSeries)[:0]
 				series, err = dec.Series(rec, series)
 				if err != nil {
 					errCh <- &wlog.CorruptionErr{
@@ -440,7 +454,7 @@ func (db *DB) loadWAL(r *wlog.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 				}
 				decoded <- series
 			case record.Samples:
-				samples := samplesPool.Get()[:0]
+				samples := samplesPool.Get().([]record.RefSample)[:0]
 				samples, err = dec.Samples(rec, samples)
 				if err != nil {
 					errCh <- &wlog.CorruptionErr{
@@ -452,7 +466,7 @@ func (db *DB) loadWAL(r *wlog.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 				}
 				decoded <- samples
 			case record.HistogramSamples:
-				histograms := histogramsPool.Get()[:0]
+				histograms := histogramsPool.Get().([]record.RefHistogramSample)[:0]
 				histograms, err = dec.HistogramSamples(rec, histograms)
 				if err != nil {
 					errCh <- &wlog.CorruptionErr{
@@ -464,7 +478,7 @@ func (db *DB) loadWAL(r *wlog.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 				}
 				decoded <- histograms
 			case record.FloatHistogramSamples:
-				floatHistograms := floatHistogramsPool.Get()[:0]
+				floatHistograms := floatHistogramsPool.Get().([]record.RefFloatHistogramSample)[:0]
 				floatHistograms, err = dec.FloatHistogramSamples(rec, floatHistograms)
 				if err != nil {
 					errCh <- &wlog.CorruptionErr{
@@ -509,6 +523,8 @@ func (db *DB) loadWAL(r *wlog.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 					}
 				}
 			}
+
+			//nolint:staticcheck
 			seriesPool.Put(v)
 		case []record.RefSample:
 			for _, entry := range v {
@@ -523,6 +539,8 @@ func (db *DB) loadWAL(r *wlog.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 					series.lastTs = entry.T
 				}
 			}
+
+			//nolint:staticcheck
 			samplesPool.Put(v)
 		case []record.RefHistogramSample:
 			for _, entry := range v {
@@ -537,6 +555,7 @@ func (db *DB) loadWAL(r *wlog.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 					series.lastTs = entry.T
 				}
 			}
+			//nolint:staticcheck
 			histogramsPool.Put(v)
 		case []record.RefFloatHistogramSample:
 			for _, entry := range v {
@@ -551,6 +570,7 @@ func (db *DB) loadWAL(r *wlog.Reader, multiRef map[chunks.HeadSeriesRef]chunks.H
 					series.lastTs = entry.T
 				}
 			}
+			//nolint:staticcheck
 			floatHistogramsPool.Put(v)
 		default:
 			panic(fmt.Errorf("unexpected decoded type: %T", d))
diff --git a/tsdb/chunkenc/chunk.go b/tsdb/chunkenc/chunk.go
index d6a6f4614..397c09ed8 100644
--- a/tsdb/chunkenc/chunk.go
+++ b/tsdb/chunkenc/chunk.go
@@ -94,7 +94,7 @@ type Appender interface {
 	// The returned bool isRecoded can be used to distinguish between the new Chunk c being a completely new Chunk
 	// or the current Chunk recoded to a new Chunk.
 	// The Appender app that can be used for the next append is always returned.
-	AppendHistogram(prev *HistogramAppender, t int64, h *histogram.Histogram, appendOnly bool) (c Chunk, isRecoded bool, app Appender, err error)
+	AppendHistogram(prev *HistogramAppender, t int64, h *histogram.Histogram, appendOny bool) (c Chunk, isRecoded bool, app Appender, err error)
 	AppendFloatHistogram(prev *FloatHistogramAppender, t int64, h *histogram.FloatHistogram, appendOnly bool) (c Chunk, isRecoded bool, app Appender, err error)
 }
 
diff --git a/tsdb/chunks/chunks.go b/tsdb/chunks/chunks.go
index 9817fe47a..6d04998e8 100644
--- a/tsdb/chunks/chunks.go
+++ b/tsdb/chunks/chunks.go
@@ -85,21 +85,13 @@ func (p HeadChunkRef) Unpack() (HeadSeriesRef, HeadChunkID) {
 //   - less than the above, but >= memSeries.firstID, then it's
 //     memSeries.mmappedChunks[i] where i = HeadChunkID - memSeries.firstID.
 //
-// If memSeries.headChunks is non-nil it points to a *memChunk that holds the current
-// "open" (accepting appends) instance. *memChunk is a linked list and memChunk.next pointer
-// might link to the older *memChunk instance.
-// If there are multiple *memChunk instances linked to each other from memSeries.headChunks
-// they will be m-mapped as soon as possible leaving only "open" *memChunk instance.
-//
 // Example:
 // assume a memSeries.firstChunkID=7 and memSeries.mmappedChunks=[p5,p6,p7,p8,p9].
 // | HeadChunkID value | refers to ...                                                                          |
 // |-------------------|----------------------------------------------------------------------------------------|
 // |               0-6 | chunks that have been compacted to blocks, these won't return data for queries in Head |
 // |              7-11 | memSeries.mmappedChunks[i] where i is 0 to 4.                                          |
-// |                12 |                                                         *memChunk{next: nil}
-// |                13 |                                         *memChunk{next: ^}
-// |                14 | memSeries.headChunks -> *memChunk{next: ^}
+// |                12 | memSeries.headChunk                                                                    |
 type HeadChunkID uint64
 
 // BlockChunkRef refers to a chunk within a persisted block.
diff --git a/tsdb/db.go b/tsdb/db.go
index 0c69ae6e6..e181a8f20 100644
--- a/tsdb/db.go
+++ b/tsdb/db.go
@@ -973,8 +973,6 @@ func (db *DB) run() {
 			case db.compactc <- struct{}{}:
 			default:
 			}
-			// We attempt mmapping of head chunks regularly.
-			db.head.mmapHeadChunks()
 		case <-db.compactc:
 			db.metrics.compactionsTriggered.Inc()
 
@@ -2092,7 +2090,8 @@ func isBlockDir(fi fs.DirEntry) bool {
 	return err == nil
 }
 
-// isTmpDir returns true if the given file-info contains a block ULID or checkpoint prefix and a tmp extension.
+// isTmpDir returns true if the given file-info contains a block ULID, a checkpoint prefix,
+// or a chunk snapshot prefix and a tmp extension.
 func isTmpDir(fi fs.DirEntry) bool {
 	if !fi.IsDir() {
 		return false
@@ -2104,6 +2103,9 @@ func isTmpDir(fi fs.DirEntry) bool {
 		if strings.HasPrefix(fn, "checkpoint.") {
 			return true
 		}
+		if strings.HasPrefix(fn, chunkSnapshotPrefix) {
+			return true
+		}
 		if _, err := ulid.ParseStrict(fn[:len(fn)-len(ext)]); err == nil {
 			return true
 		}
diff --git a/tsdb/head.go b/tsdb/head.go
index 34a289a98..499be067a 100644
--- a/tsdb/head.go
+++ b/tsdb/head.go
@@ -206,7 +206,7 @@ type SeriesLifecycleCallback interface {
 	// PostCreation is called after creating a series to indicate a creation of series.
 	PostCreation(labels.Labels)
 	// PostDeletion is called after deletion of series.
-	PostDeletion(map[chunks.HeadSeriesRef]labels.Labels)
+	PostDeletion(...labels.Labels)
 }
 
 // NewHead opens the head block in dir.
@@ -344,7 +344,6 @@ type headMetrics struct {
 	mmapChunkCorruptionTotal  prometheus.Counter
 	snapshotReplayErrorTotal  prometheus.Counter // Will be either 0 or 1.
 	oooHistogram              prometheus.Histogram
-	mmapChunksTotal           prometheus.Counter
 }
 
 const (
@@ -469,10 +468,6 @@ func newHeadMetrics(h *Head, r prometheus.Registerer) *headMetrics {
 				60 * 60 * 12, // 12h
 			},
 		}),
-		mmapChunksTotal: prometheus.NewCounter(prometheus.CounterOpts{
-			Name: "prometheus_tsdb_mmap_chunks_total",
-			Help: "Total number of chunks that were memory-mapped.",
-		}),
 	}
 
 	if r != nil {
@@ -500,7 +495,6 @@ func newHeadMetrics(h *Head, r prometheus.Registerer) *headMetrics {
 			m.checkpointDeleteTotal,
 			m.checkpointCreationFail,
 			m.checkpointCreationTotal,
-			m.mmapChunksTotal,
 			m.mmapChunkCorruptionTotal,
 			m.snapshotReplayErrorTotal,
 			// Metrics bound to functions and not needed in tests
@@ -886,11 +880,11 @@ func (h *Head) loadMmappedChunks(refSeries map[chunks.HeadSeriesRef]*memSeries)
 			numSamples: numSamples,
 		})
 		h.updateMinMaxTime(mint, maxt)
-		if ms.headChunks != nil && maxt >= ms.headChunks.minTime {
+		if ms.headChunk != nil && maxt >= ms.headChunk.minTime {
 			// The head chunk was completed and was m-mapped after taking the snapshot.
 			// Hence remove this chunk.
 			ms.nextAt = 0
-			ms.headChunks = nil
+			ms.headChunk = nil
 			ms.app = nil
 		}
 		return nil
@@ -1580,10 +1574,6 @@ func (h *Head) Close() error {
 	defer h.closedMtx.Unlock()
 	h.closed = true
 
-	// mmap all but last chunk in case we're performing snapshot since that only
-	// takes samples from most recent head chunk.
-	h.mmapHeadChunks()
-
 	errs := tsdb_errors.NewMulti(h.chunkDiskMapper.Close())
 	if errs.Err() == nil && h.opts.EnableMemorySnapshotOnShutdown {
 		errs.Add(h.performChunkSnapshot())
@@ -1640,37 +1630,6 @@ func (h *Head) getOrCreateWithID(id chunks.HeadSeriesRef, hash uint64, lset labe
 	return s, true, nil
 }
 
-// mmapHeadChunks will iterate all memSeries stored on Head and call mmapHeadChunks() on each of them.
-//
-// There are two types of chunks that store samples for each memSeries:
-// A) Head chunk - stored on Go heap, when new samples are appended they go there.
-// B) M-mapped chunks - memory mapped chunks, kernel manages the memory for us on-demand, these chunks
-//
-//	are read-only.
-//
-// Calling mmapHeadChunks() will iterate all memSeries and m-mmap all chunks that should be m-mapped.
-// The m-mapping operation is needs to be serialised and so it goes via central lock.
-// If there are multiple concurrent memSeries that need to m-map some chunk then they can block each-other.
-//
-// To minimise the effect of locking on TSDB operations m-mapping is serialised and done away from
-// sample append path, since waiting on a lock inside an append would lock the entire memSeries for
-// (potentially) a long time, since that could eventually delay next scrape and/or cause query timeouts.
-func (h *Head) mmapHeadChunks() {
-	var count int
-	for i := 0; i < h.series.size; i++ {
-		h.series.locks[i].RLock()
-		for _, all := range h.series.hashes[i] {
-			for _, series := range all {
-				series.Lock()
-				count += series.mmapChunks(h.chunkDiskMapper)
-				series.Unlock()
-			}
-		}
-		h.series.locks[i].RUnlock()
-	}
-	h.metrics.mmapChunksTotal.Add(float64(count))
-}
-
 // seriesHashmap is a simple hashmap for memSeries by their label set. It is built
 // on top of a regular hashmap and holds a slice of series to resolve hash collisions.
 // Its methods require the hash to be submitted with it to avoid re-computations throughout
@@ -1762,17 +1721,16 @@ func newStripeSeries(stripeSize int, seriesCallback SeriesLifecycleCallback) *st
 // minMmapFile is the min mmap file number seen in the series (in-order and out-of-order) after gc'ing the series.
 func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) (_ map[storage.SeriesRef]struct{}, _ int, _, _ int64, minMmapFile int) {
 	var (
-		deleted                     = map[storage.SeriesRef]struct{}{}
-		rmChunks                    = 0
-		actualMint            int64 = math.MaxInt64
-		minOOOTime            int64 = math.MaxInt64
-		deletedFromPrevStripe       = 0
+		deleted                  = map[storage.SeriesRef]struct{}{}
+		deletedForCallback       = []labels.Labels{}
+		rmChunks                 = 0
+		actualMint         int64 = math.MaxInt64
+		minOOOTime         int64 = math.MaxInt64
 	)
 	minMmapFile = math.MaxInt32
 	// Run through all series and truncate old chunks. Mark those with no
 	// chunks left as deleted and store their ID.
 	for i := 0; i < s.size; i++ {
-		deletedForCallback := make(map[chunks.HeadSeriesRef]labels.Labels, deletedFromPrevStripe)
 		s.locks[i].Lock()
 
 		for hash, all := range s.hashes[i] {
@@ -1802,7 +1760,7 @@ func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) (
 						minOOOTime = series.ooo.oooHeadChunk.minTime
 					}
 				}
-				if len(series.mmappedChunks) > 0 || series.headChunks != nil || series.pendingCommit ||
+				if len(series.mmappedChunks) > 0 || series.headChunk != nil || series.pendingCommit ||
 					(series.ooo != nil && (len(series.ooo.oooMmappedChunks) > 0 || series.ooo.oooHeadChunk != nil)) {
 					seriesMint := series.minTime()
 					if seriesMint < actualMint {
@@ -1826,7 +1784,7 @@ func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) (
 				deleted[storage.SeriesRef(series.ref)] = struct{}{}
 				s.hashes[i].del(hash, series.lset)
 				delete(s.series[j], series.ref)
-				deletedForCallback[series.ref] = series.lset
+				deletedForCallback = append(deletedForCallback, series.lset)
 
 				if i != j {
 					s.locks[j].Unlock()
@@ -1838,8 +1796,8 @@ func (s *stripeSeries) gc(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) (
 
 		s.locks[i].Unlock()
 
-		s.seriesLifecycleCallback.PostDeletion(deletedForCallback)
-		deletedFromPrevStripe = len(deletedForCallback)
+		s.seriesLifecycleCallback.PostDeletion(deletedForCallback...)
+		deletedForCallback = deletedForCallback[:0]
 	}
 
 	if actualMint == math.MaxInt64 {
@@ -1957,11 +1915,8 @@ type memSeries struct {
 	//
 	// pN is the pointer to the mmappedChunk referered to by HeadChunkID=N
 	mmappedChunks []*mmappedChunk
-	// Most recent chunks in memory that are still being built or waiting to be mmapped.
-	// This is a linked list, headChunks points to the most recent chunk, headChunks.next points
-	// to older chunk and so on.
-	headChunks   *memChunk
-	firstChunkID chunks.HeadChunkID // HeadChunkID for mmappedChunks[0]
+	headChunk     *memChunk          // Most recent chunk in memory that's still being built.
+	firstChunkID  chunks.HeadChunkID // HeadChunkID for mmappedChunks[0]
 
 	ooo *memSeriesOOOFields
 
@@ -1977,7 +1932,7 @@ type memSeries struct {
 	lastFloatHistogramValue *histogram.FloatHistogram
 
 	// Current appender for the head chunk. Set when a new head chunk is cut.
-	// It is nil only if headChunks is nil. E.g. if there was an appender that created a new series, but rolled back the commit
+	// It is nil only if headChunk is nil. E.g. if there was an appender that created a new series, but rolled back the commit
 	// (the first sample would create a headChunk, hence appender, but rollback skipped it while the Append() call would create a series).
 	app chunkenc.Appender
 
@@ -2011,16 +1966,17 @@ func (s *memSeries) minTime() int64 {
 	if len(s.mmappedChunks) > 0 {
 		return s.mmappedChunks[0].minTime
 	}
-	if s.headChunks != nil {
-		return s.headChunks.oldest().minTime
+	if s.headChunk != nil {
+		return s.headChunk.minTime
 	}
 	return math.MinInt64
 }
 
 func (s *memSeries) maxTime() int64 {
 	// The highest timestamps will always be in the regular (non-OOO) chunks, even if OOO is enabled.
-	if s.headChunks != nil {
-		return s.headChunks.maxTime
+	c := s.head()
+	if c != nil {
+		return c.maxTime
 	}
 	if len(s.mmappedChunks) > 0 {
 		return s.mmappedChunks[len(s.mmappedChunks)-1].maxTime
@@ -2033,29 +1989,12 @@ func (s *memSeries) maxTime() int64 {
 // Chunk IDs remain unchanged.
 func (s *memSeries) truncateChunksBefore(mint int64, minOOOMmapRef chunks.ChunkDiskMapperRef) int {
 	var removedInOrder int
-	if s.headChunks != nil {
-		var i int
-		var nextChk *memChunk
-		chk := s.headChunks
-		for chk != nil {
-			if chk.maxTime < mint {
-				// If any head chunk is truncated, we can truncate all mmapped chunks.
-				removedInOrder = chk.len() + len(s.mmappedChunks)
-				s.firstChunkID += chunks.HeadChunkID(removedInOrder)
-				if i == 0 {
-					// This is the first chunk on the list so we need to remove the entire list.
-					s.headChunks = nil
-				} else {
-					// This is NOT the first chunk, unlink it from parent.
-					nextChk.prev = nil
-				}
-				s.mmappedChunks = nil
-				break
-			}
-			nextChk = chk
-			chk = chk.prev
-			i++
-		}
+	if s.headChunk != nil && s.headChunk.maxTime < mint {
+		// If head chunk is truncated, we can truncate all mmapped chunks.
+		removedInOrder = 1 + len(s.mmappedChunks)
+		s.firstChunkID += chunks.HeadChunkID(removedInOrder)
+		s.headChunk = nil
+		s.mmappedChunks = nil
 	}
 	if len(s.mmappedChunks) > 0 {
 		for i, c := range s.mmappedChunks {
@@ -2095,52 +2034,13 @@ func (s *memSeries) cleanupAppendIDsBelow(bound uint64) {
 	}
 }
 
+func (s *memSeries) head() *memChunk {
+	return s.headChunk
+}
+
 type memChunk struct {
 	chunk            chunkenc.Chunk
 	minTime, maxTime int64
-	prev             *memChunk // Link to the previous element on the list.
-}
-
-// len returns the length of memChunk list, including the element it was called on.
-func (mc *memChunk) len() (count int) {
-	elem := mc
-	for elem != nil {
-		count++
-		elem = elem.prev
-	}
-	return count
-}
-
-// oldest returns the oldest element on the list.
-// For single element list this will be the same memChunk oldest() was called on.
-func (mc *memChunk) oldest() (elem *memChunk) {
-	elem = mc
-	for elem.prev != nil {
-		elem = elem.prev
-	}
-	return elem
-}
-
-// atOffset returns a memChunk that's Nth element on the linked list.
-func (mc *memChunk) atOffset(offset int) (elem *memChunk) {
-	if offset == 0 {
-		return mc
-	}
-	if offset < 0 {
-		return nil
-	}
-
-	var i int
-	elem = mc
-	for i < offset {
-		i++
-		elem = elem.prev
-		if elem == nil {
-			break
-		}
-	}
-
-	return elem
 }
 
 type oooHeadChunk struct {
@@ -2162,7 +2062,7 @@ func overlapsClosedInterval(mint1, maxt1, mint2, maxt2 int64) bool {
 	return mint1 <= maxt2 && mint2 <= maxt1
 }
 
-// mmappedChunk describes a head chunk on disk that has been mmapped
+// mappedChunks describes a head chunk on disk that has been mmapped
 type mmappedChunk struct {
 	ref              chunks.ChunkDiskMapperRef
 	numSamples       uint16
@@ -2176,9 +2076,9 @@ func (mc *mmappedChunk) OverlapsClosedInterval(mint, maxt int64) bool {
 
 type noopSeriesLifecycleCallback struct{}
 
-func (noopSeriesLifecycleCallback) PreCreation(labels.Labels) error                     { return nil }
-func (noopSeriesLifecycleCallback) PostCreation(labels.Labels)                          {}
-func (noopSeriesLifecycleCallback) PostDeletion(map[chunks.HeadSeriesRef]labels.Labels) {}
+func (noopSeriesLifecycleCallback) PreCreation(labels.Labels) error { return nil }
+func (noopSeriesLifecycleCallback) PostCreation(labels.Labels)      {}
+func (noopSeriesLifecycleCallback) PostDeletion(...labels.Labels)   {}
 
 func (h *Head) Size() int64 {
 	var walSize, wblSize int64
diff --git a/tsdb/head_append.go b/tsdb/head_append.go
index f06aacba4..cbbb60f03 100644
--- a/tsdb/head_append.go
+++ b/tsdb/head_append.go
@@ -395,7 +395,7 @@ func (a *headAppender) Append(ref storage.SeriesRef, lset labels.Labels, t int64
 func (s *memSeries) appendable(t int64, v float64, headMaxt, minValidTime, oooTimeWindow int64) (isOOO bool, oooDelta int64, err error) {
 	// Check if we can append in the in-order chunk.
 	if t >= minValidTime {
-		if s.headChunks == nil {
+		if s.head() == nil {
 			// The series has no sample and was freshly created.
 			return false, 0, nil
 		}
@@ -433,14 +433,15 @@ func (s *memSeries) appendable(t int64, v float64, headMaxt, minValidTime, oooTi
 
 // appendableHistogram checks whether the given histogram is valid for appending to the series.
 func (s *memSeries) appendableHistogram(t int64, h *histogram.Histogram) error {
-	if s.headChunks == nil {
+	c := s.head()
+	if c == nil {
 		return nil
 	}
 
-	if t > s.headChunks.maxTime {
+	if t > c.maxTime {
 		return nil
 	}
-	if t < s.headChunks.maxTime {
+	if t < c.maxTime {
 		return storage.ErrOutOfOrderSample
 	}
 
@@ -454,14 +455,15 @@ func (s *memSeries) appendableHistogram(t int64, h *histogram.Histogram) error {
 
 // appendableFloatHistogram checks whether the given float histogram is valid for appending to the series.
 func (s *memSeries) appendableFloatHistogram(t int64, fh *histogram.FloatHistogram) error {
-	if s.headChunks == nil {
+	c := s.head()
+	if c == nil {
 		return nil
 	}
 
-	if t > s.headChunks.maxTime {
+	if t > c.maxTime {
 		return nil
 	}
-	if t < s.headChunks.maxTime {
+	if t < c.maxTime {
 		return storage.ErrOutOfOrderSample
 	}
 
@@ -930,8 +932,6 @@ func (a *headAppender) Commit() (err error) {
 
 		oooSample, _, err := series.appendable(s.T, s.V, a.headMaxt, a.minValidTime, a.oooTimeWindow)
 		switch err {
-		case nil:
-			// Do nothing.
 		case storage.ErrOutOfOrderSample:
 			samplesAppended--
 			oooRejected++
@@ -941,6 +941,8 @@ func (a *headAppender) Commit() (err error) {
 		case storage.ErrTooOldSample:
 			samplesAppended--
 			tooOldRejected++
+		case nil:
+			// Do nothing.
 		default:
 			samplesAppended--
 		}
@@ -1198,11 +1200,12 @@ func (s *memSeries) appendHistogram(t int64, h *histogram.Histogram, appendID ui
 		return true, false
 	}
 
-	s.headChunks = &memChunk{
+	// This is a brand new chunk, switch out the head chunk (based on cutNewHeadChunk).
+	s.mmapCurrentHeadChunk(o.chunkDiskMapper)
+	s.headChunk = &memChunk{
 		chunk:   newChunk,
 		minTime: t,
 		maxTime: t,
-		prev:    s.headChunks,
 	}
 	s.nextAt = rangeForTimestamp(t, o.chunkRange)
 	return true, true
@@ -1255,11 +1258,12 @@ func (s *memSeries) appendFloatHistogram(t int64, fh *histogram.FloatHistogram,
 		return true, false
 	}
 
-	s.headChunks = &memChunk{
+	// This is a brand new chunk, switch out the head chunk (based on cutNewHeadChunk).
+	s.mmapCurrentHeadChunk(o.chunkDiskMapper)
+	s.headChunk = &memChunk{
 		chunk:   newChunk,
 		minTime: t,
 		maxTime: t,
-		prev:    s.headChunks,
 	}
 	s.nextAt = rangeForTimestamp(t, o.chunkRange)
 	return true, true
@@ -1269,7 +1273,7 @@ func (s *memSeries) appendFloatHistogram(t int64, fh *histogram.FloatHistogram,
 // It is unsafe to call this concurrently with s.iterator(...) without holding the series lock.
 // This should be called only when appending data.
 func (s *memSeries) appendPreprocessor(t int64, e chunkenc.Encoding, o chunkOpts) (c *memChunk, sampleInOrder, chunkCreated bool) {
-	c = s.headChunks
+	c = s.head()
 
 	if c == nil {
 		if len(s.mmappedChunks) > 0 && s.mmappedChunks[len(s.mmappedChunks)-1].maxTime >= t {
@@ -1277,7 +1281,7 @@ func (s *memSeries) appendPreprocessor(t int64, e chunkenc.Encoding, o chunkOpts
 			return c, false, false
 		}
 		// There is no head chunk in this series yet, create the first chunk for the sample.
-		c = s.cutNewHeadChunk(t, e, o.chunkRange)
+		c = s.cutNewHeadChunk(t, e, o.chunkDiskMapper, o.chunkRange)
 		chunkCreated = true
 	}
 
@@ -1289,9 +1293,8 @@ func (s *memSeries) appendPreprocessor(t int64, e chunkenc.Encoding, o chunkOpts
 	if c.chunk.Encoding() != e {
 		// The chunk encoding expected by this append is different than the head chunk's
 		// encoding. So we cut a new chunk with the expected encoding.
-		c = s.cutNewHeadChunk(t, e, o.chunkRange)
+		c = s.cutNewHeadChunk(t, e, o.chunkDiskMapper, o.chunkRange)
 		chunkCreated = true
-
 	}
 
 	numSamples := c.chunk.NumSamples()
@@ -1315,7 +1318,7 @@ func (s *memSeries) appendPreprocessor(t int64, e chunkenc.Encoding, o chunkOpts
 	// as we expect more chunks to come.
 	// Note that next chunk will have its nextAt recalculated for the new rate.
 	if t >= s.nextAt || numSamples >= o.samplesPerChunk*2 {
-		c = s.cutNewHeadChunk(t, e, o.chunkRange)
+		c = s.cutNewHeadChunk(t, e, o.chunkDiskMapper, o.chunkRange)
 		chunkCreated = true
 	}
 
@@ -1335,37 +1338,36 @@ func computeChunkEndTime(start, cur, max int64) int64 {
 	return start + (max-start)/n
 }
 
-func (s *memSeries) cutNewHeadChunk(mint int64, e chunkenc.Encoding, chunkRange int64) *memChunk {
-	// When cutting a new head chunk we create a new memChunk instance with .prev
-	// pointing at the current .headChunks, so it forms a linked list.
-	// All but first headChunks list elements will be m-mapped as soon as possible
-	// so this is a single element list most of the time.
-	s.headChunks = &memChunk{
+func (s *memSeries) cutNewHeadChunk(
+	mint int64, e chunkenc.Encoding, chunkDiskMapper *chunks.ChunkDiskMapper, chunkRange int64,
+) *memChunk {
+	s.mmapCurrentHeadChunk(chunkDiskMapper)
+
+	s.headChunk = &memChunk{
 		minTime: mint,
 		maxTime: math.MinInt64,
-		prev:    s.headChunks,
 	}
 
 	if chunkenc.IsValidEncoding(e) {
 		var err error
-		s.headChunks.chunk, err = chunkenc.NewEmptyChunk(e)
+		s.headChunk.chunk, err = chunkenc.NewEmptyChunk(e)
 		if err != nil {
 			panic(err) // This should never happen.
 		}
 	} else {
-		s.headChunks.chunk = chunkenc.NewXORChunk()
+		s.headChunk.chunk = chunkenc.NewXORChunk()
 	}
 
 	// Set upper bound on when the next chunk must be started. An earlier timestamp
 	// may be chosen dynamically at a later point.
 	s.nextAt = rangeForTimestamp(mint, chunkRange)
 
-	app, err := s.headChunks.chunk.Appender()
+	app, err := s.headChunk.chunk.Appender()
 	if err != nil {
 		panic(err)
 	}
 	s.app = app
-	return s.headChunks
+	return s.headChunk
 }
 
 // cutNewOOOHeadChunk cuts a new OOO chunk and m-maps the old chunk.
@@ -1399,32 +1401,19 @@ func (s *memSeries) mmapCurrentOOOHeadChunk(chunkDiskMapper *chunks.ChunkDiskMap
 	return chunkRef
 }
 
-// mmapChunks will m-map all but first chunk on s.headChunks list.
-func (s *memSeries) mmapChunks(chunkDiskMapper *chunks.ChunkDiskMapper) (count int) {
-	if s.headChunks == nil || s.headChunks.prev == nil {
-		// There is none or only one head chunk, so nothing to m-map here.
+func (s *memSeries) mmapCurrentHeadChunk(chunkDiskMapper *chunks.ChunkDiskMapper) {
+	if s.headChunk == nil || s.headChunk.chunk.NumSamples() == 0 {
+		// There is no head chunk, so nothing to m-map here.
 		return
 	}
 
-	// Write chunks starting from the oldest one and stop before we get to current s.headChunk.
-	// If we have this chain: s.headChunk{t4} -> t3 -> t2 -> t1 -> t0
-	// then we need to write chunks t0 to t3, but skip s.headChunks.
-	for i := s.headChunks.len() - 1; i > 0; i-- {
-		chk := s.headChunks.atOffset(i)
-		chunkRef := chunkDiskMapper.WriteChunk(s.ref, chk.minTime, chk.maxTime, chk.chunk, false, handleChunkWriteError)
-		s.mmappedChunks = append(s.mmappedChunks, &mmappedChunk{
-			ref:        chunkRef,
-			numSamples: uint16(chk.chunk.NumSamples()),
-			minTime:    chk.minTime,
-			maxTime:    chk.maxTime,
-		})
-		count++
-	}
-
-	// Once we've written out all chunks except s.headChunks we need to unlink these from s.headChunk.
-	s.headChunks.prev = nil
-
-	return count
+	chunkRef := chunkDiskMapper.WriteChunk(s.ref, s.headChunk.minTime, s.headChunk.maxTime, s.headChunk.chunk, false, handleChunkWriteError)
+	s.mmappedChunks = append(s.mmappedChunks, &mmappedChunk{
+		ref:        chunkRef,
+		numSamples: uint16(s.headChunk.chunk.NumSamples()),
+		minTime:    s.headChunk.minTime,
+		maxTime:    s.headChunk.maxTime,
+	})
 }
 
 func handleChunkWriteError(err error) {
diff --git a/tsdb/head_read.go b/tsdb/head_read.go
index f27d4ef76..b2af74ace 100644
--- a/tsdb/head_read.go
+++ b/tsdb/head_read.go
@@ -174,27 +174,12 @@ func (h *headIndexReader) Series(ref storage.SeriesRef, builder *labels.ScratchB
 			Ref:     chunks.ChunkRef(chunks.NewHeadChunkRef(s.ref, s.headChunkID(i))),
 		})
 	}
-
-	if s.headChunks != nil {
-		var maxTime int64
-		var i, j int
-		for i = s.headChunks.len() - 1; i >= 0; i-- {
-			chk := s.headChunks.atOffset(i)
-			if i == 0 {
-				// Set the head chunk as open (being appended to) for the first headChunk.
-				maxTime = math.MaxInt64
-			} else {
-				maxTime = chk.maxTime
-			}
-			if chk.OverlapsClosedInterval(h.mint, h.maxt) {
-				*chks = append(*chks, chunks.Meta{
-					MinTime: chk.minTime,
-					MaxTime: maxTime,
-					Ref:     chunks.ChunkRef(chunks.NewHeadChunkRef(s.ref, s.headChunkID(len(s.mmappedChunks)+j))),
-				})
-			}
-			j++
-		}
+	if s.headChunk != nil && s.headChunk.OverlapsClosedInterval(h.mint, h.maxt) {
+		*chks = append(*chks, chunks.Meta{
+			MinTime: s.headChunk.minTime,
+			MaxTime: math.MaxInt64, // Set the head chunks as open (being appended to).
+			Ref:     chunks.ChunkRef(chunks.NewHeadChunkRef(s.ref, s.headChunkID(len(s.mmappedChunks)))),
+		})
 	}
 
 	return nil
@@ -202,7 +187,7 @@ func (h *headIndexReader) Series(ref storage.SeriesRef, builder *labels.ScratchB
 
 // headChunkID returns the HeadChunkID referred to by the given position.
 // * 0 <= pos < len(s.mmappedChunks) refer to s.mmappedChunks[pos]
-// * pos >= len(s.mmappedChunks) refers to s.headChunks linked list
+// * pos == len(s.mmappedChunks) refers to s.headChunk
 func (s *memSeries) headChunkID(pos int) chunks.HeadChunkID {
 	return chunks.HeadChunkID(pos) + s.firstChunkID
 }
@@ -311,7 +296,7 @@ func (h *headChunkReader) chunk(meta chunks.Meta, copyLastChunk bool) (chunkenc.
 	}
 
 	s.Lock()
-	c, headChunk, isOpen, err := s.chunk(cid, h.head.chunkDiskMapper, &h.head.memChunkPool)
+	c, headChunk, err := s.chunk(cid, h.head.chunkDiskMapper, &h.head.memChunkPool)
 	if err != nil {
 		s.Unlock()
 		return nil, 0, err
@@ -320,7 +305,6 @@ func (h *headChunkReader) chunk(meta chunks.Meta, copyLastChunk bool) (chunkenc.
 		if !headChunk {
 			// Set this to nil so that Go GC can collect it after it has been used.
 			c.chunk = nil
-			c.prev = nil
 			h.head.memChunkPool.Put(c)
 		}
 	}()
@@ -332,14 +316,14 @@ func (h *headChunkReader) chunk(meta chunks.Meta, copyLastChunk bool) (chunkenc.
 	}
 
 	chk, maxTime := c.chunk, c.maxTime
-	if headChunk && isOpen && copyLastChunk {
+	if headChunk && copyLastChunk {
 		// The caller may ask to copy the head chunk in order to take the
 		// bytes of the chunk without causing the race between read and append.
-		b := s.headChunks.chunk.Bytes()
+		b := s.headChunk.chunk.Bytes()
 		newB := make([]byte, len(b))
 		copy(newB, b) // TODO(codesome): Use bytes.Clone() when we upgrade to Go 1.20.
 		// TODO(codesome): Put back in the pool (non-trivial).
-		chk, err = h.head.opts.ChunkPool.Get(s.headChunks.chunk.Encoding(), newB)
+		chk, err = h.head.opts.ChunkPool.Get(s.headChunk.chunk.Encoding(), newB)
 		if err != nil {
 			return nil, 0, err
 		}
@@ -357,60 +341,34 @@ func (h *headChunkReader) chunk(meta chunks.Meta, copyLastChunk bool) (chunkenc.
 // chunk returns the chunk for the HeadChunkID from memory or by m-mapping it from the disk.
 // If headChunk is false, it means that the returned *memChunk
 // (and not the chunkenc.Chunk inside it) can be garbage collected after its usage.
-// if isOpen is true, it means that the returned *memChunk is used for appends.
-func (s *memSeries) chunk(id chunks.HeadChunkID, chunkDiskMapper *chunks.ChunkDiskMapper, memChunkPool *sync.Pool) (chunk *memChunk, headChunk, isOpen bool, err error) {
+func (s *memSeries) chunk(id chunks.HeadChunkID, chunkDiskMapper *chunks.ChunkDiskMapper, memChunkPool *sync.Pool) (chunk *memChunk, headChunk bool, err error) {
 	// ix represents the index of chunk in the s.mmappedChunks slice. The chunk id's are
 	// incremented by 1 when new chunk is created, hence (id - firstChunkID) gives the slice index.
 	// The max index for the s.mmappedChunks slice can be len(s.mmappedChunks)-1, hence if the ix
-	// is >= len(s.mmappedChunks), it represents one of the chunks on s.headChunks linked list.
-	// The order of elemens is different for slice and linked list.
-	// For s.mmappedChunks slice newer chunks are appended to it.
-	// For s.headChunks list newer chunks are prepended to it.
-	//
-	// memSeries {
-	//   mmappedChunks: [t0, t1, t2]
-	//   headChunk:     {t5}->{t4}->{t3}
-	// }
+	// is len(s.mmappedChunks), it represents the next chunk, which is the head chunk.
 	ix := int(id) - int(s.firstChunkID)
-
-	var headChunksLen int
-	if s.headChunks != nil {
-		headChunksLen = s.headChunks.len()
+	if ix < 0 || ix > len(s.mmappedChunks) {
+		return nil, false, storage.ErrNotFound
 	}
 
-	if ix < 0 || ix > len(s.mmappedChunks)+headChunksLen-1 {
-		return nil, false, false, storage.ErrNotFound
-	}
-
-	if ix < len(s.mmappedChunks) {
-		chk, err := chunkDiskMapper.Chunk(s.mmappedChunks[ix].ref)
-		if err != nil {
-			if _, ok := err.(*chunks.CorruptionErr); ok {
-				panic(err)
-			}
-			return nil, false, false, err
+	if ix == len(s.mmappedChunks) {
+		if s.headChunk == nil {
+			return nil, false, errors.New("invalid head chunk")
 		}
-		mc := memChunkPool.Get().(*memChunk)
-		mc.chunk = chk
-		mc.minTime = s.mmappedChunks[ix].minTime
-		mc.maxTime = s.mmappedChunks[ix].maxTime
-		return mc, false, false, nil
+		return s.headChunk, true, nil
 	}
-
-	ix -= len(s.mmappedChunks)
-
-	offset := headChunksLen - ix - 1
-	// headChunks is a linked list where first element is the most recent one and the last one is the oldest.
-	// This order is reversed when compared with mmappedChunks, since mmappedChunks[0] is the oldest chunk,
-	// while headChunk.atOffset(0) would give us the most recent chunk.
-	// So when calling headChunk.atOffset() we need to reverse the value of ix.
-	elem := s.headChunks.atOffset(offset)
-	if elem == nil {
-		// This should never really happen and would mean that headChunksLen value is NOT equal
-		// to the length of the headChunks list.
-		return nil, false, false, storage.ErrNotFound
+	chk, err := chunkDiskMapper.Chunk(s.mmappedChunks[ix].ref)
+	if err != nil {
+		if _, ok := err.(*chunks.CorruptionErr); ok {
+			panic(err)
+		}
+		return nil, false, err
 	}
-	return elem, true, offset == 0, nil
+	mc := memChunkPool.Get().(*memChunk)
+	mc.chunk = chk
+	mc.minTime = s.mmappedChunks[ix].minTime
+	mc.maxTime = s.mmappedChunks[ix].maxTime
+	return mc, false, nil
 }
 
 // oooMergedChunk returns the requested chunk based on the given chunks.Meta
@@ -702,21 +660,8 @@ func (s *memSeries) iterator(id chunks.HeadChunkID, c chunkenc.Chunk, isoState *
 			}
 		}
 
-		ix -= len(s.mmappedChunks)
-		if s.headChunks != nil {
-			// Iterate all head chunks from the oldest to the newest.
-			headChunksLen := s.headChunks.len()
-			for j := headChunksLen - 1; j >= 0; j-- {
-				chk := s.headChunks.atOffset(j)
-				chkSamples := chk.chunk.NumSamples()
-				totalSamples += chkSamples
-				// Chunk ID is len(s.mmappedChunks) + $(headChunks list position).
-				// Where $(headChunks list position) is zero for the oldest chunk and $(s.headChunks.len() - 1)
-				// for the newest (open) chunk.
-				if headChunksLen-1-j < ix {
-					previousSamples += chkSamples
-				}
-			}
+		if s.headChunk != nil {
+			totalSamples += s.headChunk.chunk.NumSamples()
 		}
 
 		// Removing the extra transactionIDs that are relevant for samples that
diff --git a/tsdb/head_wal.go b/tsdb/head_wal.go
index 3ed95887e..2397a9ec9 100644
--- a/tsdb/head_wal.go
+++ b/tsdb/head_wal.go
@@ -503,7 +503,7 @@ func (h *Head) resetSeriesWithMMappedChunks(mSeries *memSeries, mmc, oooMmc []*m
 
 	// Any samples replayed till now would already be compacted. Resetting the head chunk.
 	mSeries.nextAt = 0
-	mSeries.headChunks = nil
+	mSeries.headChunk = nil
 	mSeries.app = nil
 	return
 }
@@ -595,7 +595,6 @@ func (wp *walSubsetProcessor) processWALSamples(h *Head, mmappedChunks, oooMmapp
 			if _, chunkCreated := ms.append(s.T, s.V, 0, appendChunkOpts); chunkCreated {
 				h.metrics.chunksCreated.Inc()
 				h.metrics.chunks.Inc()
-				_ = ms.mmapChunks(h.chunkDiskMapper)
 			}
 			if s.T > maxt {
 				maxt = s.T
@@ -961,15 +960,15 @@ func (s *memSeries) encodeToSnapshotRecord(b []byte) []byte {
 	buf.PutBE64int64(0) // Backwards-compatibility; was chunkRange but now unused.
 
 	s.Lock()
-	if s.headChunks == nil {
+	if s.headChunk == nil {
 		buf.PutUvarint(0)
 	} else {
-		enc := s.headChunks.chunk.Encoding()
+		enc := s.headChunk.chunk.Encoding()
 		buf.PutUvarint(1)
-		buf.PutBE64int64(s.headChunks.minTime)
-		buf.PutBE64int64(s.headChunks.maxTime)
+		buf.PutBE64int64(s.headChunk.minTime)
+		buf.PutBE64int64(s.headChunk.maxTime)
 		buf.PutByte(byte(enc))
-		buf.PutUvarintBytes(s.headChunks.chunk.Bytes())
+		buf.PutUvarintBytes(s.headChunk.chunk.Bytes())
 
 		switch enc {
 		case chunkenc.EncXOR:
@@ -1415,12 +1414,12 @@ func (h *Head) loadChunkSnapshot() (int, int, map[chunks.HeadSeriesRef]*memSerie
 					continue
 				}
 				series.nextAt = csr.mc.maxTime // This will create a new chunk on append.
-				series.headChunks = csr.mc
+				series.headChunk = csr.mc
 				series.lastValue = csr.lastValue
 				series.lastHistogramValue = csr.lastHistogramValue
 				series.lastFloatHistogramValue = csr.lastFloatHistogramValue
 
-				app, err := series.headChunks.chunk.Appender()
+				app, err := series.headChunk.chunk.Appender()
 				if err != nil {
 					errChan <- err
 					return
diff --git a/web/api/v1/json_codec.go b/web/api/v1/json_codec.go
index f1a8104cc..62e7563b1 100644
--- a/web/api/v1/json_codec.go
+++ b/web/api/v1/json_codec.go
@@ -19,7 +19,6 @@ import (
 	jsoniter "github.com/json-iterator/go"
 
 	"github.com/prometheus/prometheus/model/exemplar"
-	"github.com/prometheus/prometheus/model/labels"
 	"github.com/prometheus/prometheus/promql"
 	"github.com/prometheus/prometheus/util/jsonutil"
 )
@@ -30,7 +29,6 @@ func init() {
 	jsoniter.RegisterTypeEncoderFunc("promql.FPoint", marshalFPointJSON, marshalPointJSONIsEmpty)
 	jsoniter.RegisterTypeEncoderFunc("promql.HPoint", marshalHPointJSON, marshalPointJSONIsEmpty)
 	jsoniter.RegisterTypeEncoderFunc("exemplar.Exemplar", marshalExemplarJSON, marshalExemplarJSONEmpty)
-	jsoniter.RegisterTypeEncoderFunc("labels.Labels", unsafeMarshalLabelsJSON, labelsIsEmpty)
 }
 
 // JSONCodec is a Codec that encodes API responses as JSON.
@@ -70,7 +68,12 @@ func marshalSeriesJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
 	s := *((*promql.Series)(ptr))
 	stream.WriteObjectStart()
 	stream.WriteObjectField(`metric`)
-	marshalLabelsJSON(s.Metric, stream)
+	m, err := s.Metric.MarshalJSON()
+	if err != nil {
+		stream.Error = err
+		return
+	}
+	stream.SetBuffer(append(stream.Buffer(), m...))
 
 	for i, p := range s.Floats {
 		stream.WriteMore()
@@ -126,7 +129,12 @@ func marshalSampleJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
 	s := *((*promql.Sample)(ptr))
 	stream.WriteObjectStart()
 	stream.WriteObjectField(`metric`)
-	marshalLabelsJSON(s.Metric, stream)
+	m, err := s.Metric.MarshalJSON()
+	if err != nil {
+		stream.Error = err
+		return
+	}
+	stream.SetBuffer(append(stream.Buffer(), m...))
 	stream.WriteMore()
 	if s.H == nil {
 		stream.WriteObjectField(`value`)
@@ -186,7 +194,12 @@ func marshalExemplarJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
 
 	// "labels" key.
 	stream.WriteObjectField(`labels`)
-	marshalLabelsJSON(p.Labels, stream)
+	lbls, err := p.Labels.MarshalJSON()
+	if err != nil {
+		stream.Error = err
+		return
+	}
+	stream.SetBuffer(append(stream.Buffer(), lbls...))
 
 	// "value" key.
 	stream.WriteMore()
@@ -204,28 +217,3 @@ func marshalExemplarJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
 func marshalExemplarJSONEmpty(unsafe.Pointer) bool {
 	return false
 }
-
-func unsafeMarshalLabelsJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
-	labelsPtr := (*labels.Labels)(ptr)
-	marshalLabelsJSON(*labelsPtr, stream)
-}
-
-func marshalLabelsJSON(lbls labels.Labels, stream *jsoniter.Stream) {
-	stream.WriteObjectStart()
-	i := 0
-	lbls.Range(func(v labels.Label) {
-		if i != 0 {
-			stream.WriteMore()
-		}
-		i++
-		stream.WriteString(v.Name)
-		stream.WriteRaw(`:`)
-		stream.WriteString(v.Value)
-	})
-	stream.WriteObjectEnd()
-}
-
-func labelsIsEmpty(ptr unsafe.Pointer) bool {
-	labelsPtr := (*labels.Labels)(ptr)
-	return labelsPtr.IsEmpty()
-}
diff --git a/web/ui/module/codemirror-promql/package.json b/web/ui/module/codemirror-promql/package.json
index 8a9dbcb08..c7fafb133 100644
--- a/web/ui/module/codemirror-promql/package.json
+++ b/web/ui/module/codemirror-promql/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@prometheus-io/codemirror-promql",
-  "version": "0.46.0",
+  "version": "0.45.0",
   "description": "a CodeMirror mode for the PromQL language",
   "types": "dist/esm/index.d.ts",
   "module": "dist/esm/index.js",
@@ -29,7 +29,7 @@
   },
   "homepage": "https://github.com/prometheus/prometheus/blob/main/web/ui/module/codemirror-promql/README.md",
   "dependencies": {
-    "@prometheus-io/lezer-promql": "0.46.0",
+    "@prometheus-io/lezer-promql": "0.45.0",
     "lru-cache": "^6.0.0"
   },
   "devDependencies": {
diff --git a/web/ui/module/lezer-promql/package.json b/web/ui/module/lezer-promql/package.json
index 6e4f599a3..847a9610e 100644
--- a/web/ui/module/lezer-promql/package.json
+++ b/web/ui/module/lezer-promql/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@prometheus-io/lezer-promql",
-  "version": "0.46.0",
+  "version": "0.45.0",
   "description": "lezer-based PromQL grammar",
   "main": "dist/index.cjs",
   "type": "module",
diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json
index f6593bee9..ac365d0d0 100644
--- a/web/ui/package-lock.json
+++ b/web/ui/package-lock.json
@@ -28,10 +28,10 @@
     },
     "module/codemirror-promql": {
       "name": "@prometheus-io/codemirror-promql",
-      "version": "0.46.0",
+      "version": "0.45.0",
       "license": "Apache-2.0",
       "dependencies": {
-        "@prometheus-io/lezer-promql": "0.46.0",
+        "@prometheus-io/lezer-promql": "0.45.0",
         "lru-cache": "^6.0.0"
       },
       "devDependencies": {
@@ -61,7 +61,7 @@
     },
     "module/lezer-promql": {
       "name": "@prometheus-io/lezer-promql",
-      "version": "0.46.0",
+      "version": "0.45.0",
       "license": "Apache-2.0",
       "devDependencies": {
         "@lezer/generator": "^1.2.3",
@@ -20765,7 +20765,7 @@
     },
     "react-app": {
       "name": "@prometheus-io/app",
-      "version": "0.46.0",
+      "version": "0.45.0",
       "dependencies": {
         "@codemirror/autocomplete": "^6.7.1",
         "@codemirror/commands": "^6.2.4",
@@ -20783,7 +20783,7 @@
         "@lezer/lr": "^1.3.6",
         "@nexucis/fuzzy": "^0.4.1",
         "@nexucis/kvsearch": "^0.8.1",
-        "@prometheus-io/codemirror-promql": "0.46.0",
+        "@prometheus-io/codemirror-promql": "0.45.0",
         "bootstrap": "^4.6.2",
         "css.escape": "^1.5.1",
         "downshift": "^7.6.0",
@@ -23423,7 +23423,7 @@
         "@lezer/lr": "^1.3.6",
         "@nexucis/fuzzy": "^0.4.1",
         "@nexucis/kvsearch": "^0.8.1",
-        "@prometheus-io/codemirror-promql": "0.46.0",
+        "@prometheus-io/codemirror-promql": "0.45.0",
         "@testing-library/react-hooks": "^7.0.2",
         "@types/enzyme": "^3.10.13",
         "@types/flot": "0.0.32",
@@ -23487,7 +23487,7 @@
         "@lezer/common": "^1.0.3",
         "@lezer/highlight": "^1.1.6",
         "@lezer/lr": "^1.3.6",
-        "@prometheus-io/lezer-promql": "0.46.0",
+        "@prometheus-io/lezer-promql": "0.45.0",
         "@types/lru-cache": "^5.1.1",
         "isomorphic-fetch": "^3.0.0",
         "lru-cache": "^6.0.0",
diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json
index 969de4279..81eba150a 100644
--- a/web/ui/react-app/package.json
+++ b/web/ui/react-app/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@prometheus-io/app",
-  "version": "0.46.0",
+  "version": "0.45.0",
   "private": true,
   "dependencies": {
     "@codemirror/autocomplete": "^6.7.1",
@@ -19,7 +19,7 @@
     "@lezer/common": "^1.0.3",
     "@nexucis/fuzzy": "^0.4.1",
     "@nexucis/kvsearch": "^0.8.1",
-    "@prometheus-io/codemirror-promql": "0.46.0",
+    "@prometheus-io/codemirror-promql": "0.45.0",
     "bootstrap": "^4.6.2",
     "css.escape": "^1.5.1",
     "downshift": "^7.6.0",


Test patch:
diff --git a/promql/bench_test.go b/promql/bench_test.go
index c6a528f7b..6818498bf 100644
--- a/promql/bench_test.go
+++ b/promql/bench_test.go
@@ -186,10 +186,6 @@ func rangeQueryCases() []benchCase {
 			expr:  "count({__name__!=\"\",l=\"\"})",
 			steps: 1,
 		},
-		// Functions which have special handling inside eval()
-		{
-			expr: "timestamp(a_X)",
-		},
 	}
 
 	// X in an expr will be replaced by different metric sizes.
diff --git a/promql/engine_test.go b/promql/engine_test.go
index 54567d154..ca4a022e0 100644
--- a/promql/engine_test.go
+++ b/promql/engine_test.go
@@ -1977,50 +1977,6 @@ func TestSubquerySelector(t *testing.T) {
 	}
 }
 
-func TestTimestampFunction_StepsMoreOftenThanSamples(t *testing.T) {
-	test, err := NewTest(t, `
-load 1m
-  metric 0+1x1000
-`)
-	require.NoError(t, err)
-	defer test.Close()
-
-	err = test.Run()
-	require.NoError(t, err)
-
-	query := "timestamp(metric)"
-	start := time.Unix(0, 0)
-	end := time.Unix(61, 0)
-	interval := time.Second
-
-	// We expect the value to be 0 for t=0s to t=59s (inclusive), then 60 for t=60s and t=61s.
-	expectedPoints := []FPoint{}
-
-	for t := 0; t <= 59; t++ {
-		expectedPoints = append(expectedPoints, FPoint{F: 0, T: int64(t * 1000)})
-	}
-
-	expectedPoints = append(
-		expectedPoints,
-		FPoint{F: 60, T: 60_000},
-		FPoint{F: 60, T: 61_000},
-	)
-
-	expectedResult := Matrix{
-		Series{
-			Floats: expectedPoints,
-			Metric: labels.EmptyLabels(),
-		},
-	}
-
-	qry, err := test.QueryEngine().NewRangeQuery(test.context, test.Queryable(), nil, query, start, end, interval)
-	require.NoError(t, err)
-
-	res := qry.Exec(test.Context())
-	require.NoError(t, res.Err)
-	require.Equal(t, expectedResult, res.Value)
-}
-
 type FakeQueryLogger struct {
 	closed bool
 	logs   []interface{}
diff --git a/tsdb/db_test.go b/tsdb/db_test.go
index 0eb361db7..6da774a35 100644
--- a/tsdb/db_test.go
+++ b/tsdb/db_test.go
@@ -1535,7 +1535,6 @@ func TestSizeRetention(t *testing.T) {
 		}
 	}
 	require.NoError(t, headApp.Commit())
-	db.Head().mmapHeadChunks()
 
 	require.Eventually(t, func() bool {
 		return db.Head().chunkDiskMapper.IsQueueEmpty()
@@ -3147,6 +3146,9 @@ func TestOpen_VariousBlockStates(t *testing.T) {
 	tmpCheckpointDir := path.Join(tmpDir, "wal/checkpoint.00000001.tmp")
 	err := os.MkdirAll(tmpCheckpointDir, 0o777)
 	require.NoError(t, err)
+	tmpChunkSnapshotDir := path.Join(tmpDir, chunkSnapshotPrefix+"0000.00000001.tmp")
+	err = os.MkdirAll(tmpChunkSnapshotDir, 0o777)
+	require.NoError(t, err)
 
 	opts := DefaultOptions()
 	opts.RetentionDuration = 0
@@ -3180,6 +3182,8 @@ func TestOpen_VariousBlockStates(t *testing.T) {
 	require.Equal(t, len(expectedIgnoredDirs), ignored)
 	_, err = os.Stat(tmpCheckpointDir)
 	require.True(t, os.IsNotExist(err))
+	_, err = os.Stat(tmpChunkSnapshotDir)
+	require.True(t, os.IsNotExist(err))
 }
 
 func TestOneCheckpointPerCompactCall(t *testing.T) {
@@ -6050,14 +6054,12 @@ func TestDiskFillingUpAfterDisablingOOO(t *testing.T) {
 
 	// Check that m-map files gets deleted properly after compactions.
 
-	db.head.mmapHeadChunks()
 	checkMmapFileContents([]string{"000001", "000002"}, nil)
 	require.NoError(t, db.Compact())
 	checkMmapFileContents([]string{"000002"}, []string{"000001"})
 	require.Nil(t, ms.ooo, "OOO mmap chunk was not compacted")
 
 	addSamples(501, 650)
-	db.head.mmapHeadChunks()
 	checkMmapFileContents([]string{"000002", "000003"}, []string{"000001"})
 	require.NoError(t, db.Compact())
 	checkMmapFileContents(nil, []string{"000001", "000002", "000003"})
diff --git a/tsdb/head_bench_test.go b/tsdb/head_bench_test.go
index 8fdf94db0..2f8e0ba37 100644
--- a/tsdb/head_bench_test.go
+++ b/tsdb/head_bench_test.go
@@ -22,7 +22,6 @@ import (
 	"go.uber.org/atomic"
 
 	"github.com/prometheus/prometheus/model/labels"
-	"github.com/prometheus/prometheus/tsdb/chunks"
 )
 
 func BenchmarkHeadStripeSeriesCreate(b *testing.B) {
@@ -81,6 +80,6 @@ func BenchmarkHeadStripeSeriesCreate_PreCreationFailure(b *testing.B) {
 
 type failingSeriesLifecycleCallback struct{}
 
-func (failingSeriesLifecycleCallback) PreCreation(labels.Labels) error                     { return errors.New("failed") }
-func (failingSeriesLifecycleCallback) PostCreation(labels.Labels)                          {}
-func (failingSeriesLifecycleCallback) PostDeletion(map[chunks.HeadSeriesRef]labels.Labels) {}
+func (failingSeriesLifecycleCallback) PreCreation(labels.Labels) error { return errors.New("failed") }
+func (failingSeriesLifecycleCallback) PostCreation(labels.Labels)      {}
+func (failingSeriesLifecycleCallback) PostDeletion(...labels.Labels)   {}
diff --git a/tsdb/head_read_test.go b/tsdb/head_read_test.go
index ad0a59d34..2712bcd1a 100644
--- a/tsdb/head_read_test.go
+++ b/tsdb/head_read_test.go
@@ -15,14 +15,11 @@ package tsdb
 
 import (
 	"fmt"
-	"sync"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/prometheus/prometheus/model/labels"
 	"github.com/prometheus/prometheus/tsdb/chunkenc"
-	"github.com/prometheus/prometheus/tsdb/chunks"
 )
 
 func TestBoundedChunk(t *testing.T) {
@@ -179,387 +176,3 @@ func newTestChunk(numSamples int) chunkenc.Chunk {
 	}
 	return xor
 }
-
-// TestMemSeries_chunk runs a series of tests on memSeries.chunk() calls.
-// It will simulate various conditions to ensure all code paths in that function are covered.
-func TestMemSeries_chunk(t *testing.T) {
-	const chunkRange int64 = 100
-	const chunkStep int64 = 5
-
-	appendSamples := func(t *testing.T, s *memSeries, start, end int64, cdm *chunks.ChunkDiskMapper) {
-		for i := start; i < end; i += chunkStep {
-			ok, _ := s.append(i, float64(i), 0, chunkOpts{
-				chunkDiskMapper: cdm,
-				chunkRange:      chunkRange,
-				samplesPerChunk: DefaultSamplesPerChunk,
-			})
-			require.True(t, ok, "sample append failed")
-		}
-	}
-
-	type setupFn func(*testing.T, *memSeries, *chunks.ChunkDiskMapper)
-
-	type callOutput uint8
-	const (
-		outOpenHeadChunk   callOutput = iota // memSeries.chunk() call returned memSeries.headChunks with headChunk=true & isOpen=true
-		outClosedHeadChunk                   // memSeries.chunk() call returned memSeries.headChunks with headChunk=true & isOpen=false
-		outMmappedChunk                      // memSeries.chunk() call returned a chunk from memSeries.mmappedChunks with headChunk=false
-		outErr                               // memSeries.chunk() call returned an error
-	)
-
-	tests := []struct {
-		name     string
-		setup    setupFn            // optional function called just before the test memSeries.chunk() call
-		inputID  chunks.HeadChunkID // requested chunk id for memSeries.chunk() call
-		expected callOutput
-	}{
-		{
-			name:     "call ix=0 on empty memSeries",
-			inputID:  0,
-			expected: outErr,
-		},
-		{
-			name:     "call ix=1 on empty memSeries",
-			inputID:  1,
-			expected: outErr,
-		},
-		{
-			name: "firstChunkID > ix",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange, cdm)
-				require.Len(t, s.mmappedChunks, 0, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, int64(0), s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, chunkRange-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-				s.firstChunkID = 5
-			},
-			inputID:  1,
-			expected: outErr,
-		},
-		{
-			name: "call ix=0 on memSeries with no mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange, cdm)
-				require.Len(t, s.mmappedChunks, 0, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, int64(0), s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, chunkRange-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  0,
-			expected: outOpenHeadChunk,
-		},
-		{
-			name: "call ix=1 on memSeries with no mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange, cdm)
-				require.Len(t, s.mmappedChunks, 0, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, int64(0), s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, chunkRange-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  1,
-			expected: outErr,
-		},
-		{
-			name: "call ix=10 on memSeries with no mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange, cdm)
-				require.Len(t, s.mmappedChunks, 0, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, int64(0), s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, chunkRange-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  10,
-			expected: outErr,
-		},
-		{
-			name: "call ix=0 on memSeries with 3 mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*4)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  0,
-			expected: outMmappedChunk,
-		},
-		{
-			name: "call ix=1 on memSeries with 3 mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*4)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  1,
-			expected: outMmappedChunk,
-		},
-		{
-			name: "call ix=3 on memSeries with 3 mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*4)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  3,
-			expected: outOpenHeadChunk,
-		},
-		{
-			name: "call ix=0 on memSeries with 3 mmapped chunks and no headChunk",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*4)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-				s.headChunks = nil
-			},
-			inputID:  0,
-			expected: outMmappedChunk,
-		},
-		{
-			name: "call ix=2 on memSeries with 3 mmapped chunks and no headChunk",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*4)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-				s.headChunks = nil
-			},
-			inputID:  2,
-			expected: outMmappedChunk,
-		},
-		{
-			name: "call ix=3 on memSeries with 3 mmapped chunks and no headChunk",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*4)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-				s.headChunks = nil
-			},
-			inputID:  3,
-			expected: outErr,
-		},
-		{
-			name: "call ix=1 on memSeries with 3 mmapped chunks and closed ChunkDiskMapper",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*4)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-				cdm.Close()
-			},
-			inputID:  1,
-			expected: outErr,
-		},
-		{
-			name: "call ix=3 on memSeries with 3 mmapped chunks and closed ChunkDiskMapper",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*4)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-				cdm.Close()
-			},
-			inputID:  3,
-			expected: outOpenHeadChunk,
-		},
-		{
-			name: "call ix=0 on memSeries with 3 head chunks and no mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*3, cdm)
-				require.Len(t, s.mmappedChunks, 0, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 3, "wrong number of headChunks")
-				require.Equal(t, int64(0), s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*3)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  0,
-			expected: outClosedHeadChunk,
-		},
-		{
-			name: "call ix=1 on memSeries with 3 head chunks and no mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*3, cdm)
-				require.Len(t, s.mmappedChunks, 0, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 3, "wrong number of headChunks")
-				require.Equal(t, int64(0), s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*3)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  1,
-			expected: outClosedHeadChunk,
-		},
-		{
-			name: "call ix=10 on memSeries with 3 head chunks and no mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*3, cdm)
-				require.Len(t, s.mmappedChunks, 0, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 3, "wrong number of headChunks")
-				require.Equal(t, int64(0), s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*3)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  10,
-			expected: outErr,
-		},
-		{
-			name: "call ix=0 on memSeries with 3 head chunks and 3 mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-
-				appendSamples(t, s, chunkRange*4, chunkRange*6, cdm)
-				require.Equal(t, s.headChunks.len(), 3, "wrong number of headChunks")
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*6)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  0,
-			expected: outMmappedChunk,
-		},
-		{
-			name: "call ix=2 on memSeries with 3 head chunks and 3 mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-
-				appendSamples(t, s, chunkRange*4, chunkRange*6, cdm)
-				require.Equal(t, s.headChunks.len(), 3, "wrong number of headChunks")
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*6)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  2,
-			expected: outMmappedChunk,
-		},
-		{
-			name: "call ix=3 on memSeries with 3 head chunks and 3 mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-
-				appendSamples(t, s, chunkRange*4, chunkRange*6, cdm)
-				require.Equal(t, s.headChunks.len(), 3, "wrong number of headChunks")
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*6)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  3,
-			expected: outClosedHeadChunk,
-		},
-		{
-			name: "call ix=5 on memSeries with 3 head chunks and 3 mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-
-				appendSamples(t, s, chunkRange*4, chunkRange*6, cdm)
-				require.Equal(t, s.headChunks.len(), 3, "wrong number of headChunks")
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*6)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  5,
-			expected: outOpenHeadChunk,
-		},
-		{
-			name: "call ix=6 on memSeries with 3 head chunks and 3 mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-
-				appendSamples(t, s, chunkRange*4, chunkRange*6, cdm)
-				require.Equal(t, s.headChunks.len(), 3, "wrong number of headChunks")
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*6)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  6,
-			expected: outErr,
-		},
-
-		{
-			name: "call ix=10 on memSeries with 3 head chunks and 3 mmapped chunks",
-			setup: func(t *testing.T, s *memSeries, cdm *chunks.ChunkDiskMapper) {
-				appendSamples(t, s, 0, chunkRange*4, cdm)
-				s.mmapChunks(cdm)
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, s.headChunks.len(), 1, "wrong number of headChunks")
-
-				appendSamples(t, s, chunkRange*4, chunkRange*6, cdm)
-				require.Equal(t, s.headChunks.len(), 3, "wrong number of headChunks")
-				require.Len(t, s.mmappedChunks, 3, "wrong number of mmappedChunks")
-				require.Equal(t, chunkRange*3, s.headChunks.oldest().minTime, "wrong minTime on last headChunks element")
-				require.Equal(t, (chunkRange*6)-chunkStep, s.headChunks.maxTime, "wrong maxTime on first headChunks element")
-			},
-			inputID:  10,
-			expected: outErr,
-		},
-	}
-
-	memChunkPool := &sync.Pool{
-		New: func() interface{} {
-			return &memChunk{}
-		},
-	}
-
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			dir := t.TempDir()
-			chunkDiskMapper, err := chunks.NewChunkDiskMapper(nil, dir, chunkenc.NewPool(), chunks.DefaultWriteBufferSize, chunks.DefaultWriteQueueSize)
-			require.NoError(t, err)
-			defer func() {
-				require.NoError(t, chunkDiskMapper.Close())
-			}()
-
-			series := newMemSeries(labels.EmptyLabels(), 1, true)
-
-			if tc.setup != nil {
-				tc.setup(t, series, chunkDiskMapper)
-			}
-
-			chk, headChunk, isOpen, err := series.chunk(tc.inputID, chunkDiskMapper, memChunkPool)
-			switch tc.expected {
-			case outOpenHeadChunk:
-				require.NoError(t, err, "unexpected error")
-				require.True(t, headChunk, "expected a chunk with headChunk=true but got headChunk=%v", headChunk)
-				require.True(t, isOpen, "expected a chunk with isOpen=true but got isOpen=%v", isOpen)
-			case outClosedHeadChunk:
-				require.NoError(t, err, "unexpected error")
-				require.True(t, headChunk, "expected a chunk with headChunk=true but got headChunk=%v", headChunk)
-				require.False(t, isOpen, "expected a chunk with isOpen=false but got isOpen=%v", isOpen)
-			case outMmappedChunk:
-				require.NoError(t, err, "unexpected error")
-				require.False(t, headChunk, "expected a chunk with headChunk=false but got gc=%v", headChunk)
-			case outErr:
-				require.Nil(t, chk, "got a non-nil chunk reference returned with an error")
-				require.Error(t, err)
-			}
-		})
-	}
-}
diff --git a/tsdb/head_test.go b/tsdb/head_test.go
index 9b49aca03..0a6eef66c 100644
--- a/tsdb/head_test.go
+++ b/tsdb/head_test.go
@@ -292,7 +292,7 @@ func BenchmarkLoadWAL(b *testing.B) {
 							// Create one mmapped chunk per series, with one sample at the given time.
 							s := newMemSeries(labels.Labels{}, chunks.HeadSeriesRef(k)*101, defaultIsolationDisabled)
 							s.append(c.mmappedChunkT, 42, 0, cOpts)
-							s.mmapChunks(chunkDiskMapper)
+							s.mmapCurrentHeadChunk(chunkDiskMapper)
 						}
 						require.NoError(b, chunkDiskMapper.Close())
 					}
@@ -587,15 +587,15 @@ func TestHead_ReadWAL(t *testing.T) {
 				return x
 			}
 
-			c, _, _, err := s10.chunk(0, head.chunkDiskMapper, &head.memChunkPool)
+			c, _, err := s10.chunk(0, head.chunkDiskMapper, &head.memChunkPool)
 			require.NoError(t, err)
 			require.Equal(t, []sample{{100, 2, nil, nil}, {101, 5, nil, nil}}, expandChunk(c.chunk.Iterator(nil)))
-			c, _, _, err = s50.chunk(0, head.chunkDiskMapper, &head.memChunkPool)
+			c, _, err = s50.chunk(0, head.chunkDiskMapper, &head.memChunkPool)
 			require.NoError(t, err)
 			require.Equal(t, []sample{{101, 6, nil, nil}}, expandChunk(c.chunk.Iterator(nil)))
 			// The samples before the new series record should be discarded since a duplicate record
 			// is only possible when old samples were compacted.
-			c, _, _, err = s100.chunk(0, head.chunkDiskMapper, &head.memChunkPool)
+			c, _, err = s100.chunk(0, head.chunkDiskMapper, &head.memChunkPool)
 			require.NoError(t, err)
 			require.Equal(t, []sample{{101, 7, nil, nil}}, expandChunk(c.chunk.Iterator(nil)))
 
@@ -822,200 +822,30 @@ func TestMemSeries_truncateChunks(t *testing.T) {
 		ok, _ := s.append(int64(i), float64(i), 0, cOpts)
 		require.True(t, ok, "sample append failed")
 	}
-	s.mmapChunks(chunkDiskMapper)
 
 	// Check that truncate removes half of the chunks and afterwards
 	// that the ID of the last chunk still gives us the same chunk afterwards.
 	countBefore := len(s.mmappedChunks) + 1 // +1 for the head chunk.
 	lastID := s.headChunkID(countBefore - 1)
-	lastChunk, _, _, err := s.chunk(lastID, chunkDiskMapper, &memChunkPool)
+	lastChunk, _, err := s.chunk(lastID, chunkDiskMapper, &memChunkPool)
 	require.NoError(t, err)
 	require.NotNil(t, lastChunk)
 
-	chk, _, _, err := s.chunk(0, chunkDiskMapper, &memChunkPool)
+	chk, _, err := s.chunk(0, chunkDiskMapper, &memChunkPool)
 	require.NotNil(t, chk)
 	require.NoError(t, err)
 
 	s.truncateChunksBefore(2000, 0)
 
 	require.Equal(t, int64(2000), s.mmappedChunks[0].minTime)
-	_, _, _, err = s.chunk(0, chunkDiskMapper, &memChunkPool)
+	_, _, err = s.chunk(0, chunkDiskMapper, &memChunkPool)
 	require.Equal(t, storage.ErrNotFound, err, "first chunks not gone")
 	require.Equal(t, countBefore/2, len(s.mmappedChunks)+1) // +1 for the head chunk.
-	chk, _, _, err = s.chunk(lastID, chunkDiskMapper, &memChunkPool)
+	chk, _, err = s.chunk(lastID, chunkDiskMapper, &memChunkPool)
 	require.NoError(t, err)
 	require.Equal(t, lastChunk, chk)
 }
 
-func TestMemSeries_truncateChunks_scenarios(t *testing.T) {
-	const chunkRange = 100
-	const chunkStep = 5
-
-	tests := []struct {
-		name                 string
-		headChunks           int                // the number of head chubks to create on memSeries by appending enough samples
-		mmappedChunks        int                // the number of mmapped chunks to create on memSeries by appending enough samples
-		truncateBefore       int64              // the mint to pass to truncateChunksBefore()
-		expectedTruncated    int                // the number of chunks that we're expecting be truncated and returned by truncateChunksBefore()
-		expectedHead         int                // the expected number of head chunks after truncation
-		expectedMmap         int                // the expected number of mmapped chunks after truncation
-		expectedFirstChunkID chunks.HeadChunkID // the expected series.firstChunkID after truncation
-	}{
-		{
-			name:           "empty memSeries",
-			truncateBefore: chunkRange * 10,
-		},
-		{
-			name:         "single head chunk, not truncated",
-			headChunks:   1,
-			expectedHead: 1,
-		},
-		{
-			name:                 "single head chunk, truncated",
-			headChunks:           1,
-			truncateBefore:       chunkRange,
-			expectedTruncated:    1,
-			expectedHead:         0,
-			expectedFirstChunkID: 1,
-		},
-		{
-			name:         "2 head chunks, not truncated",
-			headChunks:   2,
-			expectedHead: 2,
-		},
-		{
-			name:                 "2 head chunks, first truncated",
-			headChunks:           2,
-			truncateBefore:       chunkRange,
-			expectedTruncated:    1,
-			expectedHead:         1,
-			expectedFirstChunkID: 1,
-		},
-		{
-			name:                 "2 head chunks, everything truncated",
-			headChunks:           2,
-			truncateBefore:       chunkRange * 2,
-			expectedTruncated:    2,
-			expectedHead:         0,
-			expectedFirstChunkID: 2,
-		},
-		{
-			name:                 "no head chunks, 3 mmap chunks, second mmap truncated",
-			headChunks:           0,
-			mmappedChunks:        3,
-			truncateBefore:       chunkRange * 2,
-			expectedTruncated:    2,
-			expectedHead:         0,
-			expectedMmap:         1,
-			expectedFirstChunkID: 2,
-		},
-		{
-			name:          "single head chunk, single mmap chunk, not truncated",
-			headChunks:    1,
-			mmappedChunks: 1,
-			expectedHead:  1,
-			expectedMmap:  1,
-		},
-		{
-			name:                 "single head chunk, single mmap chunk, mmap truncated",
-			headChunks:           1,
-			mmappedChunks:        1,
-			truncateBefore:       chunkRange,
-			expectedTruncated:    1,
-			expectedHead:         1,
-			expectedMmap:         0,
-			expectedFirstChunkID: 1,
-		},
-		{
-			name:                 "5 head chunk, 5 mmap chunk, third head truncated",
-			headChunks:           5,
-			mmappedChunks:        5,
-			truncateBefore:       chunkRange * 7,
-			expectedTruncated:    7,
-			expectedHead:         3,
-			expectedMmap:         0,
-			expectedFirstChunkID: 7,
-		},
-		{
-			name:                 "2 head chunks, 3 mmap chunks, second mmap truncated",
-			headChunks:           2,
-			mmappedChunks:        3,
-			truncateBefore:       chunkRange * 2,
-			expectedTruncated:    2,
-			expectedHead:         2,
-			expectedMmap:         1,
-			expectedFirstChunkID: 2,
-		},
-	}
-
-	for _, tc := range tests {
-		t.Run(tc.name, func(t *testing.T) {
-			dir := t.TempDir()
-			chunkDiskMapper, err := chunks.NewChunkDiskMapper(nil, dir, chunkenc.NewPool(), chunks.DefaultWriteBufferSize, chunks.DefaultWriteQueueSize)
-			require.NoError(t, err)
-			defer func() {
-				require.NoError(t, chunkDiskMapper.Close())
-			}()
-
-			series := newMemSeries(labels.EmptyLabels(), 1, true)
-
-			cOpts := chunkOpts{
-				chunkDiskMapper: chunkDiskMapper,
-				chunkRange:      chunkRange,
-				samplesPerChunk: DefaultSamplesPerChunk,
-			}
-
-			var headStart int
-			if tc.mmappedChunks > 0 {
-				headStart = (tc.mmappedChunks + 1) * chunkRange
-				for i := 0; i < (tc.mmappedChunks+1)*chunkRange; i += chunkStep {
-					ok, _ := series.append(int64(i), float64(i), 0, cOpts)
-					require.True(t, ok, "sample append failed")
-				}
-				series.mmapChunks(chunkDiskMapper)
-			}
-
-			if tc.headChunks == 0 {
-				series.headChunks = nil
-			} else {
-				for i := headStart; i < chunkRange*(tc.mmappedChunks+tc.headChunks); i += chunkStep {
-					ok, _ := series.append(int64(i), float64(i), 0, cOpts)
-					require.True(t, ok, "sample append failed: %d", i)
-				}
-			}
-
-			if tc.headChunks > 0 {
-				require.NotNil(t, series.headChunks, "head chunk is missing")
-				require.Equal(t, tc.headChunks, series.headChunks.len(), "wrong number of head chunks")
-			} else {
-				require.Nil(t, series.headChunks, "head chunk is present")
-			}
-			require.Equal(t, tc.mmappedChunks, len(series.mmappedChunks), "wrong number of mmapped chunks")
-
-			truncated := series.truncateChunksBefore(tc.truncateBefore, 0)
-			require.Equal(t, tc.expectedTruncated, truncated, "wrong number of truncated chunks returned")
-
-			require.Equal(t, tc.expectedMmap, len(series.mmappedChunks), "wrong number of mmappedChunks after truncation")
-
-			if tc.expectedHead > 0 {
-				require.NotNil(t, series.headChunks, "headChunks should is nil after truncation")
-				require.Equal(t, tc.expectedHead, series.headChunks.len(), "wrong number of head chunks after truncation")
-				require.Nil(t, series.headChunks.oldest().prev, "last head chunk cannot have any next chunk set")
-			} else {
-				require.Nil(t, series.headChunks, "headChunks should is non-nil after truncation")
-			}
-
-			if series.headChunks != nil || len(series.mmappedChunks) > 0 {
-				require.GreaterOrEqual(t, series.maxTime(), tc.truncateBefore, "wrong value of series.maxTime() after truncation")
-			} else {
-				require.Equal(t, int64(math.MinInt64), series.maxTime(), "wrong value of series.maxTime() after truncation")
-			}
-
-			require.Equal(t, tc.expectedFirstChunkID, series.firstChunkID, "wrong firstChunkID after truncation")
-		})
-	}
-}
-
 func TestHeadDeleteSeriesWithoutSamples(t *testing.T) {
 	for _, compress := range []wlog.CompressionType{wlog.CompressionNone, wlog.CompressionSnappy, wlog.CompressionZstd} {
 		t.Run(fmt.Sprintf("compress=%s", compress), func(t *testing.T) {
@@ -1533,7 +1363,6 @@ func TestMemSeries_append(t *testing.T) {
 	ok, chunkCreated = s.append(999, 2, 0, cOpts)
 	require.True(t, ok, "append failed")
 	require.False(t, chunkCreated, "second sample should use same chunk")
-	s.mmapChunks(chunkDiskMapper)
 
 	ok, chunkCreated = s.append(1000, 3, 0, cOpts)
 	require.True(t, ok, "append failed")
@@ -1543,12 +1372,11 @@ func TestMemSeries_append(t *testing.T) {
 	require.True(t, ok, "append failed")
 	require.False(t, chunkCreated, "second sample should use same chunk")
 
-	s.mmapChunks(chunkDiskMapper)
 	require.Equal(t, 1, len(s.mmappedChunks), "there should be only 1 mmapped chunk")
 	require.Equal(t, int64(998), s.mmappedChunks[0].minTime, "wrong chunk range")
 	require.Equal(t, int64(999), s.mmappedChunks[0].maxTime, "wrong chunk range")
-	require.Equal(t, int64(1000), s.headChunks.minTime, "wrong chunk range")
-	require.Equal(t, int64(1001), s.headChunks.maxTime, "wrong chunk range")
+	require.Equal(t, int64(1000), s.headChunk.minTime, "wrong chunk range")
+	require.Equal(t, int64(1001), s.headChunk.maxTime, "wrong chunk range")
 
 	// Fill the range [1000,2000) with many samples. Intermediate chunks should be cut
 	// at approximately 120 samples per chunk.
@@ -1556,7 +1384,6 @@ func TestMemSeries_append(t *testing.T) {
 		ok, _ := s.append(1001+int64(i), float64(i), 0, cOpts)
 		require.True(t, ok, "append failed")
 	}
-	s.mmapChunks(chunkDiskMapper)
 
 	require.Greater(t, len(s.mmappedChunks)+1, 7, "expected intermediate chunks")
 
@@ -1610,23 +1437,21 @@ func TestMemSeries_appendHistogram(t *testing.T) {
 	require.True(t, ok, "append failed")
 	require.False(t, chunkCreated, "second sample should use same chunk")
 
-	s.mmapChunks(chunkDiskMapper)
 	require.Equal(t, 1, len(s.mmappedChunks), "there should be only 1 mmapped chunk")
 	require.Equal(t, int64(998), s.mmappedChunks[0].minTime, "wrong chunk range")
 	require.Equal(t, int64(999), s.mmappedChunks[0].maxTime, "wrong chunk range")
-	require.Equal(t, int64(1000), s.headChunks.minTime, "wrong chunk range")
-	require.Equal(t, int64(1001), s.headChunks.maxTime, "wrong chunk range")
+	require.Equal(t, int64(1000), s.headChunk.minTime, "wrong chunk range")
+	require.Equal(t, int64(1001), s.headChunk.maxTime, "wrong chunk range")
 
 	ok, chunkCreated = s.appendHistogram(1002, histogramWithOneMoreBucket, 0, cOpts)
 	require.True(t, ok, "append failed")
 	require.False(t, chunkCreated, "third sample should trigger a re-encoded chunk")
 
-	s.mmapChunks(chunkDiskMapper)
 	require.Equal(t, 1, len(s.mmappedChunks), "there should be only 1 mmapped chunk")
 	require.Equal(t, int64(998), s.mmappedChunks[0].minTime, "wrong chunk range")
 	require.Equal(t, int64(999), s.mmappedChunks[0].maxTime, "wrong chunk range")
-	require.Equal(t, int64(1000), s.headChunks.minTime, "wrong chunk range")
-	require.Equal(t, int64(1002), s.headChunks.maxTime, "wrong chunk range")
+	require.Equal(t, int64(1000), s.headChunk.minTime, "wrong chunk range")
+	require.Equal(t, int64(1002), s.headChunk.maxTime, "wrong chunk range")
 }
 
 func TestMemSeries_append_atVariableRate(t *testing.T) {
@@ -1670,7 +1495,6 @@ func TestMemSeries_append_atVariableRate(t *testing.T) {
 	require.True(t, ok, "new chunk sample was not appended")
 	require.True(t, chunkCreated, "sample at block duration timestamp should create a new chunk")
 
-	s.mmapChunks(chunkDiskMapper)
 	var totalSamplesInChunks int
 	for i, c := range s.mmappedChunks {
 		totalSamplesInChunks += int(c.numSamples)
@@ -2017,7 +1841,6 @@ func TestHeadReadWriterRepair(t *testing.T) {
 			require.True(t, ok, "series append failed")
 			require.False(t, chunkCreated, "chunk was created")
 			h.chunkDiskMapper.CutNewFile()
-			s.mmapChunks(h.chunkDiskMapper)
 		}
 		require.NoError(t, h.Close())
 
@@ -2162,7 +1985,6 @@ func TestMemSeriesIsolation(t *testing.T) {
 			_, err := app.Append(0, labels.FromStrings("foo", "bar"), int64(i), float64(i))
 			require.NoError(t, err)
 			require.NoError(t, app.Commit())
-			h.mmapHeadChunks()
 		}
 		return i
 	}
@@ -2844,7 +2666,7 @@ func TestIteratorSeekIntoBuffer(t *testing.T) {
 		require.True(t, ok, "sample append failed")
 	}
 
-	c, _, _, err := s.chunk(0, chunkDiskMapper, &sync.Pool{
+	c, _, err := s.chunk(0, chunkDiskMapper, &sync.Pool{
 		New: func() interface{} {
 			return &memChunk{}
 		},
@@ -3270,7 +3092,6 @@ func TestHistogramInWALAndMmapChunk(t *testing.T) {
 			}
 		}
 		require.NoError(t, app.Commit())
-		head.mmapHeadChunks()
 	}
 
 	// There should be 11 mmap chunks in s1.
@@ -3282,7 +3103,7 @@ func TestHistogramInWALAndMmapChunk(t *testing.T) {
 		cpy := *mmap
 		expMmapChunks = append(expMmapChunks, &cpy)
 	}
-	expHeadChunkSamples := ms.headChunks.chunk.NumSamples()
+	expHeadChunkSamples := ms.headChunk.chunk.NumSamples()
 	require.Greater(t, expHeadChunkSamples, 0)
 
 	// Series with mix of histograms and float.
@@ -3378,7 +3199,7 @@ func TestHistogramInWALAndMmapChunk(t *testing.T) {
 	// Checking contents of s1.
 	ms = head.series.getByHash(s1.Hash(), s1)
 	require.Equal(t, expMmapChunks, ms.mmappedChunks)
-	require.Equal(t, expHeadChunkSamples, ms.headChunks.chunk.NumSamples())
+	require.Equal(t, expHeadChunkSamples, ms.headChunk.chunk.NumSamples())
 
 	testQuery := func() {
 		q, err := NewBlockQuerier(head, head.MinTime(), head.MaxTime())
@@ -3917,8 +3738,6 @@ func testHistogramStaleSampleHelper(t *testing.T, floatHistogram bool) {
 	// Only 1 chunk in the memory, no m-mapped chunk.
 	s := head.series.getByHash(l.Hash(), l)
 	require.NotNil(t, s)
-	require.NotNil(t, s.headChunks)
-	require.Equal(t, s.headChunks.len(), 1)
 	require.Equal(t, 0, len(s.mmappedChunks))
 	testQuery(1)
 
@@ -3947,13 +3766,10 @@ func testHistogramStaleSampleHelper(t *testing.T, floatHistogram bool) {
 		expHistograms = append(expHistograms, timedHistogram{t: 100*int64(len(expHistograms)) + 1, h: &histogram.Histogram{Sum: math.Float64frombits(value.StaleNaN)}})
 	}
 	require.NoError(t, app.Commit())
-	head.mmapHeadChunks()
 
 	// Total 2 chunks, 1 m-mapped.
 	s = head.series.getByHash(l.Hash(), l)
 	require.NotNil(t, s)
-	require.NotNil(t, s.headChunks)
-	require.Equal(t, s.headChunks.len(), 1)
 	require.Equal(t, 1, len(s.mmappedChunks))
 	testQuery(2)
 }
@@ -3988,7 +3804,6 @@ func TestHistogramCounterResetHeader(t *testing.T) {
 
 				ms, _, err := head.getOrCreate(l.Hash(), l)
 				require.NoError(t, err)
-				ms.mmapChunks(head.chunkDiskMapper)
 				require.Len(t, ms.mmappedChunks, len(expHeaders)-1) // One is the head chunk.
 
 				for i, mmapChunk := range ms.mmappedChunks {
@@ -4001,9 +3816,9 @@ func TestHistogramCounterResetHeader(t *testing.T) {
 					}
 				}
 				if floatHisto {
-					require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunks.chunk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
+					require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunk.chunk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
 				} else {
-					require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunks.chunk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
+					require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunk.chunk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
 				}
 			}
 
@@ -4094,7 +3909,7 @@ func TestAppendingDifferentEncodingToSameSeries(t *testing.T) {
 		require.NoError(t, err)
 		require.False(t, created)
 		require.NotNil(t, ms)
-		require.Equal(t, count, ms.headChunks.len())
+		require.Len(t, ms.mmappedChunks, count-1) // One will be the head chunk.
 	}
 
 	appends := []struct {
@@ -4535,7 +4350,6 @@ func TestHeadInit_DiscardChunksWithUnsupportedEncoding(t *testing.T) {
 	require.False(t, created, "should already exist")
 	require.NotNil(t, series, "should return the series we created above")
 
-	series.mmapChunks(h.chunkDiskMapper)
 	expChunks := make([]*mmappedChunk, len(series.mmappedChunks))
 	copy(expChunks, series.mmappedChunks)
 
@@ -4693,7 +4507,6 @@ func TestReplayAfterMmapReplayError(t *testing.T) {
 	require.NoError(t, f.Close())
 
 	openHead()
-	h.mmapHeadChunks()
 
 	// There should be less m-map files due to corruption.
 	files, err = os.ReadDir(filepath.Join(dir, "chunks_head"))
@@ -4884,7 +4697,7 @@ func TestOOOAppendWithNoSeries(t *testing.T) {
 		require.False(t, created)
 		require.NotNil(t, ms)
 
-		require.Nil(t, ms.headChunks)
+		require.Nil(t, ms.headChunk)
 		require.NotNil(t, ms.ooo.oooHeadChunk)
 		require.Equal(t, expSamples, ms.ooo.oooHeadChunk.chunk.NumSamples())
 	}
@@ -4896,8 +4709,8 @@ func TestOOOAppendWithNoSeries(t *testing.T) {
 		require.NotNil(t, ms)
 
 		require.Nil(t, ms.ooo)
-		require.NotNil(t, ms.headChunks)
-		require.Equal(t, expSamples, ms.headChunks.chunk.NumSamples())
+		require.NotNil(t, ms.headChunk)
+		require.Equal(t, expSamples, ms.headChunk.chunk.NumSamples())
 	}
 
 	newLabels := func(idx int) labels.Labels { return labels.FromStrings("foo", fmt.Sprintf("%d", idx)) }
@@ -5008,7 +4821,6 @@ func TestGaugeHistogramWALAndChunkHeader(t *testing.T) {
 	appendHistogram(hists[4])
 
 	checkHeaders := func() {
-		head.mmapHeadChunks()
 		ms, _, err := head.getOrCreate(l.Hash(), l)
 		require.NoError(t, err)
 		require.Len(t, ms.mmappedChunks, 3)
@@ -5023,7 +4835,7 @@ func TestGaugeHistogramWALAndChunkHeader(t *testing.T) {
 			require.NoError(t, err)
 			require.Equal(t, expHeaders[i], chk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
 		}
-		require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunks.chunk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
+		require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunk.chunk.(*chunkenc.HistogramChunk).GetCounterResetHeader())
 	}
 	checkHeaders()
 
@@ -5086,7 +4898,6 @@ func TestGaugeFloatHistogramWALAndChunkHeader(t *testing.T) {
 	checkHeaders := func() {
 		ms, _, err := head.getOrCreate(l.Hash(), l)
 		require.NoError(t, err)
-		head.mmapHeadChunks()
 		require.Len(t, ms.mmappedChunks, 3)
 		expHeaders := []chunkenc.CounterResetHeader{
 			chunkenc.UnknownCounterReset,
@@ -5099,7 +4910,7 @@ func TestGaugeFloatHistogramWALAndChunkHeader(t *testing.T) {
 			require.NoError(t, err)
 			require.Equal(t, expHeaders[i], chk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
 		}
-		require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunks.chunk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
+		require.Equal(t, expHeaders[len(expHeaders)-1], ms.headChunk.chunk.(*chunkenc.FloatHistogramChunk).GetCounterResetHeader())
 	}
 	checkHeaders()
 
diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go
index 99e3b292e..321a13d5e 100644
--- a/web/api/v1/api_test.go
+++ b/web/api/v1/api_test.go
@@ -3417,57 +3417,27 @@ func TestReturnAPIError(t *testing.T) {
 var testResponseWriter = httptest.ResponseRecorder{}
 
 func BenchmarkRespond(b *testing.B) {
+	b.ReportAllocs()
+	request, err := http.NewRequest(http.MethodGet, "/does-not-matter", nil)
+	require.NoError(b, err)
 	points := []promql.FPoint{}
 	for i := 0; i < 10000; i++ {
 		points = append(points, promql.FPoint{F: float64(i * 1000000), T: int64(i)})
 	}
-	matrix := promql.Matrix{}
-	for i := 0; i < 1000; i++ {
-		matrix = append(matrix, promql.Series{
-			Metric: labels.FromStrings("__name__", fmt.Sprintf("series%v", i),
-				"label", fmt.Sprintf("series%v", i),
-				"label2", fmt.Sprintf("series%v", i)),
-			Floats: points[:10],
-		})
-	}
-	series := []labels.Labels{}
-	for i := 0; i < 1000; i++ {
-		series = append(series, labels.FromStrings("__name__", fmt.Sprintf("series%v", i),
-			"label", fmt.Sprintf("series%v", i),
-			"label2", fmt.Sprintf("series%v", i)))
+	response := &QueryData{
+		ResultType: parser.ValueTypeMatrix,
+		Result: promql.Matrix{
+			promql.Series{
+				Floats: points,
+				Metric: labels.EmptyLabels(),
+			},
+		},
 	}
-
-	cases := []struct {
-		name     string
-		response interface{}
-	}{
-		{name: "10000 points no labels", response: &QueryData{
-			ResultType: parser.ValueTypeMatrix,
-			Result: promql.Matrix{
-				promql.Series{
-					Floats: points,
-					Metric: labels.EmptyLabels(),
-				},
-			},
-		}},
-		{name: "1000 labels", response: series},
-		{name: "1000 series 10 points", response: &QueryData{
-			ResultType: parser.ValueTypeMatrix,
-			Result:     matrix,
-		}},
-	}
-	for _, c := range cases {
-		b.Run(c.name, func(b *testing.B) {
-			b.ReportAllocs()
-			request, err := http.NewRequest(http.MethodGet, "/does-not-matter", nil)
-			require.NoError(b, err)
-			b.ResetTimer()
-			api := API{}
-			api.InstallCodec(JSONCodec{})
-			for n := 0; n < b.N; n++ {
-				api.respond(&testResponseWriter, request, c.response, nil)
-			}
-		})
+	b.ResetTimer()
+	api := API{}
+	api.InstallCodec(JSONCodec{})
+	for n := 0; n < b.N; n++ {
+		api.respond(&testResponseWriter, request, response, nil)
 	}
 }