Repository: prometheus/prometheus (language: go)

Issue:
wrong release date info for v2.55.1
### What did you do?

check the release:https://github.com/prometheus/prometheus/releases/tag/v2.55.1

### What did you expect to see?

Expect got the date part as 2024-11-04

### What did you see instead? Under which circumstances?

Got `2024-01-04`:
![img_v3_02gi_e0318762-77dc-4196-8835-374897d9916g](https://github.com/user-attachments/assets/1cee5439-7188-4eb0-965b-e3f4c22830a6)


### System information

_No response_

### Prometheus version

_No response_

### Prometheus configuration file

_No response_

### Alertmanager version

_No response_

### Alertmanager configuration file

_No response_

### Logs

_No response_


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2163c3b0e19..ead292fc7dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,29 +2,40 @@
 
 ## unreleased
 
+## 3.0.0 / 2024-11-14
+
+This release includes new features such as a brand new UI and UTF-8 support enabled by default. As this marks the first new major version in seven years, several breaking changes are introduced. The breaking changes are mainly around the removal of deprecated feature flags and CLI arguments, and the full list can be found below. For users that want to upgrade we recommend to read through our [migration guide](https://prometheus.io/docs/prometheus/3.0/migration/).
+
+* [CHANGE] Set the `GOMAXPROCS` variable automatically to match the Linux CPU quota. Use `--no-auto-gomaxprocs` to disable it. The `auto-gomaxprocs` feature flag was removed. #15376
+* [CHANGE] Set the `GOMEMLIMIT` variable automatically to match the Linux container memory limit. Use `--no-auto-gomemlimit` to disable it. The `auto-gomemlimit` feature flag was removed. #15373
 * [CHANGE] Scraping: Remove implicit fallback to the Prometheus text format in case of invalid/missing Content-Type and fail the scrape instead. Add ability to specify a `fallback_scrape_protocol` in the scrape config. #15136
 * [CHANGE] Remote-write: default enable_http2 to false. #15219
 * [CHANGE] Scraping: normalize "le" and "quantile" label values upon ingestion. #15164
 * [CHANGE] Scraping: config `scrape_classic_histograms` was renamed to `always_scrape_classic_histograms`. #15178
 * [CHANGE] Config: remove expand-external-labels flag, expand external labels env vars by default. #14657
 * [CHANGE] Disallow configuring AM with the v1 api. #13883
-* [ENHANCEMENT] Scraping, rules: handle targets reappearing, or rules moving group, when out-of-order is enabled. #14710
-* [ENHANCEMENT] Tools: add debug printouts to promtool rules unit testing #15196
-* [ENHANCEMENT] Scraping: support Created-Timestamp feature on native histograms. #14694
-* [BUGFIX] PromQL: Fix stddev+stdvar aggregations to always ignore native histograms. #14941
-* [BUGFIX] PromQL: Fix stddev+stdvar aggregations to treat Infinity consistently. #14941
-* [BUGFIX] OTLP receiver: Preserve colons when generating metric names in suffix adding mode (this mode is always enabled, unless one uses Prometheus as a library). #15251
-* [BUGFIX] Clamp functions: Ignore any points with native histograms. #15169
-
-## 3.0.0-beta.1 / 2024-10-09
-
 * [CHANGE] regexp `.` now matches all characters (performance improvement). #14505
 * [CHANGE] `holt_winters` is now called `double_exponential_smoothing` and moves behind the [experimental-promql-functions feature flag](https://prometheus.io/docs/prometheus/latest/feature_flags/#experimental-promql-functions). #14930
 * [CHANGE] API: The OTLP receiver endpoint can now be enabled using `--web.enable-otlp-receiver` instead of `--enable-feature=otlp-write-receiver`. #14894
 * [CHANGE] Prometheus will not add or remove port numbers from the target address. `no-default-scrape-port` feature flag removed. #14160
 * [CHANGE] Logging: the format of log lines has changed a little, along with the adoption of Go's Structured Logging package. #14906
-* [CHANGE] Don't create extra `_created` timeseries if feature-flag `created-timestamp-zero-ingestion' is enabled. #14738
+* [CHANGE] Don't create extra `_created` timeseries if feature-flag `created-timestamp-zero-ingestion` is enabled. #14738
 * [CHANGE] Float literals and time durations being the same is now a stable fetaure. #15111
+* [CHANGE] UI: The old web UI has been replaced by a completely new one that is less cluttered and adds a few new features (PromLens-style tree view, better metrics explorer, "Explain" tab). However, it is still missing some features of the old UI (notably, exemplar display and heatmaps). To switch back to the old UI, you can use the feature flag `--enable-feature=old-ui` for the time being. #14872
+* [CHANGE] PromQL: Range selectors and the lookback delta are now left-open, i.e. a sample coinciding with the lower time limit is excluded rather than included. #13904
+* [CHANGE] Kubernetes SD: Remove support for `discovery.k8s.io/v1beta1` API version of EndpointSlice. This version is no longer served as of Kubernetes v1.25. #14365
+* [CHANGE] Kubernetes SD: Remove support for `networking.k8s.io/v1beta1` API version of Ingress. This version is no longer served as of Kubernetes v1.22. #14365
+* [CHANGE] UTF-8: Enable UTF-8 support by default. Prometheus now allows all UTF-8 characters in metric and label names. The corresponding `utf8-name` feature flag has been removed. #14705
+* [CHANGE] Console: Remove example files for the console feature. Users can continue using the console feature by supplying their own JavaScript and templates. #14807
+* [CHANGE] SD: Enable the new service discovery manager by default. This SD manager does not restart unchanged discoveries upon reloading. This makes reloads faster and reduces pressure on service discoveries' sources. The corresponding `new-service-discovery-manager` feature flag has been removed. #14770
+* [CHANGE] Agent mode has been promoted to stable. The feature flag `agent` has been removed. To run Prometheus in Agent mode, use the new `--agent` cmdline arg instead. #14747
+* [CHANGE] Remove deprecated `remote-write-receiver`,`promql-at-modifier`, and `promql-negative-offset` feature flags. #13456, #14526
+* [CHANGE] Remove deprecated `storage.tsdb.allow-overlapping-blocks`, `alertmanager.timeout`, and `storage.tsdb.retention` flags. #14640, #14643
+* [FEATURE] OTLP receiver: Ability to skip UTF-8 normalization using `otlp.translation_strategy = NoUTF8EscapingWithSuffixes` configuration option. #15384
+* [FEATURE] Support config reload automatically - feature flag `auto-reload-config`. #14769
+* [ENHANCEMENT] Scraping, rules: handle targets reappearing, or rules moving group, when out-of-order is enabled. #14710
+* [ENHANCEMENT] Tools: add debug printouts to promtool rules unit testing #15196
+* [ENHANCEMENT] Scraping: support Created-Timestamp feature on native histograms. #14694
 * [ENHANCEMENT] UI: Many fixes and improvements. #14898, #14899, #14907, #14908, #14912, #14913, #14914, #14931, #14940, #14945, #14946, #14972, #14981, #14982, #14994, #15096
 * [ENHANCEMENT] UI: Web UI now displays notifications, e.g. when starting up and shutting down. #15082
 * [ENHANCEMENT] PromQL: Introduce exponential interpolation for native histograms. #14677
@@ -32,10 +43,16 @@
 * [ENHANCEMENT] Alerts: remove metrics for removed Alertmanagers. #13909
 * [ENHANCEMENT] Kubernetes SD: Support sidecar containers in endpoint discovery. #14929
 * [ENHANCEMENT] Consul SD: Support catalog filters. #11224
+* [ENHANCEMENT] Move AM discovery page from "Monitoring status" to "Server status". #14875
 * [PERF] TSDB: Parallelize deletion of postings after head compaction. #14975
 * [PERF] TSDB: Chunk encoding: shorten some write sequences. #14932
 * [PERF] TSDB: Grow postings by doubling. #14721
 * [PERF] Relabeling: Optimize adding a constant label pair. #12180
+* [BUGFIX] Scraping: Don't log errors on empty scrapes. #15357
+* [BUGFIX] UI: fix selector / series formatting for empty metric names. #15341
+* [BUGFIX] PromQL: Fix stddev+stdvar aggregations to always ignore native histograms. #14941
+* [BUGFIX] PromQL: Fix stddev+stdvar aggregations to treat Infinity consistently. #14941
+* [BUGFIX] OTLP receiver: Preserve colons when generating metric names in suffix adding mode (this mode is always enabled, unless one uses Prometheus as a library). #15251
 * [BUGFIX] Scraping: Unit was missing when using protobuf format. #15095
 * [BUGFIX] PromQL: Only return "possible non-counter" annotation when `rate` returns points. #14910
 * [BUGFIX] TSDB: Chunks could have one unnecessary zero byte at the end. #14854
@@ -43,28 +60,20 @@
 * [BUGFIX] PromQL: Unary negation of native histograms. #14821
 * [BUGFIX] PromQL: Handle stale marker in native histogram series (e.g. if series goes away and comes back). #15025
 * [BUGFIX] Autoreload: Reload invalid yaml files. #14947
+* [BUGFIX] Scrape: Do not override target parameter labels with config params. #11029
 
-## 3.0.0-beta.0 / 2024-09-05
+## 2.53.3 / 2024-11-04
 
-Release 3.0.0-beta.0 includes new features such as a brand new UI and UTF-8 support enabled by default. As a new major version, several breaking changes are introduced. The breaking changes are mainly around the removal of deprecated feature flags and CLI arguments, and the full list can be found below. Most users should be able to try this release out of the box without any configuration changes.
+* [BUGFIX] Scraping: allow multiple samples on same series, with explicit timestamps. #14685, #14740
 
-As is traditional with a beta release, we do **not** recommend users install 3.0.0-beta on critical production systems, but we do want everyone to test it out and find bugs.
+## 2.53.2 / 2024-08-09
 
-* [CHANGE] UI: The old web UI has been replaced by a completely new one that is less cluttered and adds a few new features (PromLens-style tree view, better metrics explorer, "Explain" tab). However, it is still missing some features of the old UI (notably, exemplar display and heatmaps). To switch back to the old UI, you can use the feature flag `--enable-feature=old-ui` for the time being. #14872
-* [CHANGE] PromQL: Range selectors and the lookback delta are now left-open, i.e. a sample coinciding with the lower time limit is excluded rather than included. #13904
-* [CHANGE] Kubernetes SD: Remove support for `discovery.k8s.io/v1beta1` API version of EndpointSlice. This version is no longer served as of Kubernetes v1.25. #14365
-* [CHANGE] Kubernetes SD: Remove support for `networking.k8s.io/v1beta1` API version of Ingress. This version is no longer served as of Kubernetes v1.22. #14365
-* [CHANGE] UTF-8: Enable UTF-8 support by default. Prometheus now allows all UTF-8 characters in metric and label names. The corresponding `utf8-name` feature flag has been removed. #14705
-* [CHANGE] Console: Remove example files for the console feature. Users can continue using the console feature by supplying their own JavaScript and templates. #14807
-* [CHANGE] SD: Enable the new service discovery manager by default. This SD manager does not restart unchanged discoveries upon reloading. This makes reloads faster and reduces pressure on service discoveries' sources. The corresponding `new-service-discovery-manager` feature flag has been removed. #14770
-* [CHANGE] Agent mode has been promoted to stable. The feature flag `agent` has been removed. To run Prometheus in Agent mode, use the new `--agent` cmdline arg instead. #14747
-* [CHANGE] Remove deprecated `remote-write-receiver`,`promql-at-modifier`, and `promql-negative-offset` feature flags. #13456, #14526
-* [CHANGE] Remove deprecated `storage.tsdb.allow-overlapping-blocks`, `alertmanager.timeout`, and `storage.tsdb.retention` flags. #14640, #14643
-* [ENHANCEMENT] Move AM discovery page from "Monitoring status" to "Server status". #14875
-* [FEATURE] Support config reload automatically - feature flag `auto-reload-config`. #14769
-* [BUGFIX] Scrape: Do not override target parameter labels with config params. #11029
+Fix a bug where Prometheus would crash with a segmentation fault if a remote-read
+request accessed a block on disk at about the same time as TSDB created a new block.
+
+[BUGFIX] Remote-Read: Resolve occasional segmentation fault on query. #14515,#14523
 
-## 2.55.1 / 2024-01-04
+## 2.55.1 / 2024-11-04
 
 * [BUGFIX] `round()` function did not remove `__name__` label. #15250
 
diff --git a/VERSION b/VERSION
index 1941d528275..4a36342fcab 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-3.0.0-beta.1
+3.0.0
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index 2a64d8c387c..6fa5ac0d9b0 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -171,8 +171,17 @@ remote_write:
   [ - <remote_write> ... ]
 
 # Settings related to the OTLP receiver feature.
+# See https://prometheus.io/docs/guides/opentelemetry/ for best practices.
 otlp:
   [ promote_resource_attributes: [<string>, ...] | default = [ ] ]
+  # Configures translation of OTLP metrics when received through the OTLP metrics
+  # endpoint. Available values:
+  # - "UnderscoreEscapingWithSuffixes" refers to commonly agreed normalization used
+  #   by OpenTelemetry in https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/translator/prometheus
+  # - "NoUTF8EscapingWithSuffixes" is a mode that relies on UTF-8 support in Prometheus.
+  #   It preserves all special characters like dots, but it still add required suffixes
+  #   for units and _total like in UnderscoreEscapingWithSuffixes.
+  [ translation_strategy: <string> | default = "UnderscoreEscapingWithSuffixes" ]
 
 # Settings related to the remote read feature.
 remote_read:
diff --git a/docs/migration.md b/docs/migration.md
index 43fc43df2a1..73de5bcaaff 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -18,19 +18,27 @@ This document offers guidance on migrating from Prometheus 2.x to Prometheus 3.0
   - `remote-write-receiver`
   - `new-service-discovery-manager`
   - `expand-external-labels`
-    Environment variable references `${var}` or `$var` in external label values 
+      - Environment variable references `${var}` or `$var` in external label values 
     are replaced according to the values of the current environment variables.  
-    References to undefined variables are replaced by the empty string.
+      - References to undefined variables are replaced by the empty string.
     The `$` character can be escaped by using `$$`.
   - `no-default-scrape-port`
-    Prometheus v3 will no longer add ports to scrape targets according to the 
+      - Prometheus v3 will no longer add ports to scrape targets according to the 
     specified scheme. Target will now appear in labels as configured.
-    If you rely on scrape targets like 
-    `https://example.com/metrics` or `http://exmaple.com/metrics` to be 
-    represented as `https://example.com/metrics:443` and 
-    `http://example.com/metrics:80` respectively, add them to your target URLs
-    - `agent`
-      Instead use the dedicated `--agent` cli flag.
+      - If you rely on scrape targets like 
+      `https://example.com/metrics` or `http://exmaple.com/metrics` to be 
+      represented as `https://example.com/metrics:443` and 
+      `http://example.com/metrics:80` respectively, add them to your target URLs
+  - `agent`
+      - Instead use the dedicated `--agent` CLI flag.
+  - `auto-gomemlimit`
+      - Prometheus v3 will automatically set `GOMEMLIMIT` to match the Linux 
+      container memory limit. If there is no container limit, or the process is 
+      running outside of containers, the system memory total is used. To disable 
+      this, `--no-auto-gomemlimit` is available.
+  - `auto-gomaxprocs`
+      - Prometheus v3 will automatically set `GOMAXPROCS` to match the Linux 
+      container CPU quota. To disable this, `--no-auto-gomaxprocs` is available.
 
   Prometheus v3 will log a warning if you continue to pass these to 
   `--enable-feature`.
@@ -54,38 +62,34 @@ This document offers guidance on migrating from Prometheus 2.x to Prometheus 3.0
 
 - The `.` pattern in regular expressions in PromQL matches newline characters. 
   With this change a regular expressions like `.*` matches strings that include 
-  `\n`. This applies to matchers in queries and relabel configs. For example the 
-  following regular expressions now match the accompanying strings, wheras in 
-  Prometheus v2 these combinations didn't match.
-
-| Regex      | Additional matches  |
-| -----      | ------              |
-| ".*"       | "foo\n", "Foo\nBar" |
-| "foo.?bar" | "foo\nbar"          |
-| "foo.+bar" | "foo\nbar"          |
-
-  If you want Prometheus v3 to behave like v2 did, you will have to change your 
-  regular expressions by replacing all `.` patterns with `[^\n]`, e.g.  
+  `\n`. This applies to matchers in queries and relabel configs.
+  - For example, the following regular expressions now match the accompanying
+  strings, whereas in Prometheus v2 these combinations didn't match.
+      - `.*` additionally matches `foo\n` and `Foo\nBar`
+      - `foo.?bar` additionally matches `foo\nbar`
+      - `foo.+bar` additionally matches `foo\nbar`
+  - If you want Prometheus v3 to behave like v2, you will have to change your
+  regular expressions by replacing all `.` patterns with `[^\n]`, e.g.
   `foo[^\n]*`.
 - Lookback and range selectors are left open and right closed (previously left 
   closed and right closed). This change affects queries when the evaluation time 
   perfectly aligns with the sample timestamps. For example assume querying a 
-  timeseries with even spaced samples exactly 1 minute apart. Before Prometheus 
-  3.x, range query with `5m` will mostly return 5 samples. But if the query 
+  timeseries with evenly spaced samples exactly 1 minute apart. Before Prometheus
+  v3, a range query with `5m` would usually return 5 samples. But if the query
   evaluation aligns perfectly with a scrape, it would return 6 samples. In 
-  Prometheus 3.x queries like this will always return 5 samples.
-  This change has likely few effects for everyday use, except for some sub query 
-  use cases.
-  Query front-ends that align queries usually align sub-queries to multiples of 
-  the step size. These sub queries will likely be affected.
+  Prometheus v3 queries like this will always return 5 samples.  
+  This change has likely few effects for everyday use, except for some subquery
+  use cases.  
+  Query front-ends that align queries usually align subqueries to multiples of
+  the step size. These subqueries will likely be affected.  
   Tests are more likely to affected. To fix those either adjust the expected 
-  number of samples or extend to range by less then one sample interval.
+  number of samples or extend the range by less than one sample interval.
 - The `holt_winters` function has been renamed to `double_exponential_smoothing` 
   and is now guarded by the `promql-experimental-functions` feature flag.
-  If you want to keep using holt_winters, you have to do both of these things:
-    - Rename holt_winters to double_exponential_smoothing in your queries.
+  If you want to keep using `holt_winters`, you have to do both of these things:
+    - Rename `holt_winters` to `double_exponential_smoothing` in your queries.
     - Pass `--enable-feature=promql-experimental-functions` in your Prometheus 
-      cli invocation..
+      CLI invocation.
 
 ## Scrape protocols
 Prometheus v3 is more strict concerning the Content-Type header received when
@@ -95,7 +99,7 @@ header was unparsable or unrecognised. This could lead to incorrect data being
 parsed in the scrape. Prometheus v3 will now fail the scrape in such cases.
 
 If a scrape target is not providing the correct Content-Type header the
-fallback protocol can be specified using the fallback_scrape_protocol
+fallback protocol can be specified using the `fallback_scrape_protocol`
 parameter. See [Prometheus scrape_config documentation.](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config)
 
 This is a breaking change as scrapes that may have succeeded with Prometheus v2
@@ -104,26 +108,32 @@ may now fail if this fallback protocol is not specified.
 ## Miscellaneous
 
 ### TSDB format and downgrade
-The TSDB format has been changed in Prometheus v2.55 in preparation for changes 
-to the index format. Consequently a Prometheus v3 tsdb can only be read by a 
-Prometheus v2.55 or newer.
-Before upgrading to Prometheus v3 please upgrade to v2.55 first and confirm 
-Prometheus works as expected. Only then continue with the upgrade to v3.
 
-### TSDB Storage contract
+The TSDB format has been changed slightly in Prometheus v2.55 in preparation for changes 
+to the index format. Consequently, a Prometheus v3 TSDB can only be read by a
+Prometheus v2.55 or newer. Keep that in mind when upgrading to v3 -- you will be only
+able to downgrade to v2.55, not lower, without loosing your TSDB persitent data.
+
+As an extra safety measure, you could optionally consider upgrading to v2.55 first and
+confirm Prometheus works as expected, before upgrading to v3.
+
+### TSDB storage contract
+
 TSDB compatible storage is now expected to return results matching the specified 
 selectors. This might impact some third party implementations, most likely 
 implementing `remote_read`.
+
 This contract is not explicitly enforced, but can cause undefined behavior.
 
 ### UTF-8 names
+
 Prometheus v3 supports UTF-8 in metric and label names. This means metric and
 label names can change after upgrading according to what is exposed by
 endpoints. Furthermore, metric and label names that would have previously been
 flagged as invalid no longer will be.
 
 Users wishing to preserve the original validation behavior can update their
-prometheus yaml configuration to specify the legacy validation scheme:
+Prometheus yaml configuration to specify the legacy validation scheme:
 
 ```
 global:
@@ -143,6 +153,7 @@ scrape_configs:
 ### Log message format
 Prometheus v3 has adopted `log/slog` over the previous `go-kit/log`. This 
 results in a change of log message format. An example of the old log format is:
+
 ```
 ts=2024-10-23T22:01:06.074Z caller=main.go:627 level=info msg="No time or size retention was set so using the default time retention" duration=15d
 ts=2024-10-23T22:01:06.074Z caller=main.go:671 level=info msg="Starting Prometheus Server" mode=server version="(version=, branch=, revision=91d80252c3e528728b0f88d254dd720f6be07cb8-modified)"
@@ -151,6 +162,7 @@ ts=2024-10-23T22:01:06.074Z caller=main.go:677 level=info host_details="(Linux 5
 ```
 
 a similar sequence in the new log format looks like this:
+
 ```
 time=2024-10-24T00:03:07.542+02:00 level=INFO source=/home/user/go/src/github.com/prometheus/prometheus/cmd/prometheus/main.go:640 msg="No time or size retention was set so using the default time retention" duration=15d
 time=2024-10-24T00:03:07.542+02:00 level=INFO source=/home/user/go/src/github.com/prometheus/prometheus/cmd/prometheus/main.go:681 msg="Starting Prometheus Server" mode=server version="(version=, branch=, revision=7c7116fea8343795cae6da42960cacd0207a2af8)"
@@ -159,7 +171,7 @@ time=2024-10-24T00:03:07.542+02:00 level=INFO source=/home/user/go/src/github.co
 
 ### `le` and `quantile` label values
 In Prometheus v3, the values of the `le` label of classic histograms and the 
-`quantile` label of summaries are normalized upon ingestions. In Prometheus v2 
+`quantile` label of summaries are normalized upon ingestion. In Prometheus v2
 the value of these labels depended on the scrape protocol (protobuf vs text 
 format) in some situations. This led to label values changing based on the 
 scrape protocol. E.g. a metric exposed as `my_classic_hist{le="1"}` would be 
@@ -195,6 +207,12 @@ This should **only** be applied to metrics that currently produce such labels.
         regex: (\d+)\.0+;.*_bucket
 ```
 
+### Disallow configuring Alertmanager with the v1 API
+Prometheus 3 no longer supports Alertmanager's v1 API. Effectively Prometheus 3 
+requires [Alertmanager 0.16.0](https://github.com/prometheus/alertmanager/releases/tag/v0.16.0) or later. Users with older Alertmanager
+versions or configurations that use `alerting: alertmanagers: [api_version: v1]` 
+need to upgrade Alertmanager and change their configuration to use `api_version: v2`.
+
 # Prometheus 2.0 migration guide
 
 For the Prometheus 1.8 to 2.0 please refer to the [Prometheus v2.55 documentation](https://prometheus.io/docs/prometheus/2.55/migration/).
diff --git a/scrape/scrape.go b/scrape/scrape.go
index c5e34498209..80e46e43994 100644
--- a/scrape/scrape.go
+++ b/scrape/scrape.go
@@ -1552,7 +1552,34 @@ type appendErrors struct {
 	numExemplarOutOfOrder int
 }
 
+// Update the stale markers.
+func (sl *scrapeLoop) updateStaleMarkers(app storage.Appender, defTime int64) (err error) {
+	sl.cache.forEachStale(func(lset labels.Labels) bool {
+		// Series no longer exposed, mark it stale.
+		app.SetOptions(&storage.AppendOptions{DiscardOutOfOrder: true})
+		_, err = app.Append(0, lset, defTime, math.Float64frombits(value.StaleNaN))
+		app.SetOptions(nil)
+		switch {
+		case errors.Is(err, storage.ErrOutOfOrderSample), errors.Is(err, storage.ErrDuplicateSampleForTimestamp):
+			// Do not count these in logging, as this is expected if a target
+			// goes away and comes back again with a new scrape loop.
+			err = nil
+		}
+		return err == nil
+	})
+	return
+}
+
 func (sl *scrapeLoop) append(app storage.Appender, b []byte, contentType string, ts time.Time) (total, added, seriesAdded int, err error) {
+	defTime := timestamp.FromTime(ts)
+
+	if len(b) == 0 {
+		// Empty scrape. Just update the stale makers and swap the cache (but don't flush it).
+		err = sl.updateStaleMarkers(app, defTime)
+		sl.cache.iterDone(false)
+		return
+	}
+
 	p, err := textparse.New(b, contentType, sl.fallbackScrapeProtocol, sl.alwaysScrapeClassicHist, sl.enableCTZeroIngestion, sl.symbolTable)
 	if p == nil {
 		sl.l.Error(
@@ -1574,9 +1601,7 @@ func (sl *scrapeLoop) append(app storage.Appender, b []byte, contentType string,
 			"err", err,
 		)
 	}
-
 	var (
-		defTime         = timestamp.FromTime(ts)
 		appErrs         = appendErrors{}
 		sampleLimitErr  error
 		bucketLimitErr  error
@@ -1617,9 +1642,8 @@ func (sl *scrapeLoop) append(app storage.Appender, b []byte, contentType string,
 		if err != nil {
 			return
 		}
-		// Only perform cache cleaning if the scrape was not empty.
-		// An empty scrape (usually) is used to indicate a failed scrape.
-		sl.cache.iterDone(len(b) > 0)
+		// Flush and swap the cache as the scrape was non-empty.
+		sl.cache.iterDone(true)
 	}()
 
 loop:
@@ -1862,19 +1886,7 @@ loop:
 		sl.l.Warn("Error on ingesting out-of-order exemplars", "num_dropped", appErrs.numExemplarOutOfOrder)
 	}
 	if err == nil {
-		sl.cache.forEachStale(func(lset labels.Labels) bool {
-			// Series no longer exposed, mark it stale.
-			app.SetOptions(&storage.AppendOptions{DiscardOutOfOrder: true})
-			_, err = app.Append(0, lset, defTime, math.Float64frombits(value.StaleNaN))
-			app.SetOptions(nil)
-			switch {
-			case errors.Is(err, storage.ErrOutOfOrderSample), errors.Is(err, storage.ErrDuplicateSampleForTimestamp):
-				// Do not count these in logging, as this is expected if a target
-				// goes away and comes back again with a new scrape loop.
-				err = nil
-			}
-			return err == nil
-		})
+		err = sl.updateStaleMarkers(app, defTime)
 	}
 	return
 }
diff --git a/web/ui/mantine-ui/package.json b/web/ui/mantine-ui/package.json
index 4e886e482f8..dc32eee96d7 100644
--- a/web/ui/mantine-ui/package.json
+++ b/web/ui/mantine-ui/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@prometheus-io/mantine-ui",
   "private": true,
-  "version": "0.300.0-beta.1",
+  "version": "0.300.0",
   "type": "module",
   "scripts": {
     "start": "vite",
@@ -28,7 +28,7 @@
     "@microsoft/fetch-event-source": "^2.0.1",
     "@nexucis/fuzzy": "^0.5.1",
     "@nexucis/kvsearch": "^0.9.1",
-    "@prometheus-io/codemirror-promql": "0.300.0-beta.1",
+    "@prometheus-io/codemirror-promql": "0.300.0",
     "@reduxjs/toolkit": "^2.2.1",
     "@tabler/icons-react": "^3.19.0",
     "@tanstack/react-query": "^5.59.0",
diff --git a/web/ui/module/codemirror-promql/package.json b/web/ui/module/codemirror-promql/package.json
index bcc54647957..8fb6dc4ba78 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.300.0-beta.1",
+  "version": "0.300.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.300.0-beta.1",
+    "@prometheus-io/lezer-promql": "0.300.0",
     "lru-cache": "^11.0.1"
   },
   "devDependencies": {
diff --git a/web/ui/module/lezer-promql/package.json b/web/ui/module/lezer-promql/package.json
index 0883552c845..6564d3fa56d 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.300.0-beta.1",
+  "version": "0.300.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 10b14a8bc7a..f5ae7642b03 100644
--- a/web/ui/package-lock.json
+++ b/web/ui/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "prometheus-io",
-  "version": "0.300.0-beta.1",
+  "version": "0.300.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "prometheus-io",
-      "version": "0.300.0-beta.1",
+      "version": "0.300.0",
       "workspaces": [
         "mantine-ui",
         "module/*"
@@ -24,7 +24,7 @@
     },
     "mantine-ui": {
       "name": "@prometheus-io/mantine-ui",
-      "version": "0.300.0-beta.1",
+      "version": "0.300.0",
       "dependencies": {
         "@codemirror/autocomplete": "^6.18.1",
         "@codemirror/language": "^6.10.2",
@@ -42,7 +42,7 @@
         "@microsoft/fetch-event-source": "^2.0.1",
         "@nexucis/fuzzy": "^0.5.1",
         "@nexucis/kvsearch": "^0.9.1",
-        "@prometheus-io/codemirror-promql": "0.300.0-beta.1",
+        "@prometheus-io/codemirror-promql": "0.300.0",
         "@reduxjs/toolkit": "^2.2.1",
         "@tabler/icons-react": "^3.19.0",
         "@tanstack/react-query": "^5.59.0",
@@ -155,10 +155,10 @@
     },
     "module/codemirror-promql": {
       "name": "@prometheus-io/codemirror-promql",
-      "version": "0.300.0-beta.1",
+      "version": "0.300.0",
       "license": "Apache-2.0",
       "dependencies": {
-        "@prometheus-io/lezer-promql": "0.300.0-beta.1",
+        "@prometheus-io/lezer-promql": "0.300.0",
         "lru-cache": "^11.0.1"
       },
       "devDependencies": {
@@ -188,7 +188,7 @@
     },
     "module/lezer-promql": {
       "name": "@prometheus-io/lezer-promql",
-      "version": "0.300.0-beta.1",
+      "version": "0.300.0",
       "license": "Apache-2.0",
       "devDependencies": {
         "@lezer/generator": "^1.7.1",
diff --git a/web/ui/package.json b/web/ui/package.json
index 6d6e2801087..ef5bdb81fc9 100644
--- a/web/ui/package.json
+++ b/web/ui/package.json
@@ -1,7 +1,7 @@
 {
   "name": "prometheus-io",
   "description": "Monorepo for the Prometheus UI",
-  "version": "0.300.0-beta.1",
+  "version": "0.300.0",
   "private": true,
   "scripts": {
     "build": "bash build_ui.sh --all",
diff --git a/web/ui/react-app/package-lock.json b/web/ui/react-app/package-lock.json
index d3de03e5e05..bdc2d72beed 100644
--- a/web/ui/react-app/package-lock.json
+++ b/web/ui/react-app/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@prometheus-io/app",
-  "version": "0.55.0",
+  "version": "0.300.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@prometheus-io/app",
-      "version": "0.55.0",
+      "version": "0.300.0",
       "dependencies": {
         "@codemirror/autocomplete": "^6.17.0",
         "@codemirror/commands": "^6.6.0",
diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json
index d75aba5c4a8..30db488ccca 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.55.0",
+  "version": "0.300.0",
   "private": true,
   "dependencies": {
     "@codemirror/autocomplete": "^6.17.0",


Test patch:
diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go
index 02a31b76277..ed40f52fa69 100644
--- a/scrape/scrape_test.go
+++ b/scrape/scrape_test.go
@@ -120,13 +120,13 @@ func runScrapeLoopTest(t *testing.T, s *teststorage.TestStorage, expectOutOfOrde
 	timestampInorder2 := now.Add(5 * time.Minute)
 
 	slApp := sl.appender(context.Background())
-	_, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1`), "", timestampInorder1)
+	_, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1`), "text/plain", timestampInorder1)
 	require.NoError(t, err)
 
-	_, _, _, err = sl.append(slApp, []byte(`metric_a{a="1",b="1"} 2`), "", timestampOutOfOrder)
+	_, _, _, err = sl.append(slApp, []byte(`metric_a{a="1",b="1"} 2`), "text/plain", timestampOutOfOrder)
 	require.NoError(t, err)
 
-	_, _, _, err = sl.append(slApp, []byte(`metric_a{a="1",b="1"} 3`), "", timestampInorder2)
+	_, _, _, err = sl.append(slApp, []byte(`metric_a{a="1",b="1"} 3`), "text/plain", timestampInorder2)
 	require.NoError(t, err)
 
 	require.NoError(t, slApp.Commit())
@@ -756,6 +756,10 @@ func TestScrapePoolScrapeLoopsStarted(t *testing.T) {
 }
 
 func newBasicScrapeLoop(t testing.TB, ctx context.Context, scraper scraper, app func(ctx context.Context) storage.Appender, interval time.Duration) *scrapeLoop {
+	return newBasicScrapeLoopWithFallback(t, ctx, scraper, app, interval, "")
+}
+
+func newBasicScrapeLoopWithFallback(t testing.TB, ctx context.Context, scraper scraper, app func(ctx context.Context) storage.Appender, interval time.Duration, fallback string) *scrapeLoop {
 	return newScrapeLoop(ctx,
 		scraper,
 		nil, nil,
@@ -783,7 +787,7 @@ func newBasicScrapeLoop(t testing.TB, ctx context.Context, scraper scraper, app
 		newTestScrapeMetrics(t),
 		false,
 		model.LegacyValidation,
-		"text/plain",
+		fallback,
 	)
 }
 
@@ -844,7 +848,8 @@ func TestScrapeLoopStop(t *testing.T) {
 		app      = func(ctx context.Context) storage.Appender { return appender }
 	)
 
-	sl := newBasicScrapeLoop(t, context.Background(), scraper, app, 10*time.Millisecond)
+	// Since we're writing samples directly below we need to provide a protocol fallback.
+	sl := newBasicScrapeLoopWithFallback(t, context.Background(), scraper, app, 10*time.Millisecond, "text/plain")
 
 	// Terminate loop after 2 scrapes.
 	numScrapes := 0
@@ -928,7 +933,7 @@ func TestScrapeLoopRun(t *testing.T) {
 		scrapeMetrics,
 		false,
 		model.LegacyValidation,
-		"text/plain",
+		"",
 	)
 
 	// The loop must terminate during the initial offset if the context
@@ -1075,7 +1080,7 @@ func TestScrapeLoopMetadata(t *testing.T) {
 		scrapeMetrics,
 		false,
 		model.LegacyValidation,
-		"text/plain",
+		"",
 	)
 	defer cancel()
 
@@ -1126,7 +1131,7 @@ func TestScrapeLoopSeriesAdded(t *testing.T) {
 	ctx, sl := simpleTestScrapeLoop(t)
 
 	slApp := sl.appender(ctx)
-	total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\n"), "", time.Time{})
+	total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\n"), "text/plain", time.Time{})
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 	require.Equal(t, 1, total)
@@ -1134,7 +1139,7 @@ func TestScrapeLoopSeriesAdded(t *testing.T) {
 	require.Equal(t, 1, seriesAdded)
 
 	slApp = sl.appender(ctx)
-	total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1\n"), "", time.Time{})
+	total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1\n"), "text/plain", time.Time{})
 	require.NoError(t, slApp.Commit())
 	require.NoError(t, err)
 	require.Equal(t, 1, total)
@@ -1164,7 +1169,7 @@ func TestScrapeLoopFailWithInvalidLabelsAfterRelabel(t *testing.T) {
 	}
 
 	slApp := sl.appender(ctx)
-	total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\n"), "", time.Time{})
+	total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\n"), "text/plain", time.Time{})
 	require.ErrorContains(t, err, "invalid metric name or label names")
 	require.NoError(t, slApp.Rollback())
 	require.Equal(t, 1, total)
@@ -1188,7 +1193,7 @@ func TestScrapeLoopFailLegacyUnderUTF8(t *testing.T) {
 	sl.validationScheme = model.LegacyValidation
 
 	slApp := sl.appender(ctx)
-	total, added, seriesAdded, err := sl.append(slApp, []byte("{\"test.metric\"} 1\n"), "", time.Time{})
+	total, added, seriesAdded, err := sl.append(slApp, []byte("{\"test.metric\"} 1\n"), "text/plain", time.Time{})
 	require.ErrorContains(t, err, "invalid metric name or label names")
 	require.NoError(t, slApp.Rollback())
 	require.Equal(t, 1, total)
@@ -1199,7 +1204,7 @@ func TestScrapeLoopFailLegacyUnderUTF8(t *testing.T) {
 	sl.validationScheme = model.UTF8Validation
 
 	slApp = sl.appender(ctx)
-	total, added, seriesAdded, err = sl.append(slApp, []byte("{\"test.metric\"} 1\n"), "", time.Time{})
+	total, added, seriesAdded, err = sl.append(slApp, []byte("{\"test.metric\"} 1\n"), "text/plain", time.Time{})
 	require.NoError(t, err)
 	require.Equal(t, 1, total)
 	require.Equal(t, 1, added)
@@ -1229,7 +1234,7 @@ func BenchmarkScrapeLoopAppend(b *testing.B) {
 
 	for i := 0; i < b.N; i++ {
 		ts = ts.Add(time.Second)
-		_, _, _, _ = sl.append(slApp, metrics, "", ts)
+		_, _, _, _ = sl.append(slApp, metrics, "text/plain", ts)
 	}
 }
 
@@ -1338,7 +1343,8 @@ func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrape(t *testing.T) {
 	)
 
 	ctx, cancel := context.WithCancel(context.Background())
-	sl := newBasicScrapeLoop(t, ctx, scraper, app, 10*time.Millisecond)
+	// Since we're writing samples directly below we need to provide a protocol fallback.
+	sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, app, 10*time.Millisecond, "text/plain")
 	// Succeed once, several failures, then stop.
 	numScrapes := 0
 
@@ -1384,7 +1390,8 @@ func TestScrapeLoopRunCreatesStaleMarkersOnParseFailure(t *testing.T) {
 	)
 
 	ctx, cancel := context.WithCancel(context.Background())
-	sl := newBasicScrapeLoop(t, ctx, scraper, app, 10*time.Millisecond)
+	// Since we're writing samples directly below we need to provide a protocol fallback.
+	sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, app, 10*time.Millisecond, "text/plain")
 
 	// Succeed once, several failures, then stop.
 	scraper.scrapeFunc = func(ctx context.Context, w io.Writer) error {
@@ -1435,7 +1442,8 @@ func TestScrapeLoopCache(t *testing.T) {
 	ctx, cancel := context.WithCancel(context.Background())
 	// Decreasing the scrape interval could make the test fail, as multiple scrapes might be initiated at identical millisecond timestamps.
 	// See https://github.com/prometheus/prometheus/issues/12727.
-	sl := newBasicScrapeLoop(t, ctx, scraper, app, 100*time.Millisecond)
+	// Since we're writing samples directly below we need to provide a protocol fallback.
+	sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, app, 100*time.Millisecond, "text/plain")
 
 	numScrapes := 0
 
@@ -1600,7 +1608,7 @@ func TestScrapeLoopAppend(t *testing.T) {
 		now := time.Now()
 
 		slApp := sl.appender(context.Background())
-		_, _, _, err := sl.append(slApp, []byte(test.scrapeLabels), "", now)
+		_, _, _, err := sl.append(slApp, []byte(test.scrapeLabels), "text/plain", now)
 		require.NoError(t, err)
 		require.NoError(t, slApp.Commit())
 
@@ -1681,7 +1689,7 @@ func TestScrapeLoopAppendForConflictingPrefixedLabels(t *testing.T) {
 				return mutateSampleLabels(l, &Target{labels: labels.FromStrings(tc.targetLabels...)}, false, nil)
 			}
 			slApp := sl.appender(context.Background())
-			_, _, _, err := sl.append(slApp, []byte(tc.exposedLabels), "", time.Date(2000, 1, 1, 1, 0, 0, 0, time.UTC))
+			_, _, _, err := sl.append(slApp, []byte(tc.exposedLabels), "text/plain", time.Date(2000, 1, 1, 1, 0, 0, 0, time.UTC))
 			require.NoError(t, err)
 
 			require.NoError(t, slApp.Commit())
@@ -1719,7 +1727,7 @@ func TestScrapeLoopAppendCacheEntryButErrNotFound(t *testing.T) {
 	now := time.Now()
 
 	slApp := sl.appender(context.Background())
-	_, _, _, err := sl.append(slApp, metric, "", now)
+	_, _, _, err := sl.append(slApp, metric, "text/plain", now)
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -1756,7 +1764,7 @@ func TestScrapeLoopAppendSampleLimit(t *testing.T) {
 
 	now := time.Now()
 	slApp := sl.appender(context.Background())
-	total, added, seriesAdded, err := sl.append(app, []byte("metric_a 1\nmetric_b 1\nmetric_c 1\n"), "", now)
+	total, added, seriesAdded, err := sl.append(app, []byte("metric_a 1\nmetric_b 1\nmetric_c 1\n"), "text/plain", now)
 	require.ErrorIs(t, err, errSampleLimit)
 	require.NoError(t, slApp.Rollback())
 	require.Equal(t, 3, total)
@@ -1785,7 +1793,7 @@ func TestScrapeLoopAppendSampleLimit(t *testing.T) {
 
 	now = time.Now()
 	slApp = sl.appender(context.Background())
-	total, added, seriesAdded, err = sl.append(slApp, []byte("metric_a 1\nmetric_b 1\nmetric_c{deleteme=\"yes\"} 1\nmetric_d 1\nmetric_e 1\nmetric_f 1\nmetric_g 1\nmetric_h{deleteme=\"yes\"} 1\nmetric_i{deleteme=\"yes\"} 1\n"), "", now)
+	total, added, seriesAdded, err = sl.append(slApp, []byte("metric_a 1\nmetric_b 1\nmetric_c{deleteme=\"yes\"} 1\nmetric_d 1\nmetric_e 1\nmetric_f 1\nmetric_g 1\nmetric_h{deleteme=\"yes\"} 1\nmetric_i{deleteme=\"yes\"} 1\n"), "text/plain", now)
 	require.ErrorIs(t, err, errSampleLimit)
 	require.NoError(t, slApp.Rollback())
 	require.Equal(t, 9, total)
@@ -1913,12 +1921,12 @@ func TestScrapeLoop_ChangingMetricString(t *testing.T) {
 
 	now := time.Now()
 	slApp := sl.appender(context.Background())
-	_, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1`), "", now)
+	_, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1`), "text/plain", now)
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
 	slApp = sl.appender(context.Background())
-	_, _, _, err = sl.append(slApp, []byte(`metric_a{b="1",a="1"} 2`), "", now.Add(time.Minute))
+	_, _, _, err = sl.append(slApp, []byte(`metric_a{b="1",a="1"} 2`), "text/plain", now.Add(time.Minute))
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -1937,6 +1945,33 @@ func TestScrapeLoop_ChangingMetricString(t *testing.T) {
 	require.Equal(t, want, capp.resultFloats, "Appended samples not as expected:\n%s", appender)
 }
 
+func TestScrapeLoopAppendFailsWithNoContentType(t *testing.T) {
+	app := &collectResultAppender{}
+
+	// Explicitly setting the lack of fallback protocol here to make it obvious.
+	sl := newBasicScrapeLoopWithFallback(t, context.Background(), nil, func(ctx context.Context) storage.Appender { return app }, 0, "")
+
+	now := time.Now()
+	slApp := sl.appender(context.Background())
+	_, _, _, err := sl.append(slApp, []byte("metric_a 1\n"), "", now)
+	// We expect the appropriate error.
+	require.ErrorContains(t, err, "non-compliant scrape target sending blank Content-Type and no fallback_scrape_protocol specified for target", "Expected \"non-compliant scrape\" error but got: %s", err)
+}
+
+func TestScrapeLoopAppendEmptyWithNoContentType(t *testing.T) {
+	// This test ensures we there are no errors when we get a blank scrape or just want to append a stale marker.
+	app := &collectResultAppender{}
+
+	// Explicitly setting the lack of fallback protocol here to make it obvious.
+	sl := newBasicScrapeLoopWithFallback(t, context.Background(), nil, func(ctx context.Context) storage.Appender { return app }, 0, "")
+
+	now := time.Now()
+	slApp := sl.appender(context.Background())
+	_, _, _, err := sl.append(slApp, []byte(""), "", now)
+	require.NoError(t, err)
+	require.NoError(t, slApp.Commit())
+}
+
 func TestScrapeLoopAppendStaleness(t *testing.T) {
 	app := &collectResultAppender{}
 
@@ -1944,7 +1979,7 @@ func TestScrapeLoopAppendStaleness(t *testing.T) {
 
 	now := time.Now()
 	slApp := sl.appender(context.Background())
-	_, _, _, err := sl.append(slApp, []byte("metric_a 1\n"), "", now)
+	_, _, _, err := sl.append(slApp, []byte("metric_a 1\n"), "text/plain", now)
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -1973,7 +2008,7 @@ func TestScrapeLoopAppendNoStalenessIfTimestamp(t *testing.T) {
 	sl := newBasicScrapeLoop(t, context.Background(), nil, func(ctx context.Context) storage.Appender { return app }, 0)
 	now := time.Now()
 	slApp := sl.appender(context.Background())
-	_, _, _, err := sl.append(slApp, []byte("metric_a 1 1000\n"), "", now)
+	_, _, _, err := sl.append(slApp, []byte("metric_a 1 1000\n"), "text/plain", now)
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -1999,7 +2034,7 @@ func TestScrapeLoopAppendStalenessIfTrackTimestampStaleness(t *testing.T) {
 
 	now := time.Now()
 	slApp := sl.appender(context.Background())
-	_, _, _, err := sl.append(slApp, []byte("metric_a 1 1000\n"), "", now)
+	_, _, _, err := sl.append(slApp, []byte("metric_a 1 1000\n"), "text/plain", now)
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -2519,7 +2554,7 @@ func TestScrapeLoopAppendGracefullyIfAmendOrOutOfOrderOrOutOfBounds(t *testing.T
 
 	now := time.Unix(1, 0)
 	slApp := sl.appender(context.Background())
-	total, added, seriesAdded, err := sl.append(slApp, []byte("out_of_order 1\namend 1\nnormal 1\nout_of_bounds 1\n"), "", now)
+	total, added, seriesAdded, err := sl.append(slApp, []byte("out_of_order 1\namend 1\nnormal 1\nout_of_bounds 1\n"), "text/plain", now)
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -2550,7 +2585,7 @@ func TestScrapeLoopOutOfBoundsTimeError(t *testing.T) {
 
 	now := time.Now().Add(20 * time.Minute)
 	slApp := sl.appender(context.Background())
-	total, added, seriesAdded, err := sl.append(slApp, []byte("normal 1\n"), "", now)
+	total, added, seriesAdded, err := sl.append(slApp, []byte("normal 1\n"), "text/plain", now)
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 	require.Equal(t, 1, total)
@@ -2850,7 +2885,7 @@ func TestScrapeLoop_RespectTimestamps(t *testing.T) {
 
 	now := time.Now()
 	slApp := sl.appender(context.Background())
-	_, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1 0`), "", now)
+	_, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1 0`), "text/plain", now)
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -2877,7 +2912,7 @@ func TestScrapeLoop_DiscardTimestamps(t *testing.T) {
 
 	now := time.Now()
 	slApp := sl.appender(context.Background())
-	_, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1 0`), "", now)
+	_, _, _, err := sl.append(slApp, []byte(`metric_a{a="1",b="1"} 1 0`), "text/plain", now)
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -2901,7 +2936,7 @@ func TestScrapeLoopDiscardDuplicateLabels(t *testing.T) {
 
 	// We add a good and a bad metric to check that both are discarded.
 	slApp := sl.appender(ctx)
-	_, _, _, err := sl.append(slApp, []byte("test_metric{le=\"500\"} 1\ntest_metric{le=\"600\",le=\"700\"} 1\n"), "", time.Time{})
+	_, _, _, err := sl.append(slApp, []byte("test_metric{le=\"500\"} 1\ntest_metric{le=\"600\",le=\"700\"} 1\n"), "text/plain", time.Time{})
 	require.Error(t, err)
 	require.NoError(t, slApp.Rollback())
 	// We need to cycle staleness cache maps after a manual rollback. Otherwise they will have old entries in them,
@@ -2916,7 +2951,7 @@ func TestScrapeLoopDiscardDuplicateLabels(t *testing.T) {
 
 	// We add a good metric to check that it is recorded.
 	slApp = sl.appender(ctx)
-	_, _, _, err = sl.append(slApp, []byte("test_metric{le=\"500\"} 1\n"), "", time.Time{})
+	_, _, _, err = sl.append(slApp, []byte("test_metric{le=\"500\"} 1\n"), "text/plain", time.Time{})
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -2945,7 +2980,7 @@ func TestScrapeLoopDiscardUnnamedMetrics(t *testing.T) {
 	defer cancel()
 
 	slApp := sl.appender(context.Background())
-	_, _, _, err := sl.append(slApp, []byte("nok 1\nnok2{drop=\"drop\"} 1\n"), "", time.Time{})
+	_, _, _, err := sl.append(slApp, []byte("nok 1\nnok2{drop=\"drop\"} 1\n"), "text/plain", time.Time{})
 	require.Error(t, err)
 	require.NoError(t, slApp.Rollback())
 	require.Equal(t, errNameLabelMandatory, err)
@@ -3191,7 +3226,7 @@ func TestScrapeAddFast(t *testing.T) {
 	defer cancel()
 
 	slApp := sl.appender(ctx)
-	_, _, _, err := sl.append(slApp, []byte("up 1\n"), "", time.Time{})
+	_, _, _, err := sl.append(slApp, []byte("up 1\n"), "text/plain", time.Time{})
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 
@@ -3202,7 +3237,7 @@ func TestScrapeAddFast(t *testing.T) {
 	}
 
 	slApp = sl.appender(ctx)
-	_, _, _, err = sl.append(slApp, []byte("up 1\n"), "", time.Time{}.Add(time.Second))
+	_, _, _, err = sl.append(slApp, []byte("up 1\n"), "text/plain", time.Time{}.Add(time.Second))
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 }
@@ -3257,7 +3292,8 @@ func TestScrapeReportSingleAppender(t *testing.T) {
 	)
 
 	ctx, cancel := context.WithCancel(context.Background())
-	sl := newBasicScrapeLoop(t, ctx, scraper, s.Appender, 10*time.Millisecond)
+	// Since we're writing samples directly below we need to provide a protocol fallback.
+	sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, s.Appender, 10*time.Millisecond, "text/plain")
 
 	numScrapes := 0
 
@@ -3484,7 +3520,7 @@ func TestScrapeLoopLabelLimit(t *testing.T) {
 		sl.labelLimits = &test.labelLimits
 
 		slApp := sl.appender(context.Background())
-		_, _, _, err := sl.append(slApp, []byte(test.scrapeLabels), "", time.Now())
+		_, _, _, err := sl.append(slApp, []byte(test.scrapeLabels), "text/plain", time.Now())
 
 		t.Logf("Test:%s", test.title)
 		if test.expectErr {
@@ -4176,7 +4212,8 @@ func TestScrapeLoopRunCreatesStaleMarkersOnFailedScrapeForTimestampedMetrics(t *
 	)
 
 	ctx, cancel := context.WithCancel(context.Background())
-	sl := newBasicScrapeLoop(t, ctx, scraper, app, 10*time.Millisecond)
+	// Since we're writing samples directly below we need to provide a protocol fallback.
+	sl := newBasicScrapeLoopWithFallback(t, ctx, scraper, app, 10*time.Millisecond, "text/plain")
 	sl.trackTimestampsStaleness = true
 	// Succeed once, several failures, then stop.
 	numScrapes := 0
@@ -4421,7 +4458,7 @@ func TestScrapeLoopSeriesAddedDuplicates(t *testing.T) {
 	ctx, sl := simpleTestScrapeLoop(t)
 
 	slApp := sl.appender(ctx)
-	total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\ntest_metric 2\ntest_metric 3\n"), "", time.Time{})
+	total, added, seriesAdded, err := sl.append(slApp, []byte("test_metric 1\ntest_metric 2\ntest_metric 3\n"), "text/plain", time.Time{})
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 	require.Equal(t, 3, total)
@@ -4430,7 +4467,7 @@ func TestScrapeLoopSeriesAddedDuplicates(t *testing.T) {
 	require.Equal(t, 2.0, prom_testutil.ToFloat64(sl.metrics.targetScrapeSampleDuplicate))
 
 	slApp = sl.appender(ctx)
-	total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1\ntest_metric 1\ntest_metric 1\n"), "", time.Time{})
+	total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1\ntest_metric 1\ntest_metric 1\n"), "text/plain", time.Time{})
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 	require.Equal(t, 3, total)
@@ -4440,7 +4477,7 @@ func TestScrapeLoopSeriesAddedDuplicates(t *testing.T) {
 
 	// When different timestamps are supplied, multiple samples are accepted.
 	slApp = sl.appender(ctx)
-	total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1 1001\ntest_metric 1 1002\ntest_metric 1 1003\n"), "", time.Time{})
+	total, added, seriesAdded, err = sl.append(slApp, []byte("test_metric 1 1001\ntest_metric 1 1002\ntest_metric 1 1003\n"), "text/plain", time.Time{})
 	require.NoError(t, err)
 	require.NoError(t, slApp.Commit())
 	require.Equal(t, 3, total)