Repository: etcd-io/etcd

Issue:
make verify-lint fails on devcontainer
### Bug report criteria

- [x] This bug report is not security related, security issues should be disclosed privately via [etcd maintainers](mailto:etcd-maintainers@googlegroups.com).
- [x] This is not a support request or question, support requests or questions should be raised in the etcd [discussion forums](https://github.com/etcd-io/etcd/discussions).
- [x] You have read the etcd [bug reporting guidelines](https://github.com/etcd-io/etcd/blob/main/Documentation/contributor-guide/reporting_bugs.md).
- [x] Existing open issues along with etcd [frequently asked questions](https://etcd.io/docs/latest/faq) have been checked and this is not a duplicate.

### What happened?

make verify-lint fails on devcontainer development environment

### What did you expect to happen?

it shouldn' fail

### How can we reproduce it (as minimally and precisely as possible)?

open workspace in newly built devcontainer and run make verify-lint

### Anything else we need to know?

the problem is that `make install-golangci-lint` is not enforcing version of golangci-lint (it uses 2.1.6 installed during the creation of devcontainer instead of version from tool/mod/go.mod file

### Etcd version (please run commands below)

<details>

```console
$ etcd --version
vscode ➜ /workspaces/etcd (main) $ ./bin/etcd --version
etcd Version: 3.7.0-alpha.0
Git SHA: c68e3f427
Go Version: go1.24.3
Go OS/Arch: linux/amd64

$ etcdctl version
vscode ➜ /workspaces/etcd (main) $ ./bin/etcdctl version
etcdctl version: 3.7.0-alpha.0
API version: 3.7
```

</details>


### Etcd configuration (command line flags or environment variables)

<details>

# paste your configuration here

</details>


### Etcd debug information (please run commands below, feel free to obfuscate the IP address or FQDN in the output)

<details>

```console
$ etcdctl member list -w table
# paste output here

$ etcdctl --endpoints=<member list> endpoint status -w table
# paste output here
```

</details>


### Relevant log output

```Shell
`vscode ➜ /workspaces/etcd (main) $ make verify-lint
PASSES="lint" ./scripts/test.sh
Running with --race
Starting at: Sat Jun 21 12:41:14 AM UTC 2025

'lint' started at Sat Jun 21 12:41:14 AM UTC 2025
% (cd api && 'golangci-lint' 'run' '--config' '/workspaces/etcd/tools/.golangci.yaml' './...')
stderr: Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/product/migration-guide for migration instructions
stderr: Failed executing command with error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/product/migration-guide for migration instructions
FAIL: (code:3):
  % (cd api && 'golangci-lint' 'run' '--config' '/workspaces/etcd/tools/.golangci.yaml' './...')

FAIL: 'run golangci-lint run --config /workspaces/etcd/tools/.golangci.yaml ./...' checking failed (!=0 return code)
There was a Failure in module api, aborting...
FAIL: 'lint' FAILED at Sat Jun 21 12:41:14 AM UTC 2025
make: *** [Makefile:112: verify-lint] Error 255`
```

Discussion:
@BBQing: I can take care of it@BBQing: /assign

Solution patch:
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 5ba33bc16..a7cfc14dc 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -10,7 +10,7 @@ body:
       label: Bug report criteria
       description: Please confirm this bug report meets the following criteria.
       options:
-        - label: This bug report is not security related, security issues should be disclosed privately via [etcd maintainers](mailto:etcd-maintainers@googlegroups.com).
+        - label: This bug report is not security related, security issues should be disclosed privately via security@etcd.io.
         - label: This is not a support request or question, support requests or questions should be raised in the etcd [discussion forums](https://github.com/etcd-io/etcd/discussions).
         - label: You have read the etcd [bug reporting guidelines](https://github.com/etcd-io/etcd/blob/main/Documentation/contributor-guide/reporting_bugs.md).
         - label: Existing open issues along with etcd [frequently asked questions](https://etcd.io/docs/latest/faq) have been checked and this is not a duplicate.
diff --git a/.github/workflows/antithesis-test.yml b/.github/workflows/antithesis-test.yml
index 49d711d31..ad0938270 100644
--- a/.github/workflows/antithesis-test.yml
+++ b/.github/workflows/antithesis-test.yml
@@ -34,6 +34,11 @@ on:
         required: true
         type: string
         default: ""
+      cfg_node_count:
+        description: 'Number of nodes in the etcd cluster'
+        required: false
+        type: int
+        default: 3
 
 # Declare default permissions as read only.
 permissions: read-all
@@ -46,6 +51,8 @@ jobs:
   build-and-push-and-test:
     runs-on: ubuntu-latest
     environment: Antithesis
+    env:
+      CFG_NODE_COUNT: ${{ inputs.cfg_node_count }}
     steps:
       - name: Checkout the code
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -66,13 +73,12 @@ jobs:
           docker push $IMAGE
 
       - name: Build and push client image
-        uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
-        with:
-          context: .
-          file: ./tests/antithesis/test-template/Dockerfile
-          push: true
-          tags: |
-            ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-client:${{ inputs.etcd_ref }}_${{ github.sha }}
+        working-directory: ./tests/antithesis
+        run: |
+          make antithesis-build-client-docker-image
+          export IMAGE="${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-client:${{ inputs.etcd_ref }}_${{ github.sha }}"
+          docker tag etcd-client:latest $IMAGE
+          docker push $IMAGE
 
       - name: Build and push etcd image
         working-directory: ./tests/antithesis
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 786b7fb38..0d28ad53b 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -40,7 +40,7 @@ jobs:
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       # Initializes the CodeQL tools for scanning.
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
+        uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
         with:
           # If you wish to specify custom queries, you can do so here or in a config file.
           # By default, queries listed here will override any specified in a config file.
@@ -50,6 +50,6 @@ jobs:
       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
       # If this step fails, then you should remove it and run the build manually (see below)
       - name: Autobuild
-        uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
+        uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
+        uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index eaadcb5ee..86be41871 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -50,6 +50,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
+        uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
         with:
           sarif_file: results.sarif
diff --git a/.go-version b/.go-version
index 2f4320f67..6521720b4 100644
--- a/.go-version
+++ b/.go-version
@@ -1 +1 @@
-1.24.4
+1.24.5
diff --git a/CHANGELOG/CHANGELOG-3.4.md b/CHANGELOG/CHANGELOG-3.4.md
index 0227647bd..f2af0f4e4 100644
--- a/CHANGELOG/CHANGELOG-3.4.md
+++ b/CHANGELOG/CHANGELOG-3.4.md
@@ -9,10 +9,11 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### etcd server
 
 - Fix [mvcc: avoid double decrement of watcher gauge on close/cancel race](https://github.com/etcd-io/etcd/pull/20065)
+- Fix [Watch on future revision returns old events or notifications](https://github.com/etcd-io/etcd/pull/20291)
 
 ### Dependencies
 
-- Compile binaries using [go 1.23.10](https://github.com/etcd-io/etcd/pull/20166).
+- Compile binaries using [go 1.23.11](https://github.com/etcd-io/etcd/pull/20322).
 
 ---
 
diff --git a/CHANGELOG/CHANGELOG-3.5.md b/CHANGELOG/CHANGELOG-3.5.md
index c2cd4a6eb..b2102f73e 100644
--- a/CHANGELOG/CHANGELOG-3.5.md
+++ b/CHANGELOG/CHANGELOG-3.5.md
@@ -10,6 +10,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 
 - Fix [the compaction pause duration metric is not emitted for every compaction batch](https://github.com/etcd-io/etcd/pull/19771)
 - Fix [mvcc: avoid double decrement of watcher gauge on close/cancel race](https://github.com/etcd-io/etcd/pull/20066)
+- Fix [Watch on future revision returns old events or notifications](https://github.com/etcd-io/etcd/pull/20290)
 
 ### Package `clientv3`
 
@@ -18,7 +19,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 
 ### Dependencies
 
-- Compile binaries using [go 1.23.10](https://github.com/etcd-io/etcd/pull/20164)
+- Compile binaries using [go 1.23.11](https://github.com/etcd-io/etcd/pull/20321)
 
 ---
 
diff --git a/CHANGELOG/CHANGELOG-3.6.md b/CHANGELOG/CHANGELOG-3.6.md
index 1c8bb7a42..ef29af6ce 100644
--- a/CHANGELOG/CHANGELOG-3.6.md
+++ b/CHANGELOG/CHANGELOG-3.6.md
@@ -4,11 +4,20 @@ Previous change logs can be found at [CHANGELOG-3.5](https://github.com/etcd-io/
 
 ---
 
-## v3.6.2 (TBD)
+## v3.6.3 (TBD)
+
+---
+
+## v3.6.2 (2025-07-09)
+
+### etcd server
+
+- Fix [Watch on future revision returns old events or notifications](https://github.com/etcd-io/etcd/pull/20286)
 
 ### Dependencies
 
-- [Bump bbolt to v1.4.1](https://github.com/etcd-io/etcd/pull/20154)
+- [Bump bbolt to v1.4.2](https://github.com/etcd-io/etcd/pull/20267)
+- Compile binaries using [go 1.23.11](https://github.com/etcd-io/etcd/pull/20314).
 
 ---
 
diff --git a/CHANGELOG/CHANGELOG-3.7.md b/CHANGELOG/CHANGELOG-3.7.md
index 4662efac8..78685988f 100644
--- a/CHANGELOG/CHANGELOG-3.7.md
+++ b/CHANGELOG/CHANGELOG-3.7.md
@@ -22,7 +22,7 @@ Previous change logs can be found at [CHANGELOG-3.6](https://github.com/etcd-io/
 
 ### Dependencies
 
-- Compile binaries using [go 1.24.4](https://github.com/etcd-io/etcd/pull/20163)
+- Compile binaries using [go 1.24.5](https://github.com/etcd-io/etcd/pull/20320)
 
 ### Deprecations
 
diff --git a/Dockerfile b/Dockerfile
index 66818aa59..f29109e4a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 ARG ARCH=amd64
-FROM --platform=linux/${ARCH} gcr.io/distroless/static-debian12@sha256:d9f9472a8f4541368192d714a995eb1a99bab1f7071fc8bde261d7eda3b667d8
+FROM --platform=linux/${ARCH} gcr.io/distroless/static-debian12@sha256:b7b9a6953e7bed6baaf37329331051d7bdc1b99c885f6dbeb72d75b1baad54f9
 
 ADD etcd /usr/local/bin/
 ADD etcdctl /usr/local/bin/
diff --git a/Documentation/contributor-guide/release.md b/Documentation/contributor-guide/release.md
index db5acdd8a..a0d90a41e 100644
--- a/Documentation/contributor-guide/release.md
+++ b/Documentation/contributor-guide/release.md
@@ -17,7 +17,6 @@ for ensuring the stability of the release branch.
 - Marek Siarkowicz [@serathius](https://github.com/serathius)
 - Sahdev Zala [@spzala](https://github.com/spzala)
 - Siyuan Zhang [@siyuanfoundation](https://github.com/siyuanfoundation)
-- Wenjia Zhang [@wenjiaswe](https://github.com/wenjiaswe)
 
 All release version numbers follow the format of [semantic versioning 2.0.0](http://semver.org/).
 
@@ -109,8 +108,8 @@ On the day of the release:
    - Open the **draft** release URL shown by the release script
    - Click the pen button at the top right to edit the release
    - Review that it looks correct, reviewing that the bottom checkboxes are checked depending on the
-     release version (v3.4 no checkboxes, v3.5 has the set as latest release checkbox checked,
-     v3.6 has the set as pre-release checkbox checked)
+     release version (v3.4 & v3.5 no checkboxes, v3.6 has the set as latest release checkbox checked,
+     v3.7 has the set as pre-release checkbox checked)
    - Then, publish the release
 5. Announce to the etcd-dev googlegroup
 
diff --git a/Documentation/contributor-guide/triage_prs.md b/Documentation/contributor-guide/triage_prs.md
index ffa0f7a1d..f32e45f4c 100644
--- a/Documentation/contributor-guide/triage_prs.md
+++ b/Documentation/contributor-guide/triage_prs.md
@@ -14,7 +14,7 @@ Following are a few example searches on PR for convenience:
 
 ## Scope
 
-These guidelines serve as a primary document for managing PRs in `etcd`. Everyone is welcome to help manage PRs but the work and responsibilities discussed in this document are created with `etcd` maintainers and active contributors in mind.
+These guidelines serve as a primary document for managing PRs and review policy in `etcd`. Everyone is welcome to help manage PRs but the work and responsibilities discussed in this document are created with `etcd` maintainers and active contributors in mind.
 
 ## Ensure tests are run
 
@@ -30,3 +30,32 @@ Reviewers are responsive in a timely fashion, but considering everyone is busy,
 ## Verify important labels are in place
 
 Make sure that appropriate reviewers are added to the PR. Also, make sure that a milestone is identified. If any of these or other important labels are missing, add them. If a correct label cannot be decided, leave a comment for the maintainers to do so as needed.
+
+## Review policy
+
+To ensure code quality and shared ownership, this review policy applies to all pull requests (PRs).
+
+### Default rule
+
+PRs should get at least two approvals (/lgtm or GitHub review approval) before merging.
+
+Notes:
+
+* Approvals should come from a maintainer, reviewer, or submodule owner familiar with the relevant code or area.
+* If there’s disagreement, maintainers should discuss and agree before merging.
+
+### Exceptions for Less Impactful PRs
+
+For low-risk changes — such as:
+
+* CI workflows
+* Documentation
+* Comments
+
+The rule can be relaxed:
+
+* One approval is generally enough.
+
+However:
+
+* If the author is a maintainer, they should still get approval from another maintainer, reviewer, or submodule owner, even for minor changes.
diff --git a/Makefile b/Makefile
index 802297166..4b44b672e 100644
--- a/Makefile
+++ b/Makefile
@@ -187,12 +187,9 @@ endif
 
 # Tools
 
-GOLANGCI_LINT_VERSION = $(shell cd tools/mod && go list -m -f {{.Version}} github.com/golangci/golangci-lint)
 .PHONY: install-golangci-lint
 install-golangci-lint:
-ifeq (, $(shell which golangci-lint))
-	$(shell curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin $(GOLANGCI_LINT_VERSION))
-endif
+	./scripts/verify_golangci-lint_version.sh
 
 .PHONY: install-lazyfs
 install-lazyfs: bin/lazyfs
diff --git a/OWNERS b/OWNERS
index 30037eb01..7783e529a 100644
--- a/OWNERS
+++ b/OWNERS
@@ -6,7 +6,19 @@ approvers:
   - jmhbnz           # James Blair <jablair@redhat.com> <mail@jamesblair.net>
   - serathius        # Marek Siarkowicz <siarkowicz@google.com> <marek.siarkowicz@gmail.com>
   - spzala           # Sahdev Zala <spzala@us.ibm.com>
-  - wenjiaswe        # Wenjia Zhang <wenjiazhang@google.com> <wenjia.swe@gmail.com>
 reviewers:
   - ivanvc           # Ivan Valdes <ivan@vald.es>
   - siyuanfoundation # Siyuan Zhang <sizhang@google.com> <physicsbug@gmail.com>
+emeritus_approvers:
+  - bdarnell         # Ben Darnell <ben@bendarnell.com>
+  - fanminshi        # Fanmin Shi <fanmin.shi@gmail.com>
+  - gyuho            # Gyuho Lee <gyuhox@gmail.com>
+  - hexfusion        # Sam Batschelet <sbatsche@redhat.com>
+  - heyitsanthony    # Anthony Romano <romanoanthony061@gmail.com>
+  - jingyih          # Jingyi Hu <jingyih@google.com>
+  - jpbetz           # Joe Betz <jpbetz@google.com>
+  - mitake           # Hitoshi Mitake <h.mitake@gmail.com>
+  - philips          # Brandon Philips <brandon@ifup.org>
+  - ptabor           # Piotr Tabor <piotr.tabor@gmail.com>
+  - wenjiaswe        # Wenjia Zhang <wenjiazhang@google.com> <wenjia.swe@gmail.com>
+  - xiang90          # Xiang Li <xiangli.cs@gmail.com>
diff --git a/README.md b/README.md
index 3927cd622..1b1623708 100644
--- a/README.md
+++ b/README.md
@@ -198,19 +198,7 @@ See [PR management](https://github.com/etcd-io/etcd/blob/main/Documentation/cont
 
 ## etcd Emeritus Maintainers
 
-These emeritus maintainers dedicated a part of their career to etcd and reviewed code, triaged bugs and pushed the project forward over a substantial period of time. Their contribution is greatly appreciated.
-
-* Fanmin Shi
-* Anthony Romano
-* Brandon Philips
-* Joe Betz
-* Gyuho Lee
-* Jingyi Hu
-* Xiang Li
-* Ben Darnell
-* Sam Batschelet
-* Piotr Tabor
-* Hitoshi Mitake
+etcd [emeritus maintainers](OWNERS) dedicated a part of their career to etcd and reviewed code, triaged bugs and pushed the project forward over a substantial period of time. Their contribution is greatly appreciated.
 
 ### License
 
diff --git a/api/etcdserverpb/gw/rpc.pb.gw.go b/api/etcdserverpb/gw/rpc.pb.gw.go
index 6ad1e9d9c..8eb910b6e 100644
--- a/api/etcdserverpb/gw/rpc.pb.gw.go
+++ b/api/etcdserverpb/gw/rpc.pb.gw.go
@@ -46,6 +46,9 @@ func request_KV_Range_0(ctx context.Context, marshaler runtime.Marshaler, client
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Range(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -70,6 +73,9 @@ func request_KV_Put_0(ctx context.Context, marshaler runtime.Marshaler, client e
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Put(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -94,6 +100,9 @@ func request_KV_DeleteRange_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.DeleteRange(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -118,6 +127,9 @@ func request_KV_Txn_0(ctx context.Context, marshaler runtime.Marshaler, client e
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Txn(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -142,6 +154,9 @@ func request_KV_Compact_0(ctx context.Context, marshaler runtime.Marshaler, clie
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Compact(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -209,6 +224,9 @@ func request_Lease_LeaseGrant_0(ctx context.Context, marshaler runtime.Marshaler
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseGrant(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -233,6 +251,9 @@ func request_Lease_LeaseRevoke_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseRevoke(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -257,6 +278,9 @@ func request_Lease_LeaseRevoke_1(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseRevoke(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -324,6 +348,9 @@ func request_Lease_LeaseTimeToLive_0(ctx context.Context, marshaler runtime.Mars
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseTimeToLive(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -348,6 +375,9 @@ func request_Lease_LeaseTimeToLive_1(ctx context.Context, marshaler runtime.Mars
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseTimeToLive(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -372,6 +402,9 @@ func request_Lease_LeaseLeases_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseLeases(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -396,6 +429,9 @@ func request_Lease_LeaseLeases_1(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.LeaseLeases(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -420,6 +456,9 @@ func request_Cluster_MemberAdd_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberAdd(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -444,6 +483,9 @@ func request_Cluster_MemberRemove_0(ctx context.Context, marshaler runtime.Marsh
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberRemove(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -468,6 +510,9 @@ func request_Cluster_MemberUpdate_0(ctx context.Context, marshaler runtime.Marsh
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberUpdate(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -492,6 +537,9 @@ func request_Cluster_MemberList_0(ctx context.Context, marshaler runtime.Marshal
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -516,6 +564,9 @@ func request_Cluster_MemberPromote_0(ctx context.Context, marshaler runtime.Mars
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MemberPromote(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -540,6 +591,9 @@ func request_Maintenance_Alarm_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Alarm(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -564,6 +618,9 @@ func request_Maintenance_Status_0(ctx context.Context, marshaler runtime.Marshal
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Status(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -588,6 +645,9 @@ func request_Maintenance_Defragment_0(ctx context.Context, marshaler runtime.Mar
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Defragment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -612,6 +672,9 @@ func request_Maintenance_Hash_0(ctx context.Context, marshaler runtime.Marshaler
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Hash(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -636,6 +699,9 @@ func request_Maintenance_HashKV_0(ctx context.Context, marshaler runtime.Marshal
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.HashKV(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -660,6 +726,9 @@ func request_Maintenance_Snapshot_0(ctx context.Context, marshaler runtime.Marsh
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	stream, err := client.Snapshot(ctx, &protoReq)
 	if err != nil {
 		return nil, metadata, err
@@ -680,6 +749,9 @@ func request_Maintenance_MoveLeader_0(ctx context.Context, marshaler runtime.Mar
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.MoveLeader(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -704,6 +776,9 @@ func request_Maintenance_Downgrade_0(ctx context.Context, marshaler runtime.Mars
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Downgrade(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -728,6 +803,9 @@ func request_Auth_AuthEnable_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.AuthEnable(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -752,6 +830,9 @@ func request_Auth_AuthDisable_0(ctx context.Context, marshaler runtime.Marshaler
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.AuthDisable(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -776,6 +857,9 @@ func request_Auth_AuthStatus_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.AuthStatus(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -800,6 +884,9 @@ func request_Auth_Authenticate_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Authenticate(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -824,6 +911,9 @@ func request_Auth_UserAdd_0(ctx context.Context, marshaler runtime.Marshaler, cl
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserAdd(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -848,6 +938,9 @@ func request_Auth_UserGet_0(ctx context.Context, marshaler runtime.Marshaler, cl
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserGet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -872,6 +965,9 @@ func request_Auth_UserList_0(ctx context.Context, marshaler runtime.Marshaler, c
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -896,6 +992,9 @@ func request_Auth_UserDelete_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserDelete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -920,6 +1019,9 @@ func request_Auth_UserChangePassword_0(ctx context.Context, marshaler runtime.Ma
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserChangePassword(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -944,6 +1046,9 @@ func request_Auth_UserGrantRole_0(ctx context.Context, marshaler runtime.Marshal
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserGrantRole(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -968,6 +1073,9 @@ func request_Auth_UserRevokeRole_0(ctx context.Context, marshaler runtime.Marsha
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.UserRevokeRole(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -992,6 +1100,9 @@ func request_Auth_RoleAdd_0(ctx context.Context, marshaler runtime.Marshaler, cl
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleAdd(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1016,6 +1127,9 @@ func request_Auth_RoleGet_0(ctx context.Context, marshaler runtime.Marshaler, cl
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleGet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1040,6 +1154,9 @@ func request_Auth_RoleList_0(ctx context.Context, marshaler runtime.Marshaler, c
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleList(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1064,6 +1181,9 @@ func request_Auth_RoleDelete_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleDelete(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1088,6 +1208,9 @@ func request_Auth_RoleGrantPermission_0(ctx context.Context, marshaler runtime.M
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleGrantPermission(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -1112,6 +1235,9 @@ func request_Auth_RoleRevokePermission_0(ctx context.Context, marshaler runtime.
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.RoleRevokePermission(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
diff --git a/api/go.mod b/api/go.mod
index 586b1e2d8..1198dfa56 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -2,15 +2,15 @@ module go.etcd.io/etcd/api/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/gogo/protobuf v1.3.2
 	github.com/golang/protobuf v1.5.4
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
 	github.com/stretchr/testify v1.10.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822
 	google.golang.org/grpc v1.73.0
 	google.golang.org/protobuf v1.36.6
 )
@@ -19,12 +19,12 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
-	go.opentelemetry.io/otel v1.36.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.36.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
 	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/api/go.sum b/api/go.sum
index 7a9abb298..f92a0d38b 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -2,8 +2,8 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr
 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -14,8 +14,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -32,16 +32,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -73,10 +73,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
 google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
diff --git a/bill-of-materials.json b/bill-of-materials.json
index 0fbec32c2..8a601abd9 100644
--- a/bill-of-materials.json
+++ b/bill-of-materials.json
@@ -665,6 +665,19 @@
 			}
 		]
 	},
+	{
+		"project": "go.yaml.in/yaml/v2",
+		"licenses": [
+			{
+				"type": "Apache License 2.0",
+				"confidence": 1
+			},
+			{
+				"type": "MIT License",
+				"confidence": 0.8975609756097561
+			}
+		]
+	},
 	{
 		"project": "golang.org/x/crypto",
 		"licenses": [
@@ -785,18 +798,5 @@
 				"confidence": 1
 			}
 		]
-	},
-	{
-		"project": "sigs.k8s.io/yaml/goyaml.v2",
-		"licenses": [
-			{
-				"type": "Apache License 2.0",
-				"confidence": 1
-			},
-			{
-				"type": "MIT License",
-				"confidence": 0.8975609756097561
-			}
-		]
 	}
 ]
diff --git a/cache/cache.go b/cache/cache.go
index 864002d5b..a021def59 100644
--- a/cache/cache.go
+++ b/cache/cache.go
@@ -1,4 +1,4 @@
-// Copyright 2015 The etcd Authors
+// Copyright 2025 The etcd Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -13,3 +13,216 @@
 // limitations under the License.
 
 package cache
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+
+	rpctypes "go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+// TODO: add gap-free replay for arbitrary startRevs and drop this guard.
+var ErrUnsupportedWatch = errors.New("cache: unsupported watch parameters")
+
+// Cache buffers a single etcd Watch for a given key‐prefix and fan‑outs local watchers.
+type Cache struct {
+	prefix      string // prefix is the key-prefix this shard is responsible for ("" = root).
+	cfg         Config // immutable runtime configuration
+	watcher     clientv3.Watcher
+	demux       *demux // demux fans incoming events out to active watchers and manages resync.
+	ready       chan struct{}
+	stop        context.CancelFunc
+	waitGroup   sync.WaitGroup
+	internalCtx context.Context
+}
+
+// watchCtx collects all the knobs that both serveWatchEvents and watchRetryLoop need.
+type watchCtx struct {
+	cache           *Cache
+	backoffStart    time.Duration
+	backoffMax      time.Duration
+	onFirstResponse func() // callback to fire once on first upstream response
+}
+
+// New builds a cache shard that watches only the requested prefix.
+// For the root cache pass "".
+func New(watcher clientv3.Watcher, prefix string, opts ...Option) (*Cache, error) {
+	cfg := defaultConfig()
+	for _, opt := range opts {
+		opt(&cfg)
+	}
+
+	if cfg.HistoryWindowSize <= 0 {
+		return nil, fmt.Errorf("invalid HistoryWindowSize %d (must be > 0)", cfg.HistoryWindowSize)
+	}
+
+	internalCtx, cancel := context.WithCancel(context.Background())
+
+	cache := &Cache{
+		prefix:      prefix,
+		cfg:         cfg,
+		watcher:     watcher,
+		ready:       make(chan struct{}),
+		stop:        cancel,
+		internalCtx: internalCtx,
+	}
+
+	cache.demux = NewDemux(internalCtx, &cache.waitGroup, cfg.HistoryWindowSize, cfg.ResyncInterval)
+
+	cache.waitGroup.Add(1)
+	go func() {
+		defer cache.waitGroup.Done()
+		readyOnce := sync.Once{}
+
+		watchCtx := &watchCtx{
+			cache:           cache,
+			backoffStart:    cfg.InitialBackoff,
+			backoffMax:      cfg.MaxBackoff,
+			onFirstResponse: func() { readyOnce.Do(func() { close(cache.ready) }) },
+		}
+		serveWatchEvents(internalCtx, watchCtx)
+	}()
+
+	return cache, nil
+}
+
+// Watch registers a cache-backed watcher for a given key or prefix.
+// It returns a WatchChan that streams WatchResponses containing events.
+func (c *Cache) Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan {
+	select {
+	case <-c.ready:
+	case <-ctx.Done():
+		emptyWatchChan := make(chan clientv3.WatchResponse)
+		close(emptyWatchChan)
+		return emptyWatchChan
+	}
+
+	op := clientv3.OpGet(key, opts...)
+	startRev := op.Rev()
+
+	// TODO: Support watch on subprefix and single key & arbitrary startRev support once we guarantee gap-free replay.
+	if key != c.prefix || !clientv3.IsOptsWithPrefix(opts) || startRev != 0 {
+		responseChan := make(chan clientv3.WatchResponse, 1)
+		responseChan <- clientv3.WatchResponse{Canceled: true}
+		close(responseChan)
+		return responseChan
+	}
+
+	w := newWatcher(c.cfg.PerWatcherBufferSize, func(k []byte) bool { return strings.HasPrefix(string(k), key) })
+	c.demux.Register(w, startRev)
+
+	responseChan := make(chan clientv3.WatchResponse)
+	c.waitGroup.Add(1)
+	go func() {
+		defer c.waitGroup.Done()
+		defer close(responseChan)
+		defer c.demux.Unregister(w)
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case <-c.internalCtx.Done():
+				return
+			case events, ok := <-w.eventQueue:
+				if !ok {
+					return
+				}
+				select {
+				case <-ctx.Done():
+					return
+				case <-c.internalCtx.Done():
+					return
+				case responseChan <- clientv3.WatchResponse{Events: events}:
+				}
+			}
+		}
+	}()
+	return responseChan
+}
+
+// Ready reports whether the cache has finished its initial load.
+func (c *Cache) Ready() bool {
+	select {
+	case <-c.ready:
+		return true
+	default:
+		return false
+	}
+}
+
+// WaitReady blocks until the cache is ready or the ctx is cancelled.
+func (c *Cache) WaitReady(ctx context.Context) error {
+	select {
+	case <-c.ready:
+		return nil
+	case <-ctx.Done():
+		return ctx.Err()
+	}
+}
+
+// Close cancels the private context and blocks until all goroutines return.
+func (c *Cache) Close() {
+	c.stop()
+	c.waitGroup.Wait()
+}
+
+func serveWatchEvents(ctx context.Context, watchCtx *watchCtx) {
+	backoff := watchCtx.backoffStart
+	for {
+		opts := []clientv3.OpOption{
+			clientv3.WithPrefix(),
+			clientv3.WithProgressNotify(),
+			clientv3.WithCreatedNotify(),
+		}
+		if oldestRev := watchCtx.cache.demux.PeekOldest(); oldestRev != 0 {
+			opts = append(opts,
+				clientv3.WithRev(oldestRev+1))
+		}
+		watchCh := watchCtx.cache.watcher.Watch(ctx, watchCtx.cache.prefix, opts...)
+
+		if err := readWatchChannel(watchCh, watchCtx.cache, watchCtx.cache.demux, watchCtx.onFirstResponse); err == nil {
+			return
+		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-time.After(backoff):
+		}
+		if backoff < watchCtx.backoffMax {
+			backoff *= 2
+		}
+	}
+}
+
+// readWatchChannel reads an etcd Watch stream into History and enqueueCh, returning nil on cancel or first error.
+func readWatchChannel(
+	watchChan clientv3.WatchChan,
+	cache *Cache,
+	demux *demux,
+	onFirstResponse func(),
+) error {
+	for resp := range watchChan {
+		onFirstResponse()
+
+		if err := resp.Err(); err != nil {
+			if errors.Is(err, rpctypes.ErrCompacted) {
+				select {
+				case <-cache.ready:
+					// TODO: Reinitialize cache.ready safely; current direct channel assignment can race with concurrent watchers
+					cache.ready = make(chan struct{})
+				default:
+				}
+				demux.Purge()
+			}
+			return err
+		}
+		demux.Broadcast(resp.Events)
+	}
+	return nil
+}
diff --git a/cache/config.go b/cache/config.go
new file mode 100644
index 000000000..ed0af99f4
--- /dev/null
+++ b/cache/config.go
@@ -0,0 +1,65 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import "time"
+
+type Config struct {
+	// PerWatcherBufferSize caps each watcher’s buffered channel.
+	// Bigger values tolerate brief client slow-downs at the cost of extra memory.
+	PerWatcherBufferSize int
+	// HistoryWindowSize is the max events kept in memory for replay.
+	// It defines how far back the cache can replay events to lagging watchers
+	HistoryWindowSize int
+	// ResyncInterval controls how often the demux attempts to catch a lagging watcher up by replaying events from History.
+	ResyncInterval time.Duration
+	// InitialBackoff is the first delay to wait before retrying an upstream etcd Watch after it ends with an error.
+	InitialBackoff time.Duration
+	// MaxBackoff caps the exponential back-off between successive upstream watch retries.
+	MaxBackoff time.Duration
+}
+
+// TODO: tune via performance/load tests.
+func defaultConfig() Config {
+	return Config{
+		PerWatcherBufferSize: 10,
+		HistoryWindowSize:    2048,
+		ResyncInterval:       50 * time.Millisecond,
+		InitialBackoff:       50 * time.Millisecond,
+		MaxBackoff:           2 * time.Second,
+	}
+}
+
+type Option func(*Config)
+
+func WithPerWatcherBufferSize(n int) Option {
+	return func(c *Config) { c.PerWatcherBufferSize = n }
+}
+
+func WithHistoryWindowSize(n int) Option {
+	return func(c *Config) { c.HistoryWindowSize = n }
+}
+
+func WithResyncInterval(d time.Duration) Option {
+	return func(c *Config) { c.ResyncInterval = d }
+}
+
+func WithInitialBackoff(d time.Duration) Option {
+	return func(c *Config) { c.InitialBackoff = d }
+}
+
+func WithMaxBackoff(d time.Duration) Option {
+	return func(c *Config) { c.MaxBackoff = d }
+}
diff --git a/cache/demux.go b/cache/demux.go
new file mode 100644
index 000000000..adbc495b6
--- /dev/null
+++ b/cache/demux.go
@@ -0,0 +1,185 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+type demux struct {
+	mu sync.RWMutex
+	// activeWatchers & laggingWatchers hold the first revision the watcher still needs (nextRev).
+	activeWatchers  map[*watcher]int64
+	laggingWatchers map[*watcher]int64
+	history         *ringBuffer
+	resyncInterval  time.Duration
+}
+
+func NewDemux(ctx context.Context, wg *sync.WaitGroup, historyWindowSize int, resyncInterval time.Duration) *demux {
+	d := newDemux(historyWindowSize, resyncInterval)
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		d.resyncLoop(ctx)
+	}()
+	return d
+}
+
+func newDemux(historyWindowSize int, resyncInterval time.Duration) *demux {
+	return &demux{
+		activeWatchers:  make(map[*watcher]int64),
+		laggingWatchers: make(map[*watcher]int64),
+		history:         newRingBuffer(historyWindowSize),
+		resyncInterval:  resyncInterval,
+	}
+}
+
+// resyncLoop periodically tries to catch lagging watchers up by replaying events from History.
+func (d *demux) resyncLoop(ctx context.Context) {
+	timer := time.NewTimer(d.resyncInterval)
+	defer timer.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-timer.C:
+			d.resyncLaggingWatchers()
+			timer.Reset(d.resyncInterval)
+		}
+	}
+}
+
+func (d *demux) Register(w *watcher, startingRev int64) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	latestRev := d.history.PeekLatest()
+
+	// Special case: 0 means “newest”.
+	if startingRev == 0 {
+		if latestRev == 0 {
+			d.activeWatchers[w] = 0
+			return
+		}
+		startingRev = latestRev + 1
+	}
+
+	if startingRev <= latestRev {
+		d.laggingWatchers[w] = startingRev
+	} else {
+		d.activeWatchers[w] = startingRev
+	}
+}
+
+func (d *demux) Unregister(w *watcher) {
+	func() {
+		d.mu.Lock()
+		defer d.mu.Unlock()
+		delete(d.activeWatchers, w)
+		delete(d.laggingWatchers, w)
+	}()
+	w.Stop()
+}
+
+func (d *demux) Broadcast(events []*clientv3.Event) {
+	if len(events) == 0 {
+		return
+	}
+
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	d.history.Append(events)
+
+	lastRev := events[len(events)-1].Kv.ModRevision
+	for w, nextRev := range d.activeWatchers {
+		start := len(events)
+		for i, ev := range events {
+			if ev.Kv.ModRevision >= nextRev {
+				start = i
+				break
+			}
+		}
+
+		if start == len(events) {
+			continue
+		}
+
+		if !w.enqueueEvent(events[start:]) { // overflow → lagging
+			d.laggingWatchers[w] = nextRev
+			delete(d.activeWatchers, w)
+		} else {
+			d.activeWatchers[w] = lastRev + 1
+		}
+	}
+}
+
+// Purge is called when etcd compaction invalidates our cached history, so clients should resubscribe.
+func (d *demux) Purge() {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.history.RebaseHistory()
+	for w := range d.activeWatchers {
+		w.Stop()
+	}
+	for w := range d.laggingWatchers {
+		w.Stop()
+	}
+	d.activeWatchers = make(map[*watcher]int64)
+	d.laggingWatchers = make(map[*watcher]int64)
+}
+
+func (d *demux) resyncLaggingWatchers() {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	oldestRev := d.history.PeekOldest()
+	for w, nextRev := range d.laggingWatchers {
+		if oldestRev != 0 && nextRev < oldestRev {
+			w.Stop()
+			delete(d.laggingWatchers, w)
+			continue
+		}
+		// TODO: re-enable key‐predicate in Filter when non‐zero startRev or performance tuning is needed
+		missed := d.history.Filter(nextRev)
+
+		enqueueFailed := false
+		for _, eventBatch := range missed {
+			if !w.enqueueEvent(eventBatch) { // buffer overflow: watcher still lagging
+				enqueueFailed = true
+				break
+			}
+			nextRev = eventBatch[0].Kv.ModRevision + 1
+		}
+
+		if !enqueueFailed {
+			delete(d.laggingWatchers, w)
+			d.activeWatchers[w] = nextRev
+		} else {
+			d.laggingWatchers[w] = nextRev
+		}
+	}
+}
+
+func (d *demux) PeekOldest() int64 {
+	d.mu.RLock()
+	defer d.mu.RUnlock()
+	return d.history.PeekOldest()
+}
diff --git a/cache/go.mod b/cache/go.mod
index 987b9b3d2..291983fc5 100644
--- a/cache/go.mod
+++ b/cache/go.mod
@@ -1,5 +1,36 @@
-module go.etcd.io/etcd/cache
+module go.etcd.io/etcd/cache/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
+
+require (
+	github.com/google/go-cmp v0.7.0
+	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
+	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
+)
+
+require (
+	github.com/coreos/go-semver v0.3.1 // indirect
+	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
+	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.27.0 // indirect
+	golang.org/x/net v0.41.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.26.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/grpc v1.73.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
+)
+
+replace (
+	go.etcd.io/etcd/api/v3 => ../api
+	go.etcd.io/etcd/client/pkg/v3 => ../client/pkg
+	go.etcd.io/etcd/client/v3 => ../client/v3
+)
diff --git a/cache/go.sum b/cache/go.sum
new file mode 100644
index 000000000..405176641
--- /dev/null
+++ b/cache/go.sum
@@ -0,0 +1,107 @@
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
+github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
+github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
+github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+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/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/cache/predicate.go b/cache/predicate.go
new file mode 100644
index 000000000..a9138bfc8
--- /dev/null
+++ b/cache/predicate.go
@@ -0,0 +1,25 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+type Prefix string
+
+func (prefix Prefix) Match(key []byte) bool {
+	if prefix == "" {
+		return true
+	}
+	prefixLen := len(prefix)
+	return len(key) >= prefixLen && string(key[:prefixLen]) == string(prefix)
+}
diff --git a/cache/ringbuffer.go b/cache/ringbuffer.go
new file mode 100644
index 000000000..b9d1681cb
--- /dev/null
+++ b/cache/ringbuffer.go
@@ -0,0 +1,115 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"fmt"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+type ringBuffer struct {
+	buffer []batch
+	// head is the index immediately after the last non-empty entry in the buffer (i.e., the next write position).
+	head, tail, size int
+}
+
+// batch groups all events that share one ModRevision.
+type batch struct {
+	rev    int64
+	events []*clientv3.Event
+}
+
+type KeyPredicate = func([]byte) bool
+
+func newRingBuffer(capacity int) *ringBuffer {
+	// assume capacity > 0 – validated by Cache
+	return &ringBuffer{
+		buffer: make([]batch, capacity),
+	}
+}
+
+func (r *ringBuffer) Append(events []*clientv3.Event) {
+	start := 0
+	for end := 1; end < len(events); end++ {
+		if events[end].Kv.ModRevision != events[start].Kv.ModRevision {
+			r.append(batch{
+				rev:    events[start].Kv.ModRevision,
+				events: events[start:end],
+			})
+			start = end
+		}
+	}
+	if start < len(events) {
+		r.append(batch{
+			rev:    events[start].Kv.ModRevision,
+			events: events[start:],
+		})
+	}
+}
+
+func (r *ringBuffer) append(b batch) {
+	if len(b.events) == 0 {
+		return
+	}
+	if r.size == len(r.buffer) {
+		r.tail = (r.tail + 1) % len(r.buffer)
+	} else {
+		r.size++
+	}
+	r.buffer[r.head] = b
+	r.head = (r.head + 1) % len(r.buffer)
+}
+
+// Filter returns all events in the buffer whose ModRevision is >= minRev.
+// TODO: use binary search on the ring buffer to locate the first entry >= nextRev instead of a full scan
+func (r *ringBuffer) Filter(minRev int64) (eventBatches [][]*clientv3.Event) {
+	eventBatches = make([][]*clientv3.Event, 0, r.size)
+
+	for n, i := 0, r.tail; n < r.size; n, i = n+1, (i+1)%len(r.buffer) {
+		eventBatch := r.buffer[i]
+		if eventBatch.events == nil {
+			panic(fmt.Sprintf("ringBuffer.Filter: unexpected nil eventBatch at index %d", i))
+		}
+		if eventBatch.rev >= minRev {
+			eventBatches = append(eventBatches, eventBatch.events)
+		}
+	}
+	return eventBatches
+}
+
+// PeekLatest returns the most recently-appended event (or nil if empty).
+func (r *ringBuffer) PeekLatest() int64 {
+	if r.size == 0 {
+		return 0
+	}
+	idx := (r.head - 1 + len(r.buffer)) % len(r.buffer)
+	return r.buffer[idx].rev
+}
+
+// PeekOldest returns the oldest event currently stored (or nil if empty).
+func (r *ringBuffer) PeekOldest() int64 {
+	if r.size == 0 {
+		return 0
+	}
+	return r.buffer[r.tail].rev
+}
+
+func (r *ringBuffer) RebaseHistory() {
+	r.head, r.tail, r.size = 0, 0, 0
+	for i := range r.buffer {
+		r.buffer[i] = batch{}
+	}
+}
diff --git a/cache/watcher.go b/cache/watcher.go
new file mode 100644
index 000000000..4269df3a0
--- /dev/null
+++ b/cache/watcher.go
@@ -0,0 +1,70 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"sync/atomic"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+// watcher holds one client’s buffered stream of events.
+type watcher struct {
+	eventQueue chan []*clientv3.Event
+	keyPred    KeyPredicate
+	stopped    int32
+	done       chan struct{} // closed together with Stop()
+}
+
+func newWatcher(bufSize int, pred KeyPredicate) *watcher {
+	return &watcher{
+		eventQueue: make(chan []*clientv3.Event, bufSize),
+		keyPred:    pred,
+		done:       make(chan struct{}),
+	}
+}
+
+// true  -> events delivered (or filtered/duplicate)
+// false -> buffer full (caller should mark watcher “lagging”)
+func (w *watcher) enqueueEvent(eventBatch []*clientv3.Event) bool {
+	if w.keyPred != nil {
+		filtered := make([]*clientv3.Event, 0, len(eventBatch))
+		for _, event := range eventBatch {
+			if w.keyPred(event.Kv.Key) {
+				filtered = append(filtered, event)
+			}
+		}
+		if len(filtered) == 0 {
+			return true
+		}
+		eventBatch = filtered
+	}
+	select {
+	case w.eventQueue <- eventBatch:
+		return true
+	default:
+		return false
+	}
+}
+
+// Stop closes the event channel atomically.
+func (w *watcher) Stop() {
+	if atomic.CompareAndSwapInt32(&w.stopped, 0, 1) {
+		close(w.eventQueue)
+		close(w.done)
+	}
+}
+
+func (w *watcher) Done() <-chan struct{} { return w.done }
diff --git a/client/pkg/go.mod b/client/pkg/go.mod
index b0e41bbf6..6c81e3237 100644
--- a/client/pkg/go.mod
+++ b/client/pkg/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/client/pkg/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 require (
 	github.com/coreos/go-systemd/v22 v22.5.0
diff --git a/client/v3/go.mod b/client/v3/go.mod
index 0cdca2780..3b00c7b29 100644
--- a/client/v3/go.mod
+++ b/client/v3/go.mod
@@ -2,19 +2,19 @@ module go.etcd.io/etcd/client/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/dustin/go-humanize v1.0.1
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
 	github.com/prometheus/client_golang v1.22.0
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.uber.org/zap v1.27.0
 	google.golang.org/grpc v1.73.0
-	sigs.k8s.io/yaml v1.4.0
+	sigs.k8s.io/yaml v1.5.0
 )
 
 require (
@@ -25,20 +25,22 @@ require (
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
-	go.opentelemetry.io/otel/metric v1.36.0 // indirect
-	go.opentelemetry.io/otel/trace v1.36.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+	go.opentelemetry.io/otel/trace v1.37.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/client/v3/go.sum b/client/v3/go.sum
index 251ab1e94..b6df4b801 100644
--- a/client/v3/go.sum
+++ b/client/v3/go.sum
@@ -10,8 +10,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -19,17 +19,16 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -48,8 +47,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/
 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -60,22 +59,26 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 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/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
+go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -107,10 +110,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
 google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
@@ -120,5 +123,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
-sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
+sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
diff --git a/etcdctl/ctlv3/command/check.go b/etcdctl/ctlv3/command/check.go
index 4626d5222..c88015400 100644
--- a/etcdctl/ctlv3/command/check.go
+++ b/etcdctl/ctlv3/command/check.go
@@ -182,7 +182,7 @@ func newCheckPerfCommand(cmd *cobra.Command, args []string) {
 	bar := pb.New(cfg.duration)
 	bar.Start()
 
-	r := report.NewReport("%4.4f")
+	r := report.NewReport("%4.4f", "", false)
 	var wg sync.WaitGroup
 
 	wg.Add(len(clients))
@@ -353,7 +353,7 @@ func newCheckDatascaleCommand(cmd *cobra.Command, args []string) {
 	ksize, vsize := 512, 512
 	k, v := make([]byte, ksize), string(make([]byte, vsize))
 
-	r := report.NewReport("%4.4f")
+	r := report.NewReport("%4.4f", "", false)
 	var wg sync.WaitGroup
 	wg.Add(len(clients))
 
diff --git a/etcdctl/ctlv3/command/printer_json.go b/etcdctl/ctlv3/command/printer_json.go
index cee458f3a..6b2d16b83 100644
--- a/etcdctl/ctlv3/command/printer_json.go
+++ b/etcdctl/ctlv3/command/printer_json.go
@@ -22,6 +22,7 @@ import (
 	"strconv"
 	"strings"
 
+	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
 	clientv3 "go.etcd.io/etcd/client/v3"
 )
 
@@ -31,6 +32,37 @@ type jsonPrinter struct {
 	printer
 }
 
+type (
+	HexResponseHeader pb.ResponseHeader
+	HexMember         pb.Member
+)
+
+func (h *HexResponseHeader) MarshalJSON() ([]byte, error) {
+	type Alias pb.ResponseHeader
+
+	return json.Marshal(&struct {
+		ClusterID string `json:"cluster_id"`
+		MemberID  string `json:"member_id"`
+		Alias
+	}{
+		ClusterID: fmt.Sprintf("%x", h.ClusterId),
+		MemberID:  fmt.Sprintf("%x", h.MemberId),
+		Alias:     (Alias)(*h),
+	})
+}
+
+func (m *HexMember) MarshalJSON() ([]byte, error) {
+	type Alias pb.Member
+
+	return json.Marshal(&struct {
+		ID string `json:"ID"`
+		Alias
+	}{
+		ID:    fmt.Sprintf("%x", m.ID),
+		Alias: (Alias)(*m),
+	})
+}
+
 func newJSONPrinter(isHex bool) printer {
 	return &jsonPrinter{
 		writer:  os.Stdout,
@@ -50,6 +82,7 @@ func (p *jsonPrinter) MemberList(r clientv3.MemberListResponse) {
 		printJSON(r)
 	}
 }
+func (p *jsonPrinter) MemberAdd(r clientv3.MemberAddResponse) { p.printJSON(r) }
 
 func printJSONTo(w io.Writer, v any) {
 	b, err := json.Marshal(v)
@@ -105,3 +138,40 @@ func printMemberListWithHexJSON(w io.Writer, r clientv3.MemberListResponse) {
 	buffer.WriteString("}")
 	fmt.Fprintln(w, buffer.String())
 }
+
+func (p *jsonPrinter) printJSON(v any) {
+	var data any
+	if !p.isHex {
+		printJSONTo(p.writer, v)
+		return
+	}
+
+	switch r := v.(type) {
+	case clientv3.MemberAddResponse:
+		type Alias clientv3.MemberAddResponse
+
+		data = &struct {
+			Header  *HexResponseHeader `json:"header"`
+			Member  *HexMember         `json:"member"`
+			Members []*HexMember       `json:"members"`
+			*Alias
+		}{
+			Header:  (*HexResponseHeader)(r.Header),
+			Member:  (*HexMember)(r.Member),
+			Members: toHexMembers(r.Members),
+			Alias:   (*Alias)(&r),
+		}
+	default:
+		data = v
+	}
+
+	printJSONTo(p.writer, data)
+}
+
+func toHexMembers(members []*pb.Member) []*HexMember {
+	hexMembers := make([]*HexMember, len(members))
+	for i, member := range members {
+		hexMembers[i] = (*HexMember)(member)
+	}
+	return hexMembers
+}
diff --git a/etcdctl/go.mod b/etcdctl/go.mod
index 7e00b5408..162339694 100644
--- a/etcdctl/go.mod
+++ b/etcdctl/go.mod
@@ -2,13 +2,13 @@ module go.etcd.io/etcd/etcdctl/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 require (
 	github.com/bgentry/speakeasy v0.2.0
 	github.com/cheggaaa/pb/v3 v3.1.7
 	github.com/dustin/go-humanize v1.0.1
-	github.com/olekukonko/tablewriter v1.0.7
+	github.com/olekukonko/tablewriter v1.0.8
 	github.com/spf13/cobra v1.9.1
 	github.com/spf13/pflag v1.0.6
 	github.com/stretchr/testify v1.10.0
@@ -27,9 +27,10 @@ require (
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/fatih/color v1.18.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
@@ -42,8 +43,8 @@ require (
 	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/etcdctl/go.sum b/etcdctl/go.sum
index f0b331b49..7d12d4a8e 100644
--- a/etcdctl/go.sum
+++ b/etcdctl/go.sum
@@ -19,8 +19,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
 github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -32,12 +32,12 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -58,16 +58,16 @@ github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hS
 github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
 github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
 github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
-github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
+github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -86,16 +86,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -136,10 +136,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
 google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
diff --git a/etcdutl/README.md b/etcdutl/README.md
index 06cf85d7e..ead1b41cc 100644
--- a/etcdutl/README.md
+++ b/etcdutl/README.md
@@ -1,5 +1,4 @@
-etcdutl
-========
+# etcdutl
 
 `etcdutl` is a command line administration utility for [etcd][etcd].
 
@@ -8,7 +7,7 @@ For operations over a network, please use `etcdctl`.
 
 ### DEFRAG [options]
 
-DEFRAG directly defragments an etcd data directory while etcd is not running. 
+DEFRAG directly defragments an etcd data directory while etcd is not running.
 When an etcd member reclaims storage space from deleted and compacted keys, the space is kept in a free list and the database file remains the same size. By defragmenting the database, the etcd member releases this free space back to the file system.
 
 In order to defrag a live etcd instances over the network, please use `etcdctl defrag` instead.
@@ -36,7 +35,6 @@ To defragment a data directory directly, use the `--data-dir` flag:
 
 DEFRAG returns a zero exit code only if it succeeded in defragmenting all given endpoints.
 
-
 ### SNAPSHOT RESTORE [options] \<filename\>
 
 SNAPSHOT RESTORE creates an etcd data directory for an etcd cluster member from a backend database snapshot and a new cluster configuration. Restoring the snapshot into each member for a new cluster configuration will initialize a new etcd cluster preloaded by the snapshot data.
@@ -174,6 +172,74 @@ Prints etcd version and API version.
 # API version: 3.1
 ```
 
+### LIST-BUCKET [options] \<data dir or db file path\>
+
+`list-bucket` prints all bucket names.
+
+#### Flags
+
+- timeout -- Time to wait to obtain a file lock on db file, 0 to block indefinitely.
+
+##### Examples for LIST-BUCKET
+
+```bash
+
+$ ./etcdutl list-bucket ~/tmp/etcd/default.etcd/member/snap/db
+alarm
+auth
+authRoles
+authUsers
+cluster
+key
+lease
+members
+members_removed
+meta
+```
+
+### ITERATE-BUCKET [options] \<data dir or db file path\> \<bucket name\>
+
+`iterate-bucket` lists key-value pairs in a given bucket in reverse order.
+
+#### Flags for ITERATE-BUCKET
+
+- timeout -- Time to wait to obtain a file lock on db file, 0 to block indefinitely.
+- limit -- Max number of key-value pairs to iterate (0 to iterate all).
+- decode -- true to decode Protocol Buffer encoded data.
+
+##### Examples for ITERATE-BUCKET
+
+```bash
+
+# with `--decode` option
+$ ./etcdutl iterate-bucket ~/tmp/etcd/default.etcd/member/snap/db key --decode
+rev={Revision:{Main:4 Sub:0} tombstone:false}, value=[key "k1" | val "v3" | created 2 | mod 4 | ver 3]
+rev={Revision:{Main:3 Sub:0} tombstone:false}, value=[key "k1" | val "v2" | created 2 | mod 3 | ver 2]
+rev={Revision:{Main:2 Sub:0} tombstone:false}, value=[key "k1" | val "v1" | created 2 | mod 2 | ver 1]
+
+# without `--decode` option
+$ ./etcdutl iterate-bucket ~/tmp/etcd/default.etcd/member/snap/db key
+key="\x00\x00\x00\x00\x00\x00\x00\x04_\x00\x00\x00\x00\x00\x00\x00\x00", value="\n\x02k1\x10\x02\x18\x04 \x03*\x02v3"
+key="\x00\x00\x00\x00\x00\x00\x00\x03_\x00\x00\x00\x00\x00\x00\x00\x00", value="\n\x02k1\x10\x02\x18\x03 \x02*\x02v2"
+key="\x00\x00\x00\x00\x00\x00\x00\x02_\x00\x00\x00\x00\x00\x00\x00\x00", value="\n\x02k1\x10\x02\x18\x02 \x01*\x02v1"
+```
+
+### HASH [options] \<data dir or db file path\>
+
+`hash` prints the hash of the db file.
+
+#### Flags for HASH
+
+- timeout -- Time to wait to obtain a file lock on db file, 0 to block indefinitely.
+
+##### Examples for HASH
+
+```bash
+
+$ ./etcdutl hash ~/tmp/etcd/default.etcd/member/snap/db
+db path: /Users/wachao/tmp/etcd/default.etcd/member/snap/db
+Hash: 4031086527
+```
 
 ## Exit codes
 
diff --git a/etcdutl/ctl.go b/etcdutl/ctl.go
index 8418729bf..5e91c14cf 100644
--- a/etcdutl/ctl.go
+++ b/etcdutl/ctl.go
@@ -45,6 +45,9 @@ func init() {
 		etcdutl.NewVersionCommand(),
 		etcdutl.NewCompletionCommand(),
 		etcdutl.NewMigrateCommand(),
+		etcdutl.NewListBucketCommand(),
+		etcdutl.NewIterateBucketCommand(),
+		etcdutl.NewHashCommand(),
 	)
 }
 
diff --git a/etcdutl/etcdutl/bucket_command.go b/etcdutl/etcdutl/bucket_command.go
new file mode 100644
index 000000000..ba2461e6d
--- /dev/null
+++ b/etcdutl/etcdutl/bucket_command.go
@@ -0,0 +1,276 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package etcdutl
+
+import (
+	"encoding/binary"
+	"fmt"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/spf13/cobra"
+	"go.uber.org/zap"
+
+	bolt "go.etcd.io/bbolt"
+	"go.etcd.io/etcd/api/v3/authpb"
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	"go.etcd.io/etcd/client/pkg/v3/fileutil"
+	"go.etcd.io/etcd/server/v3/lease/leasepb"
+	"go.etcd.io/etcd/server/v3/storage/backend"
+	"go.etcd.io/etcd/server/v3/storage/datadir"
+	"go.etcd.io/etcd/server/v3/storage/mvcc"
+	"go.etcd.io/etcd/server/v3/storage/schema"
+)
+
+var (
+	// TODO: add this configure to top level etcdutl command
+	flockTimeout        time.Duration
+	iterateBucketLimit  uint64
+	iterateBucketDecode bool
+)
+
+func NewListBucketCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list-bucket [data dir or db file path]",
+		Short: "bucket lists all buckets.",
+		Args:  cobra.ExactArgs(1),
+		Run:   listBucketCommandFunc,
+	}
+
+	// TODO: add this flag to top level etctutl command
+	cmd.PersistentFlags().DurationVar(&flockTimeout, "timeout", 10*time.Second, "time to wait to obtain a file lock on db file, 0 to block indefinitely")
+
+	return cmd
+}
+
+func NewIterateBucketCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "iterate-bucket [data dir or db file path] [bucket name]",
+		Short: "iterate-bucket lists key-value pairs in reverse order.",
+		Args:  cobra.ExactArgs(2),
+		Run:   iterateBucketCommandFunc,
+	}
+
+	// TODO: add this flag to top level etctutl command
+	cmd.PersistentFlags().DurationVar(&flockTimeout, "timeout", 10*time.Second, "time to wait to obtain a file lock on db file, 0 to block indefinitely")
+	cmd.PersistentFlags().Uint64Var(&iterateBucketLimit, "limit", 0, "max number of key-value pairs to iterate (0 to iterate all)")
+	cmd.PersistentFlags().BoolVar(&iterateBucketDecode, "decode", false, "true to decode Protocol Buffer encoded data")
+
+	return cmd
+}
+
+func NewHashCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "hash [data dir or db file path]",
+		Short: "hash computes the hash of db file.",
+		Args:  cobra.ExactArgs(1),
+		Run:   getHashCommandFunc,
+	}
+
+	// TODO: add this flag to top level etctutl command
+	cmd.PersistentFlags().DurationVar(&flockTimeout, "timeout", 10*time.Second, "time to wait to obtain a file lock on db file, 0 to block indefinitely")
+
+	return cmd
+}
+
+func listBucketCommandFunc(_ *cobra.Command, args []string) {
+	lg := GetLogger()
+	dp := args[0]
+	if !strings.HasSuffix(dp, "db") {
+		dp = filepath.Join(datadir.ToSnapDir(dp), "db")
+	}
+
+	if !fileutil.Exist(dp) {
+		lg.Fatal("db file not exist", zap.String("path", dp))
+	}
+
+	bts, err := getBuckets(dp)
+	if err != nil {
+		lg.Fatal("Failed to get buckets", zap.Error(err))
+	}
+	for _, b := range bts {
+		fmt.Println(b)
+	}
+}
+
+func getBuckets(dbPath string) (buckets []string, err error) {
+	db, derr := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: flockTimeout})
+	if derr != nil {
+		return nil, fmt.Errorf("failed to open bolt DB %w", derr)
+	}
+	defer db.Close()
+
+	err = db.View(func(tx *bolt.Tx) error {
+		return tx.ForEach(func(b []byte, _ *bolt.Bucket) error {
+			buckets = append(buckets, string(b))
+			return nil
+		})
+	})
+	return buckets, err
+}
+
+func iterateBucketCommandFunc(_ *cobra.Command, args []string) {
+	lg := GetLogger()
+	dp := args[0]
+	if !strings.HasSuffix(dp, "db") {
+		dp = filepath.Join(datadir.ToSnapDir(dp), "db")
+	}
+	if !fileutil.Exist(dp) {
+		lg.Fatal("db file not exist", zap.String("path", dp))
+	}
+	bucket := args[1]
+	err := iterateBucket(dp, bucket, iterateBucketLimit, iterateBucketDecode)
+	if err != nil {
+		lg.Fatal("Failed to iterate bucket", zap.Error(err))
+	}
+}
+
+type decoder func(k, v []byte)
+
+// key is the bucket name, and value is the function to decode K/V in the bucket.
+var decoders = map[string]decoder{
+	"key":       keyDecoder,
+	"lease":     leaseDecoder,
+	"auth":      authDecoder,
+	"authRoles": authRolesDecoder,
+	"authUsers": authUsersDecoder,
+	"meta":      metaDecoder,
+}
+
+func defaultDecoder(k, v []byte) {
+	fmt.Printf("key=%q, value=%q\n", k, v)
+}
+
+func keyDecoder(k, v []byte) {
+	rev := mvcc.BytesToBucketKey(k)
+	var kv mvccpb.KeyValue
+	if err := kv.Unmarshal(v); err != nil {
+		panic(err)
+	}
+	fmt.Printf("rev=%+v, value=[key %q | val %q | created %d | mod %d | ver %d]\n", rev, string(kv.Key), string(kv.Value), kv.CreateRevision, kv.ModRevision, kv.Version)
+}
+
+func bytesToLeaseID(bytes []byte) int64 {
+	if len(bytes) != 8 {
+		panic(fmt.Errorf("lease ID must be 8-byte"))
+	}
+	return int64(binary.BigEndian.Uint64(bytes))
+}
+
+func leaseDecoder(k, v []byte) {
+	leaseID := bytesToLeaseID(k)
+	var lpb leasepb.Lease
+	if err := lpb.Unmarshal(v); err != nil {
+		panic(err)
+	}
+	fmt.Printf("lease ID=%016x, TTL=%ds, remaining TTL=%ds\n", leaseID, lpb.TTL, lpb.RemainingTTL)
+}
+
+func authDecoder(k, v []byte) {
+	if string(k) == "authRevision" {
+		rev := binary.BigEndian.Uint64(v)
+		fmt.Printf("key=%q, value=%v\n", k, rev)
+	} else {
+		fmt.Printf("key=%q, value=%v\n", k, v)
+	}
+}
+
+func authRolesDecoder(_, v []byte) {
+	role := &authpb.Role{}
+	err := role.Unmarshal(v)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("role=%q, keyPermission=%v\n", string(role.Name), role.KeyPermission)
+}
+
+func authUsersDecoder(_, v []byte) {
+	user := &authpb.User{}
+	err := user.Unmarshal(v)
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("user=%q, roles=%q, option=%v\n", user.Name, user.Roles, user.Options)
+}
+
+func metaDecoder(k, v []byte) {
+	if string(k) == string(schema.MetaConsistentIndexKeyName) || string(k) == string(schema.MetaTermKeyName) {
+		fmt.Printf("key=%q, value=%v\n", k, binary.BigEndian.Uint64(v))
+	} else if string(k) == string(schema.ScheduledCompactKeyName) || string(k) == string(schema.FinishedCompactKeyName) {
+		rev := mvcc.BytesToRev(v)
+		fmt.Printf("key=%q, value=%v\n", k, rev)
+	} else {
+		defaultDecoder(k, v)
+	}
+}
+
+func iterateBucket(dbPath, bucket string, limit uint64, decode bool) (err error) {
+	db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: flockTimeout})
+	if err != nil {
+		return fmt.Errorf("failed to open bolt DB %w", err)
+	}
+	defer db.Close()
+
+	err = db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(bucket))
+		if b == nil {
+			return fmt.Errorf("got nil bucket for %s", bucket)
+		}
+
+		c := b.Cursor()
+
+		// iterate in reverse order (use First() and Next() for ascending order)
+		for k, v := c.Last(); k != nil; k, v = c.Prev() {
+			// TODO: remove sensitive information
+			// (https://github.com/etcd-io/etcd/issues/7620)
+			if dec, ok := decoders[bucket]; decode && ok {
+				dec(k, v)
+			} else {
+				defaultDecoder(k, v)
+			}
+
+			limit--
+			if limit == 0 {
+				break
+			}
+		}
+
+		return nil
+	})
+	return err
+}
+
+func getHashCommandFunc(_ *cobra.Command, args []string) {
+	lg := GetLogger()
+	dp := args[0]
+	if !strings.HasSuffix(dp, "db") {
+		dp = filepath.Join(datadir.ToSnapDir(dp), "db")
+	}
+	if !fileutil.Exist(dp) {
+		lg.Fatal("db file not exist", zap.String("path", dp))
+	}
+
+	hash, err := getHash(dp)
+	if err != nil {
+		lg.Fatal("failed to get hash", zap.Error(err))
+	}
+	fmt.Printf("db path: %s\nHash: %d\n", dp, hash)
+}
+
+func getHash(dbPath string) (hash uint32, err error) {
+	b := backend.NewDefaultBackend(zap.NewNop(), dbPath)
+	return b.Hash(schema.DefaultIgnores)
+}
diff --git a/etcdutl/go.mod b/etcdutl/go.mod
index 8163e3884..8900cf8f1 100644
--- a/etcdutl/go.mod
+++ b/etcdutl/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/etcdutl/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 replace (
 	go.etcd.io/etcd/api/v3 => ../api
@@ -23,10 +23,10 @@ replace (
 require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/dustin/go-humanize v1.0.1
-	github.com/olekukonko/tablewriter v1.0.7
+	github.com/olekukonko/tablewriter v1.0.8
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
-	go.etcd.io/bbolt v1.4.1
+	go.etcd.io/bbolt v1.4.2
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
@@ -43,7 +43,7 @@ require (
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/fatih/color v1.18.0 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
@@ -52,9 +52,9 @@ require (
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jonboulle/clockwork v0.5.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
@@ -66,7 +66,7 @@ require (
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_golang v1.22.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
@@ -75,25 +75,26 @@ require (
 	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
-	go.opentelemetry.io/otel v1.36.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
-	go.opentelemetry.io/otel/metric v1.36.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.36.0 // indirect
-	go.opentelemetry.io/otel/trace v1.36.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+	go.opentelemetry.io/otel/trace v1.37.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/crypto v0.39.0 // indirect
 	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
 	golang.org/x/time v0.12.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/grpc v1.73.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/yaml v1.4.0 // indirect
+	sigs.k8s.io/yaml v1.5.0 // indirect
 )
diff --git a/etcdutl/go.sum b/etcdutl/go.sum
index c606cecf2..6970f5d92 100644
--- a/etcdutl/go.sum
+++ b/etcdutl/go.sum
@@ -20,8 +20,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
 github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -33,19 +33,18 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
@@ -72,8 +71,8 @@ github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hS
 github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
 github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
 github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
-github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
+github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -81,8 +80,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/
 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -109,28 +108,28 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.4.1 h1:5mOV+HWjIPLEAlUGMsveaUvK2+byZMFOzojoi7bh7uI=
-go.etcd.io/bbolt v1.4.1/go.mod h1:c8zu2BnXWTu2XM4XcICtbGSl9cFwsXtcf9zLt2OncM8=
+go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
+go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
 go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
 go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -139,6 +138,10 @@ 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/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
+go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -179,10 +182,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
 google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
@@ -195,5 +198,5 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
-sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
+sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
diff --git a/go.mod b/go.mod
index 69ace4ec1..7b4a499bf 100644
--- a/go.mod
+++ b/go.mod
@@ -2,10 +2,11 @@ module go.etcd.io/etcd/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 replace (
 	go.etcd.io/etcd/api/v3 => ./api
+	go.etcd.io/etcd/cache/v3 => ./cache
 	go.etcd.io/etcd/client/pkg/v3 => ./client/pkg
 	go.etcd.io/etcd/client/v3 => ./client/v3
 	go.etcd.io/etcd/etcdctl/v3 => ./etcdctl
@@ -22,7 +23,7 @@ require (
 	github.com/dustin/go-humanize v1.0.1
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
-	go.etcd.io/bbolt v1.4.1
+	go.etcd.io/bbolt v1.4.2
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
@@ -46,7 +47,7 @@ require (
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/fatih/color v1.18.0 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
@@ -57,9 +58,9 @@ require (
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jonboulle/clockwork v0.5.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
@@ -68,11 +69,11 @@ require (
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
 	github.com/olekukonko/ll v0.0.8 // indirect
-	github.com/olekukonko/tablewriter v1.0.7 // indirect
+	github.com/olekukonko/tablewriter v1.0.8 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_golang v1.22.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
@@ -82,22 +83,23 @@ require (
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.etcd.io/gofail v0.2.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
-	go.opentelemetry.io/otel v1.36.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
-	go.opentelemetry.io/otel/metric v1.36.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.36.0 // indirect
-	go.opentelemetry.io/otel/trace v1.36.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+	go.opentelemetry.io/otel/trace v1.37.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/crypto v0.39.0 // indirect
 	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/yaml v1.4.0 // indirect
+	sigs.k8s.io/yaml v1.5.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 787f045af..1b173ca20 100644
--- a/go.sum
+++ b/go.sum
@@ -37,8 +37,8 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -59,7 +59,6 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -68,12 +67,12 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
@@ -102,8 +101,8 @@ github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hS
 github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
 github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
 github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
-github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
+github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -114,8 +113,8 @@ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -146,30 +145,30 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.4.1 h1:5mOV+HWjIPLEAlUGMsveaUvK2+byZMFOzojoi7bh7uI=
-go.etcd.io/bbolt v1.4.1/go.mod h1:c8zu2BnXWTu2XM4XcICtbGSl9cFwsXtcf9zLt2OncM8=
+go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
+go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
 go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA=
 go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o=
 go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
 go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -181,6 +180,10 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
+go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -243,10 +246,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -267,5 +270,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
-sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
+sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
diff --git a/pkg/go.mod b/pkg/go.mod
index c52c7eb8e..4ff77b548 100644
--- a/pkg/go.mod
+++ b/pkg/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/pkg/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 require (
 	github.com/creack/pty v1.1.18
@@ -11,6 +11,7 @@ require (
 	github.com/spf13/pflag v1.0.6
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
+	go.opentelemetry.io/otel/trace v1.37.0
 	go.uber.org/zap v1.27.0
 	google.golang.org/grpc v1.73.0
 )
@@ -20,13 +21,13 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	go.opentelemetry.io/otel v1.36.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.36.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/pkg/go.sum b/pkg/go.sum
index 99e2c9ce6..f44d0ce78 100644
--- a/pkg/go.sum
+++ b/pkg/go.sum
@@ -7,8 +7,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -37,16 +37,16 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -59,8 +59,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
 golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
 google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
diff --git a/pkg/report/perfdash.go b/pkg/report/perfdash.go
new file mode 100644
index 000000000..2fe9f8dad
--- /dev/null
+++ b/pkg/report/perfdash.go
@@ -0,0 +1,87 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package report
+
+import (
+	"encoding/json"
+	"fmt"
+	"math"
+	"os"
+	"path/filepath"
+	"time"
+)
+
+type Metrics struct {
+	Perc50 float64 `json:"Perc50"`
+	Perc90 float64 `json:"Perc90"`
+	Perc99 float64 `json:"Perc99"`
+}
+
+type Labels struct {
+	Metric string `json:"Metric"`
+}
+
+type DataItem struct {
+	Data   Metrics `json:"data"`
+	Labels Labels  `json:"labels"`
+	Unit   string  `json:"unit"`
+}
+
+type perfdashFormattedReport struct {
+	Version   string     `json:"version"`
+	DataItems []DataItem `json:"dataItems"`
+}
+
+func (r *report) writePerfDashReport(reportName string) {
+	pcls, data := Percentiles(r.stats.Lats)
+	pclsData := make(map[float64]float64)
+	for i := 0; i < len(pcls); i++ {
+		pclsData[pcls[i]] = data[i] * 1000 // Since the reported data is in seconds, convert to ms.
+	}
+	report := perfdashFormattedReport{
+		Version: "v1",
+		DataItems: []DataItem{
+			{
+				Data: Metrics{
+					Perc50: math.Round(pclsData[50]*10000) / 10000,
+					Perc90: math.Round(pclsData[90]*10000) / 10000,
+					Perc99: math.Round(pclsData[99]*10000) / 10000,
+				},
+				Unit: "ms",
+				Labels: Labels{
+					Metric: "APIResponsiveness",
+				},
+			},
+		},
+	}
+	reportB, _ := json.MarshalIndent(report, "", "  ")
+
+	artifactsDir := os.Getenv("ARTIFACTS")
+	if artifactsDir == "" {
+		artifactsDir = "./_artifacts"
+	}
+
+	fileName := fmt.Sprintf("etcd_perf_%s_%s.json", reportName, time.Now().UTC().Format(time.RFC3339))
+	err := os.MkdirAll(artifactsDir, 0o755)
+	if err != nil {
+		fmt.Println("Error creating artifacts directory:", err)
+	}
+	destPath := filepath.Join(artifactsDir, fileName)
+	err = os.WriteFile(destPath, reportB, 0o644)
+	if err != nil {
+		fmt.Println("Error writing to file:", err)
+	}
+	fmt.Println("Successfully created a JSON perf report at", destPath)
+}
diff --git a/pkg/report/report.go b/pkg/report/report.go
index a33f97cfa..3f578e9f1 100644
--- a/pkg/report/report.go
+++ b/pkg/report/report.go
@@ -41,8 +41,10 @@ type Result struct {
 func (res *Result) Duration() time.Duration { return res.End.Sub(res.Start) }
 
 type report struct {
-	results   chan Result
-	precision string
+	generatePerfReport bool
+	reportName         string
+	precision          string
+	results            chan Result
 
 	stats Stats
 	sps   *secondPoints
@@ -81,19 +83,23 @@ type Report interface {
 	Stats() <-chan Stats
 }
 
-func NewReport(precision string) Report { return newReport(precision) }
+func NewReport(precision, reportName string, generatePerfReport bool) Report {
+	return newReport(precision, reportName, generatePerfReport)
+}
 
-func newReport(precision string) *report {
+func newReport(precision, reportName string, generatePerfReport bool) *report {
 	r := &report{
-		results:   make(chan Result, 16),
-		precision: precision,
+		results:            make(chan Result, 16),
+		precision:          precision,
+		generatePerfReport: generatePerfReport,
+		reportName:         reportName,
 	}
 	r.stats.ErrorDist = make(map[string]int)
 	return r
 }
 
-func NewReportSample(precision string) Report {
-	r := NewReport(precision).(*report)
+func NewReportSample(precision, reportName string, generatePerfReport bool) Report {
+	r := NewReport(precision, reportName, generatePerfReport).(*report)
 	r.sps = newSecondPoints()
 	return r
 }
@@ -105,6 +111,9 @@ func (r *report) Run() <-chan string {
 	go func() {
 		defer close(donec)
 		r.processResults()
+		if r.generatePerfReport {
+			r.writePerfDashReport(r.reportName)
+		}
 		donec <- r.String()
 	}()
 	return donec
@@ -155,8 +164,8 @@ func (r *report) sec2str(sec float64) string { return fmt.Sprintf(r.precision+"
 
 type reportRate struct{ *report }
 
-func NewReportRate(precision string) Report {
-	return &reportRate{NewReport(precision).(*report)}
+func NewReportRate(precision, reportName string, generatePerfReport bool) Report {
+	return &reportRate{NewReport(precision, reportName, generatePerfReport).(*report)}
 }
 
 func (r *reportRate) String() string {
diff --git a/pkg/report/weighted.go b/pkg/report/weighted.go
index 411214f6d..a06b59374 100644
--- a/pkg/report/weighted.go
+++ b/pkg/report/weighted.go
@@ -30,10 +30,10 @@ type weightedReport struct {
 
 // NewWeightedReport returns a report that includes
 // both weighted and unweighted statistics.
-func NewWeightedReport(r Report, precision string) Report {
+func NewWeightedReport(r Report, precision, reportName string, generatePerfReport bool) Report {
 	return &weightedReport{
 		baseReport: r,
-		report:     newReport(precision),
+		report:     newReport(precision, reportName, generatePerfReport),
 		results:    make(chan Result, 16),
 	}
 }
diff --git a/pkg/traceutil/trace.go b/pkg/traceutil/trace.go
index abf5cf1d2..f89ba83c9 100644
--- a/pkg/traceutil/trace.go
+++ b/pkg/traceutil/trace.go
@@ -22,9 +22,19 @@ import (
 	"strings"
 	"time"
 
+	"go.opentelemetry.io/otel/trace"
+	"go.opentelemetry.io/otel/trace/noop"
 	"go.uber.org/zap"
 )
 
+const instrumentationScope = "go.etcd.io/etcd"
+
+var Tracer trace.Tracer = noop.NewTracerProvider().Tracer(instrumentationScope)
+
+func Init(tp trace.TracerProvider) {
+	Tracer = tp.Tracer(instrumentationScope)
+}
+
 // TraceKey is used as a key of context for Trace.
 type TraceKey struct{}
 
@@ -72,7 +82,7 @@ type step struct {
 	isSubTraceEnd   bool
 }
 
-func New(op string, lg *zap.Logger, fields ...Field) *Trace {
+func newTrace(op string, lg *zap.Logger, fields ...Field) *Trace {
 	return &Trace{operation: op, lg: lg, startTime: time.Now(), fields: fields}
 }
 
@@ -88,6 +98,19 @@ func Get(ctx context.Context) *Trace {
 	return TODO()
 }
 
+// EnsureTrace creates a new trace if needed and adds it to the context.
+func EnsureTrace(ctx context.Context, lg *zap.Logger, operation string, fields ...Field) (context.Context, *Trace) {
+	trace := Get(ctx)
+	if trace.IsEmpty() {
+		trace = newTrace(operation,
+			lg,
+			fields...,
+		)
+		ctx = context.WithValue(ctx, TraceKey{}, trace)
+	}
+	return ctx, trace
+}
+
 func (t *Trace) GetStartTime() time.Time {
 	return t.startTime
 }
diff --git a/scripts/verify_golangci-lint_version.sh b/scripts/verify_golangci-lint_version.sh
new file mode 100755
index 000000000..3df538116
--- /dev/null
+++ b/scripts/verify_golangci-lint_version.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+function install_golangci_lint() {
+    echo "Installing golangci-lint ${GOLANGCI_LINT_VERSION}"
+    curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${GOPATH}/bin" "${GOLANGCI_LINT_VERSION}"
+}
+
+GOLANGCI_LINT_VERSION=$(cd tools/mod && go list -m -f '{{.Version}}' github.com/golangci/golangci-lint)
+echo "golangci-lint version: $GOLANGCI_LINT_VERSION"
+GOLANGCI_LINT_PRESENT=$(which golangci-lint)
+
+if [ -z "$GOLANGCI_LINT_PRESENT" ]; then
+    echo "golangci-lint is not available"
+    install_golangci_lint
+    exit 0
+fi
+GOLANGCI_LINT_INSTALLED=v$(golangci-lint version | grep -oP 'version \K[0-9.]+')
+
+if [ "$GOLANGCI_LINT_VERSION" != "$GOLANGCI_LINT_INSTALLED" ]; then
+    echo "different golangci-lint version installed: $GOLANGCI_LINT_INSTALLED"
+    install_golangci_lint
+    echo "golangci-lint version: $GOLANGCI_LINT_VERSION"
+fi
diff --git a/security/README.md b/security/README.md
index 3129778ca..99193d1dd 100644
--- a/security/README.md
+++ b/security/README.md
@@ -6,7 +6,7 @@ Join the [etcd-dev](https://groups.google.com/g/etcd-dev) group for emails about
 
 We’re extremely grateful for security researchers and users that report vulnerabilities to the etcd Open Source Community. All reports are thoroughly investigated by a dedicated committee of community volunteers called [Product Security Committee](security-release-process.md#product-security-committee).
 
-To make a report, please email the private [etcd maintainers](mailto:etcd-maintainers@googlegroups.com) list with the security details and the details expected for [all etcd bug reports](https://github.com/etcd-io/etcd/blob/main/Documentation/contributor-guide/reporting_bugs.md).
+To make a report, please email the private [security@etcd.io](mailto:security@etcd.io) list with the security details and the details expected for [all etcd bug reports](https://github.com/etcd-io/etcd/blob/main/Documentation/contributor-guide/reporting_bugs.md).
 
 ### When Should I Report a Vulnerability?
 
@@ -43,6 +43,6 @@ This list provides actionable information regarding etcd security to multiple di
 
 ### Request to Join
 
-New membership requests are sent to [etcd maintainers](mailto:etcd-maintainers@googlegroups.com).
+New membership requests are sent to [security@etcd.io](mailto:security@etcd.io).
 
 File an issue [here](https://github.com/etcd-io/etcd/issues/new?template=distributors-application.md), filling in the criteria template.
diff --git a/security/email-templates.md b/security/email-templates.md
index 8d93f76d5..766613716 100644
--- a/security/email-templates.md
+++ b/security/email-templates.md
@@ -7,6 +7,7 @@ This is a collection of email templates to handle various situations the securit
 ```
 Subject: Upcoming security release of etcd $VERSION
 To: etcd-dev@googlegroups.com
+Cc: security@etcd-io
 Cc: etcd-maintainers@googlegroups.com
 
 Hello etcd Community,
@@ -34,6 +35,7 @@ $PERSON on behalf of the etcd Product Security Committee and maintainers
 ```
 Subject: Security release of etcd $VERSION is now available
 To: etcd-dev@googlegroups.com
+Cc: security@etcd-io
 Cc: etcd-maintainers@googlegroups.com
 
 Hello etcd Community,
diff --git a/security/security-release-process.md b/security/security-release-process.md
index 78fb55dd9..3dcb606a4 100644
--- a/security/security-release-process.md
+++ b/security/security-release-process.md
@@ -22,7 +22,7 @@ The PSC members will share various tasks as listed below:
 
 ### Contacting the Product Security Committee
 
-Contact the team by sending email to [etcd maintainers](mailto:etcd-maintainers@googlegroups.com).
+Contact the team by sending email to [security@etcd.io](mailto:security@etcd.io).
 
 ### Product Security Committee Membership
 
@@ -55,7 +55,7 @@ The etcd Community asks that all suspected vulnerabilities be privately and resp
 
 ### Public Disclosure Processes
 
-If anyone knows of a publicly disclosed security vulnerability please IMMEDIATELY email [etcd maintainers](mailto:etcd-maintainers@googlegroups.com) to inform the PSC about the vulnerability so they may start the patch, release, and communication process.
+If anyone knows of a publicly disclosed security vulnerability please IMMEDIATELY email [security@etcd.io](mailto:security@etcd.io) to inform the PSC about the vulnerability so they may start the patch, release, and communication process.
 
 If possible the PSC will ask the person making the public report if the issue can be handled via a private disclosure process. If the reporter denies the PSC will move swiftly with the fix and release process. In extreme cases GitHub can be asked to delete the issue but this generally isn't necessary and is unlikely to make a public disclosure less damaging.
 
@@ -90,7 +90,7 @@ If the CVSS score is under ~4.0
 
 Note: CVSS is convenient but imperfect. Ultimately, the PSC has discretion on classifying the severity of a vulnerability.
 
-The severity of the bug and related handling decisions must be discussed on the [etcd maintainers](mailto:etcd-maintainers@googlegroups.com) mailing list.
+The severity of the bug and related handling decisions must be discussed on the [security@etcd.io](mailto:security@etcd.io) mailing list.
 
 ### Fix Disclosure Process
 
diff --git a/server/embed/config_tracing.go b/server/embed/config_tracing.go
index 0ca90fdc5..7fa985a75 100644
--- a/server/embed/config_tracing.go
+++ b/server/embed/config_tracing.go
@@ -25,6 +25,8 @@ import (
 	tracesdk "go.opentelemetry.io/otel/sdk/trace"
 	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
 	"go.uber.org/zap"
+
+	"go.etcd.io/etcd/pkg/v3/traceutil"
 )
 
 const maxSamplingRatePerMillion = 1000000
@@ -81,6 +83,8 @@ func newTracingExporter(ctx context.Context, cfg *Config) (*tracingExporter, err
 		),
 	)
 
+	traceutil.Init(traceProvider)
+
 	options := []otelgrpc.Option{
 		otelgrpc.WithPropagators(
 			propagation.NewCompositeTextMapPropagator(
diff --git a/server/etcdserver/api/v3election/v3electionpb/gw/v3election.pb.gw.go b/server/etcdserver/api/v3election/v3electionpb/gw/v3election.pb.gw.go
index 912149f5a..0f4f23d67 100644
--- a/server/etcdserver/api/v3election/v3electionpb/gw/v3election.pb.gw.go
+++ b/server/etcdserver/api/v3election/v3electionpb/gw/v3election.pb.gw.go
@@ -46,6 +46,9 @@ func request_Election_Campaign_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Campaign(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -70,6 +73,9 @@ func request_Election_Proclaim_0(ctx context.Context, marshaler runtime.Marshale
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Proclaim(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -94,6 +100,9 @@ func request_Election_Leader_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Leader(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -118,6 +127,9 @@ func request_Election_Observe_0(ctx context.Context, marshaler runtime.Marshaler
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	stream, err := client.Observe(ctx, &protoReq)
 	if err != nil {
 		return nil, metadata, err
@@ -138,6 +150,9 @@ func request_Election_Resign_0(ctx context.Context, marshaler runtime.Marshaler,
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Resign(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
diff --git a/server/etcdserver/api/v3lock/v3lockpb/gw/v3lock.pb.gw.go b/server/etcdserver/api/v3lock/v3lockpb/gw/v3lock.pb.gw.go
index 5efb75939..836214a0f 100644
--- a/server/etcdserver/api/v3lock/v3lockpb/gw/v3lock.pb.gw.go
+++ b/server/etcdserver/api/v3lock/v3lockpb/gw/v3lock.pb.gw.go
@@ -46,6 +46,9 @@ func request_Lock_Lock_0(ctx context.Context, marshaler runtime.Marshaler, clien
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Lock(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
@@ -70,6 +73,9 @@ func request_Lock_Unlock_0(ctx context.Context, marshaler runtime.Marshaler, cli
 	if err := marshaler.NewDecoder(req.Body).Decode(protov1.MessageV2(&protoReq)); err != nil && !errors.Is(err, io.EOF) {
 		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
 	}
+	if req.Body != nil {
+		_, _ = io.Copy(io.Discard, req.Body)
+	}
 	msg, err := client.Unlock(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
 	return protov1.MessageV2(msg), metadata, err
 }
diff --git a/server/etcdserver/api/v3rpc/watch.go b/server/etcdserver/api/v3rpc/watch.go
index b0a7e4a19..d4a5bc351 100644
--- a/server/etcdserver/api/v3rpc/watch.go
+++ b/server/etcdserver/api/v3rpc/watch.go
@@ -305,12 +305,7 @@ func (sws *serverWatchStream) recvLoop() error {
 
 			filters := FiltersFromRequest(creq)
 
-			wsrev := sws.watchStream.Rev()
-			rev := creq.StartRevision
-			if rev == 0 {
-				rev = wsrev + 1
-			}
-			id, err := sws.watchStream.Watch(mvcc.WatchID(creq.WatchId), creq.Key, creq.RangeEnd, rev, filters...)
+			id, err := sws.watchStream.Watch(mvcc.WatchID(creq.WatchId), creq.Key, creq.RangeEnd, creq.StartRevision, filters...)
 			if err == nil {
 				sws.mu.Lock()
 				if creq.ProgressNotify {
@@ -328,7 +323,7 @@ func (sws *serverWatchStream) recvLoop() error {
 			}
 
 			wr := &pb.WatchResponse{
-				Header:   sws.newResponseHeader(wsrev),
+				Header:   sws.newResponseHeader(sws.watchStream.Rev()),
 				WatchId:  int64(id),
 				Created:  true,
 				Canceled: err != nil,
diff --git a/server/etcdserver/apply/apply.go b/server/etcdserver/apply/apply.go
index ff454973d..9948d2a39 100644
--- a/server/etcdserver/apply/apply.go
+++ b/server/etcdserver/apply/apply.go
@@ -161,8 +161,7 @@ func (a *applierV3backend) Txn(rt *pb.TxnRequest) (*pb.TxnResponse, *traceutil.T
 func (a *applierV3backend) Compaction(compaction *pb.CompactionRequest) (*pb.CompactionResponse, <-chan struct{}, *traceutil.Trace, error) {
 	resp := &pb.CompactionResponse{}
 	resp.Header = &pb.ResponseHeader{}
-	trace := traceutil.New("compact",
-		a.options.Logger,
+	ctx, trace := traceutil.EnsureTrace(context.TODO(), a.options.Logger, "compact",
 		traceutil.Field{Key: "revision", Value: compaction.Revision},
 	)
 
@@ -171,7 +170,7 @@ func (a *applierV3backend) Compaction(compaction *pb.CompactionRequest) (*pb.Com
 		return nil, ch, nil, err
 	}
 	// get the current revision. which key to get is not important.
-	rr, _ := a.options.KV.Range(context.TODO(), []byte("compaction"), nil, mvcc.RangeOptions{})
+	rr, _ := a.options.KV.Range(ctx, []byte("compaction"), nil, mvcc.RangeOptions{})
 	resp.Header.Revision = rr.Rev
 	return resp, ch, trace, err
 }
diff --git a/server/etcdserver/txn/delete.go b/server/etcdserver/txn/delete.go
index 7d1218e8c..f1baa5122 100644
--- a/server/etcdserver/txn/delete.go
+++ b/server/etcdserver/txn/delete.go
@@ -26,7 +26,7 @@ import (
 )
 
 func DeleteRange(ctx context.Context, lg *zap.Logger, kv mvcc.KV, dr *pb.DeleteRangeRequest) (resp *pb.DeleteRangeResponse, trace *traceutil.Trace, err error) {
-	ctx, trace = ensureTrace(ctx, lg, "delete_range",
+	ctx, trace = traceutil.EnsureTrace(ctx, lg, "delete_range",
 		traceutil.Field{Key: "key", Value: string(dr.Key)},
 		traceutil.Field{Key: "range_end", Value: string(dr.RangeEnd)},
 	)
diff --git a/server/etcdserver/txn/put.go b/server/etcdserver/txn/put.go
index 8c304cc59..90768d740 100644
--- a/server/etcdserver/txn/put.go
+++ b/server/etcdserver/txn/put.go
@@ -27,7 +27,7 @@ import (
 )
 
 func Put(ctx context.Context, lg *zap.Logger, lessor lease.Lessor, kv mvcc.KV, p *pb.PutRequest) (resp *pb.PutResponse, trace *traceutil.Trace, err error) {
-	ctx, trace = ensureTrace(ctx, lg, "put",
+	ctx, trace = traceutil.EnsureTrace(ctx, lg, "put",
 		traceutil.Field{Key: "key", Value: string(p.Key)},
 		traceutil.Field{Key: "req_size", Value: p.Size()},
 	)
diff --git a/server/etcdserver/txn/range.go b/server/etcdserver/txn/range.go
index 5988691aa..2ffc920e2 100644
--- a/server/etcdserver/txn/range.go
+++ b/server/etcdserver/txn/range.go
@@ -29,7 +29,7 @@ import (
 )
 
 func Range(ctx context.Context, lg *zap.Logger, kv mvcc.KV, r *pb.RangeRequest) (resp *pb.RangeResponse, trace *traceutil.Trace, err error) {
-	ctx, trace = ensureTrace(ctx, lg, "range")
+	ctx, trace = traceutil.EnsureTrace(ctx, lg, "range")
 	defer func(start time.Time) {
 		success := err == nil
 		RangeSecObserve(success, time.Since(start))
diff --git a/server/etcdserver/txn/txn.go b/server/etcdserver/txn/txn.go
index b0d568d28..3917fbb88 100644
--- a/server/etcdserver/txn/txn.go
+++ b/server/etcdserver/txn/txn.go
@@ -30,7 +30,7 @@ import (
 )
 
 func Txn(ctx context.Context, lg *zap.Logger, rt *pb.TxnRequest, txnModeWriteWithSharedBuffer bool, kv mvcc.KV, lessor lease.Lessor) (txnResp *pb.TxnResponse, trace *traceutil.Trace, err error) {
-	ctx, trace = ensureTrace(ctx, lg, "transaction")
+	ctx, trace = traceutil.EnsureTrace(ctx, lg, "transaction")
 	isWrite := !IsTxnReadonly(rt)
 	// When the transaction contains write operations, we use ReadTx instead of
 	// ConcurrentReadTx to avoid extra overhead of copying buffer.
@@ -406,15 +406,3 @@ func checkTxnReqsPermission(as auth.AuthStore, ai *auth.AuthInfo, reqs []*pb.Req
 
 	return nil
 }
-
-func ensureTrace(ctx context.Context, lg *zap.Logger, operation string, fields ...traceutil.Field) (context.Context, *traceutil.Trace) {
-	trace := traceutil.Get(ctx)
-	if trace.IsEmpty() {
-		trace = traceutil.New(operation,
-			lg,
-			fields...,
-		)
-		ctx = context.WithValue(ctx, traceutil.TraceKey{}, trace)
-	}
-	return ctx, trace
-}
diff --git a/server/etcdserver/v3_server.go b/server/etcdserver/v3_server.go
index c6953604a..fb8f88aad 100644
--- a/server/etcdserver/v3_server.go
+++ b/server/etcdserver/v3_server.go
@@ -24,6 +24,8 @@ import (
 	"time"
 
 	"github.com/gogo/protobuf/proto"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
 	"go.uber.org/zap"
 	"golang.org/x/crypto/bcrypt"
 
@@ -102,12 +104,20 @@ type Authenticator interface {
 }
 
 func (s *EtcdServer) Range(ctx context.Context, r *pb.RangeRequest) (*pb.RangeResponse, error) {
-	trace := traceutil.New("range",
-		s.Logger(),
+	var span trace.Span
+	ctx, span = traceutil.Tracer.Start(ctx, "range", trace.WithAttributes(
+		attribute.String("range_begin", string(r.GetKey())),
+		attribute.String("range_end", string(r.GetRangeEnd())),
+		attribute.Int64("rev", r.GetRevision()),
+		attribute.Int64("limit", r.GetLimit()),
+		attribute.Bool("count_only", r.GetCountOnly()),
+	))
+	defer span.End()
+
+	ctx, trace := traceutil.EnsureTrace(ctx, s.Logger(), "range",
 		traceutil.Field{Key: "range_begin", Value: string(r.Key)},
 		traceutil.Field{Key: "range_end", Value: string(r.RangeEnd)},
 	)
-	ctx = context.WithValue(ctx, traceutil.TraceKey{}, trace)
 
 	var resp *pb.RangeResponse
 	var err error
@@ -158,13 +168,34 @@ func (s *EtcdServer) DeleteRange(ctx context.Context, r *pb.DeleteRangeRequest)
 	return resp.(*pb.DeleteRangeResponse), nil
 }
 
+// firstCompareKey returns first non-empty key in the list of comparison operations.
+func firstCompareKey(c []*pb.Compare) string {
+	for _, op := range c {
+		key := string(op.GetKey())
+		if key != "" {
+			return key
+		}
+	}
+	return ""
+}
+
 func (s *EtcdServer) Txn(ctx context.Context, r *pb.TxnRequest) (*pb.TxnResponse, error) {
-	if txn.IsTxnReadonly(r) {
-		trace := traceutil.New("transaction",
-			s.Logger(),
-			traceutil.Field{Key: "read_only", Value: true},
-		)
-		ctx = context.WithValue(ctx, traceutil.TraceKey{}, trace)
+	readOnly := txn.IsTxnReadonly(r)
+
+	var span trace.Span
+	ctx, span = traceutil.Tracer.Start(ctx, "txn", trace.WithAttributes(
+		attribute.String("compare_first_key", firstCompareKey(r.GetCompare())),
+		attribute.Int("compare_len", len(r.GetCompare())),
+		attribute.Int("success_len", len(r.GetSuccess())),
+		attribute.Int("failure_len", len(r.GetFailure())),
+		attribute.Bool("read_only", readOnly),
+	))
+	defer span.End()
+
+	ctx, trace := traceutil.EnsureTrace(ctx, s.Logger(), "transaction",
+		traceutil.Field{Key: "read_only", Value: readOnly},
+	)
+	if readOnly {
 		if !txn.IsTxnSerializable(r) {
 			err := s.linearizableReadNotify(ctx)
 			trace.Step("agreement among raft nodes before linearized reading")
@@ -202,8 +233,8 @@ func (s *EtcdServer) Txn(ctx context.Context, r *pb.TxnRequest) (*pb.TxnResponse
 
 func (s *EtcdServer) Compact(ctx context.Context, r *pb.CompactionRequest) (*pb.CompactionResponse, error) {
 	startTime := time.Now()
+	ctx, trace := traceutil.EnsureTrace(ctx, s.Logger(), "compact")
 	result, err := s.processInternalRaftRequestOnce(ctx, pb.InternalRaftRequest{Compaction: r})
-	trace := traceutil.TODO()
 	if result != nil && result.Trace != nil {
 		trace = result.Trace
 		defer func() {
@@ -816,7 +847,7 @@ func (s *EtcdServer) linearizableReadLoop() {
 
 		// as a single loop is can unlock multiple reads, it is not very useful
 		// to propagate the trace from Txn or Range.
-		trace := traceutil.New("linearizableReadLoop", s.Logger())
+		_, trace := traceutil.EnsureTrace(context.Background(), s.Logger(), "linearizableReadLoop")
 
 		nextnr := newNotifier()
 		s.readMu.Lock()
diff --git a/server/go.mod b/server/go.mod
index 21fa33b14..a352ea830 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/server/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 require (
 	github.com/coreos/go-semver v0.3.1
@@ -15,8 +15,8 @@ require (
 	github.com/google/btree v1.1.3
 	github.com/google/go-cmp v0.7.0
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
 	github.com/jonboulle/clockwork v0.5.0
 	github.com/prometheus/client_golang v1.22.0
 	github.com/prometheus/client_model v0.6.2
@@ -25,25 +25,26 @@ require (
 	github.com/stretchr/testify v1.10.0
 	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2
-	go.etcd.io/bbolt v1.4.1
+	go.etcd.io/bbolt v1.4.2
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0
 	go.etcd.io/raft/v3 v3.6.0
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0
-	go.opentelemetry.io/otel v1.36.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0
-	go.opentelemetry.io/otel/sdk v1.36.0
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0
+	go.opentelemetry.io/otel v1.37.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
+	go.opentelemetry.io/otel/sdk v1.37.0
+	go.opentelemetry.io/otel/trace v1.37.0
 	go.uber.org/zap v1.27.0
 	golang.org/x/crypto v0.39.0
 	golang.org/x/net v0.41.0
 	golang.org/x/time v0.12.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822
 	google.golang.org/grpc v1.73.0
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
-	sigs.k8s.io/yaml v1.4.0
+	sigs.k8s.io/yaml v1.5.0
 )
 
 require (
@@ -51,7 +52,7 @@ require (
 	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
@@ -60,19 +61,19 @@ require (
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
-	go.opentelemetry.io/otel/metric v1.36.0 // indirect
-	go.opentelemetry.io/otel/trace v1.36.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/server/go.sum b/server/go.sum
index 2feb16d75..5a12fe416 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -29,8 +29,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -51,7 +51,6 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -60,12 +59,12 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
@@ -94,8 +93,8 @@ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -123,28 +122,28 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.4.1 h1:5mOV+HWjIPLEAlUGMsveaUvK2+byZMFOzojoi7bh7uI=
-go.etcd.io/bbolt v1.4.1/go.mod h1:c8zu2BnXWTu2XM4XcICtbGSl9cFwsXtcf9zLt2OncM8=
+go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
+go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
 go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
 go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -156,6 +155,10 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
+go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -217,10 +220,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -241,5 +244,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
-sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
+sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
diff --git a/server/storage/mvcc/watchable_store.go b/server/storage/mvcc/watchable_store.go
index e309b5cfa..67b2d7f2d 100644
--- a/server/storage/mvcc/watchable_store.go
+++ b/server/storage/mvcc/watchable_store.go
@@ -15,12 +15,14 @@
 package mvcc
 
 import (
+	"fmt"
 	"sync"
 	"time"
 
 	"go.uber.org/zap"
 
 	"go.etcd.io/etcd/api/v3/mvccpb"
+	"go.etcd.io/etcd/client/pkg/v3/verify"
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/pkg/v3/traceutil"
 	"go.etcd.io/etcd/server/v3/lease"
@@ -37,6 +39,9 @@ var (
 
 	// maxWatchersPerSync is the number of watchers to sync in a single batch
 	maxWatchersPerSync = 512
+
+	// maxResyncPeriod is the period of executing resync.
+	watchResyncPeriod = 100 * time.Millisecond
 )
 
 func ChanBufLen() int { return chanBufLen }
@@ -122,12 +127,13 @@ func (s *watchableStore) NewWatchStream() WatchStream {
 
 func (s *watchableStore) watch(key, end []byte, startRev int64, id WatchID, ch chan<- WatchResponse, fcs ...FilterFunc) (*watcher, cancelFunc) {
 	wa := &watcher{
-		key:    key,
-		end:    end,
-		minRev: startRev,
-		id:     id,
-		ch:     ch,
-		fcs:    fcs,
+		key:      key,
+		end:      end,
+		startRev: startRev,
+		minRev:   startRev,
+		id:       id,
+		ch:       ch,
+		fcs:      fcs,
 	}
 
 	s.mu.Lock()
@@ -218,8 +224,7 @@ func (s *watchableStore) Restore(b backend.Backend) error {
 func (s *watchableStore) syncWatchersLoop() {
 	defer s.wg.Done()
 
-	waitDuration := 100 * time.Millisecond
-	delayTicker := time.NewTicker(waitDuration)
+	delayTicker := time.NewTicker(watchResyncPeriod)
 	defer delayTicker.Stop()
 	var evs []mvccpb.Event
 
@@ -235,7 +240,7 @@ func (s *watchableStore) syncWatchersLoop() {
 		}
 		syncDuration := time.Since(st)
 
-		delayTicker.Reset(waitDuration)
+		delayTicker.Reset(watchResyncPeriod)
 		// more work pending?
 		if unsyncedWatchers != 0 && lastUnsyncedWatchers > unsyncedWatchers {
 			// be fair to other store operations by yielding time taken
@@ -367,7 +372,7 @@ func (s *watchableStore) syncWatchers(evs []mvccpb.Event) (int, []mvccpb.Event)
 			// Next retry of syncWatchers would try to resend the compacted watch response to w.ch
 			continue
 		}
-		w.minRev = curRev + 1
+		w.minRev = max(curRev+1, w.minRev)
 
 		eb, ok := wb[w]
 		if !ok {
@@ -532,11 +537,15 @@ func (s *watchableStore) progressIfSync(watchers map[WatchID]*watcher, responseW
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
+	rev := s.rev()
 	// Any watcher unsynced?
 	for _, w := range watchers {
 		if _, ok := s.synced.watchers[w]; !ok {
 			return false
 		}
+		if rev < w.startRev {
+			return false
+		}
 	}
 
 	// If all watchers are synchronised, send out progress
@@ -545,7 +554,7 @@ func (s *watchableStore) progressIfSync(watchers map[WatchID]*watcher, responseW
 	// notification will be broadcasted client-side if required
 	// (see dispatchEvent in client/v3/watch.go)
 	for _, w := range watchers {
-		w.send(WatchResponse{WatchID: responseWatchID, Revision: s.rev()})
+		w.send(WatchResponse{WatchID: responseWatchID, Revision: rev})
 		return true
 	}
 	return true
@@ -572,6 +581,7 @@ type watcher struct {
 	// except when the watcher were to be moved from "synced" watcher group
 	restore bool
 
+	startRev int64
 	// minRev is the minimum revision update the watcher will accept
 	minRev int64
 	id     WatchID
@@ -602,6 +612,16 @@ func (w *watcher) send(wr WatchResponse) bool {
 		wr.Events = ne
 	}
 
+	verify.Verify(func() {
+		if w.startRev > 0 {
+			for _, ev := range wr.Events {
+				if ev.Kv.ModRevision < w.startRev {
+					panic(fmt.Sprintf("Event.ModRevision(%d) is less than the w.startRev(%d) for watchID: %d", ev.Kv.ModRevision, w.startRev, w.id))
+				}
+			}
+		}
+	})
+
 	// if all events are filtered out, we should send nothing.
 	if !progressEvent && len(wr.Events) == 0 {
 		return true
diff --git a/tools/benchmark/cmd/lease.go b/tools/benchmark/cmd/lease.go
index 5ed3dbe4b..941c98828 100644
--- a/tools/benchmark/cmd/lease.go
+++ b/tools/benchmark/cmd/lease.go
@@ -40,14 +40,14 @@ func init() {
 	leaseKeepaliveCmd.Flags().IntVar(&leaseKeepaliveTotal, "total", 10000, "Total number of lease keepalive requests")
 }
 
-func leaseKeepaliveFunc(_ *cobra.Command, _ []string) {
+func leaseKeepaliveFunc(cmd *cobra.Command, _ []string) {
 	requests := make(chan struct{})
 	clients := mustCreateClients(totalClients, totalConns)
 
 	bar = pb.New(leaseKeepaliveTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go func(c v3.Lease) {
diff --git a/tools/benchmark/cmd/mvcc-put.go b/tools/benchmark/cmd/mvcc-put.go
index 75793a326..b6a6379c2 100644
--- a/tools/benchmark/cmd/mvcc-put.go
+++ b/tools/benchmark/cmd/mvcc-put.go
@@ -69,7 +69,7 @@ func createBytesSlice(bytesN, sliceN int) [][]byte {
 	return rs
 }
 
-func mvccPutFunc(_ *cobra.Command, _ []string) {
+func mvccPutFunc(cmd *cobra.Command, _ []string) {
 	if cpuProfPath != "" {
 		f, err := os.Create(cpuProfPath)
 		if err != nil {
@@ -105,7 +105,7 @@ func mvccPutFunc(_ *cobra.Command, _ []string) {
 	vals := createBytesSlice(valueSize, mvccTotalRequests*nrTxnOps)
 
 	weight := float64(nrTxnOps)
-	r := newWeightedReport()
+	r := newWeightedReport(cmd.Name())
 	rrc := r.Results()
 
 	rc := r.Run()
diff --git a/tools/benchmark/cmd/put.go b/tools/benchmark/cmd/put.go
index 62b9a3dc8..7f66046f2 100644
--- a/tools/benchmark/cmd/put.go
+++ b/tools/benchmark/cmd/put.go
@@ -88,7 +88,7 @@ func putFunc(cmd *cobra.Command, _ []string) {
 	bar = pb.New(putTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go func(c *v3.Client) {
diff --git a/tools/benchmark/cmd/range.go b/tools/benchmark/cmd/range.go
index 111d69bd0..6bbed8ac3 100644
--- a/tools/benchmark/cmd/range.go
+++ b/tools/benchmark/cmd/range.go
@@ -86,7 +86,7 @@ func rangeFunc(cmd *cobra.Command, args []string) {
 	bar = pb.New(rangeTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go func(c *v3.Client) {
diff --git a/tools/benchmark/cmd/root.go b/tools/benchmark/cmd/root.go
index c85beb9db..95aadde05 100644
--- a/tools/benchmark/cmd/root.go
+++ b/tools/benchmark/cmd/root.go
@@ -55,6 +55,8 @@ var (
 
 	targetLeader     bool
 	autoSyncInterval time.Duration
+
+	generatePerfReport bool
 )
 
 func init() {
@@ -74,4 +76,6 @@ func init() {
 
 	RootCmd.PersistentFlags().BoolVar(&targetLeader, "target-leader", false, "connect only to the leader node")
 	RootCmd.PersistentFlags().DurationVar(&autoSyncInterval, "auto-sync-interval", time.Duration(0), "AutoSyncInterval is the interval to update endpoints with its latest members")
+
+	RootCmd.PersistentFlags().BoolVar(&generatePerfReport, "report-perfdash", false, "Generate benchmark report in perfdash format")
 }
diff --git a/tools/benchmark/cmd/stm.go b/tools/benchmark/cmd/stm.go
index d6dfba0d3..22dc88657 100644
--- a/tools/benchmark/cmd/stm.go
+++ b/tools/benchmark/cmd/stm.go
@@ -110,7 +110,7 @@ func stmFunc(cmd *cobra.Command, _ []string) {
 	bar = pb.New(stmTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go doSTM(clients[i], requests, r.Results())
diff --git a/tools/benchmark/cmd/txn_mixed.go b/tools/benchmark/cmd/txn_mixed.go
index ffc004ecf..bbd88d599 100644
--- a/tools/benchmark/cmd/txn_mixed.go
+++ b/tools/benchmark/cmd/txn_mixed.go
@@ -95,8 +95,8 @@ func mixedTxnFunc(cmd *cobra.Command, _ []string) {
 	bar = pb.New(mixedTxnTotal)
 	bar.Start()
 
-	reportRead := newReport()
-	reportWrite := newReport()
+	reportRead := newReport(cmd.Name() + "-read")
+	reportWrite := newReport(cmd.Name() + "-write")
 	for i := range clients {
 		wg.Add(1)
 		go func(c *v3.Client) {
diff --git a/tools/benchmark/cmd/txn_put.go b/tools/benchmark/cmd/txn_put.go
index d69293b83..5e2d9d7a4 100644
--- a/tools/benchmark/cmd/txn_put.go
+++ b/tools/benchmark/cmd/txn_put.go
@@ -55,7 +55,7 @@ func init() {
 	txnPutCmd.Flags().IntVar(&keySpaceSize, "key-space-size", 1, "Maximum possible keys")
 }
 
-func txnPutFunc(_ *cobra.Command, _ []string) {
+func txnPutFunc(cmd *cobra.Command, _ []string) {
 	if keySpaceSize <= 0 {
 		fmt.Fprintf(os.Stderr, "expected positive --key-space-size, got (%v)", keySpaceSize)
 		os.Exit(1)
@@ -78,7 +78,7 @@ func txnPutFunc(_ *cobra.Command, _ []string) {
 	bar = pb.New(txnPutTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport(cmd.Name())
 	for i := range clients {
 		wg.Add(1)
 		go func(c *v3.Client) {
diff --git a/tools/benchmark/cmd/util.go b/tools/benchmark/cmd/util.go
index a6b999390..4233b8477 100644
--- a/tools/benchmark/cmd/util.go
+++ b/tools/benchmark/cmd/util.go
@@ -157,24 +157,24 @@ func mustRandBytes(n int) []byte {
 	return rb
 }
 
-func newReport() report.Report {
+func newReport(reportName string) report.Report {
 	p := "%4.4f"
 	if precise {
 		p = "%g"
 	}
 	if sample {
-		return report.NewReportSample(p)
+		return report.NewReportSample(p, reportName, generatePerfReport)
 	}
-	return report.NewReport(p)
+	return report.NewReport(p, reportName, generatePerfReport)
 }
 
-func newWeightedReport() report.Report {
+func newWeightedReport(reportName string) report.Report {
 	p := "%4.4f"
 	if precise {
 		p = "%g"
 	}
 	if sample {
-		return report.NewReportSample(p)
+		return report.NewReportSample(p, reportName, generatePerfReport)
 	}
-	return report.NewWeightedReport(report.NewReport(p), p)
+	return report.NewWeightedReport(report.NewReport(p, reportName, generatePerfReport), p, reportName, generatePerfReport)
 }
diff --git a/tools/benchmark/cmd/watch.go b/tools/benchmark/cmd/watch.go
index a52a34360..7b707bf96 100644
--- a/tools/benchmark/cmd/watch.go
+++ b/tools/benchmark/cmd/watch.go
@@ -117,7 +117,7 @@ func benchMakeWatches(clients []*clientv3.Client, wk *watchedKeys) {
 	bar = pb.New(watchStreams * watchWatchesPerStream)
 	bar.Start()
 
-	r := newReport()
+	r := newReport("watch-make")
 	rch := r.Results()
 
 	wg.Add(len(streams) + 1)
@@ -189,7 +189,7 @@ func benchPutWatches(clients []*clientv3.Client, wk *watchedKeys) {
 	bar = pb.New(eventsTotal)
 	bar.Start()
 
-	r := newReport()
+	r := newReport("watch-put")
 
 	wg.Add(len(wk.watches))
 	nrRxed := int32(eventsTotal)
diff --git a/tools/benchmark/cmd/watch_get.go b/tools/benchmark/cmd/watch_get.go
index b9a8bc336..96abad665 100644
--- a/tools/benchmark/cmd/watch_get.go
+++ b/tools/benchmark/cmd/watch_get.go
@@ -49,7 +49,7 @@ func init() {
 	watchGetCmd.Flags().IntVar(&watchEvents, "events", 8, "Number of events per watcher")
 }
 
-func watchGetFunc(_ *cobra.Command, _ []string) {
+func watchGetFunc(cmd *cobra.Command, _ []string) {
 	clients := mustCreateClients(totalClients, totalConns)
 	getClient := mustCreateClients(1, 1)
 
@@ -75,7 +75,7 @@ func watchGetFunc(_ *cobra.Command, _ []string) {
 	bar.Start()
 
 	// report from trying to do serialized gets with concurrent watchers
-	r := newReport()
+	r := newReport(cmd.Name())
 	ctx, cancel := context.WithCancel(context.TODO())
 	f := func() {
 		defer close(r.Results())
diff --git a/tools/benchmark/cmd/watch_latency.go b/tools/benchmark/cmd/watch_latency.go
index 2e60860b4..4afe636d8 100644
--- a/tools/benchmark/cmd/watch_latency.go
+++ b/tools/benchmark/cmd/watch_latency.go
@@ -60,7 +60,7 @@ func init() {
 	watchLatencyCmd.Flags().IntVar(&watchLValueSize, "val-size", 32, "Value size of watch response")
 }
 
-func watchLatencyFunc(_ *cobra.Command, _ []string) {
+func watchLatencyFunc(cmd *cobra.Command, _ []string) {
 	key := string(mustRandBytes(watchLKeySize))
 	value := string(mustRandBytes(watchLValueSize))
 	wchs := setupWatchChannels(key)
@@ -93,9 +93,9 @@ func watchLatencyFunc(_ *cobra.Command, _ []string) {
 		}()
 	}
 
-	putReport := newReport()
+	putReport := newReport(cmd.Name() + "-put")
 	putReportResults := putReport.Run()
-	watchReport := newReport()
+	watchReport := newReport(cmd.Name() + "-watch")
 	watchReportResults := watchReport.Run()
 	for i := 0; i < watchLPutTotal; i++ {
 		// limit key put as per reqRate
diff --git a/tools/etcd-dump-db/README.md b/tools/etcd-dump-db/README.md
index b176096f0..e2375da05 100644
--- a/tools/etcd-dump-db/README.md
+++ b/tools/etcd-dump-db/README.md
@@ -2,6 +2,9 @@
 
 `etcd-dump-db` inspects etcd db files.
 
+> ⚠️ **Deprecated**: This tool has been deprecated.
+> Please use [etcdutl](https://github.com/etcd-io/etcd/tree/main/etcdutl) instead.
+
 ## Installation
 
 Install the tool by running the following command from the etcd source directory.
diff --git a/tools/etcd-dump-db/main.go b/tools/etcd-dump-db/main.go
index 6be6d83ac..530e64092 100644
--- a/tools/etcd-dump-db/main.go
+++ b/tools/etcd-dump-db/main.go
@@ -24,6 +24,8 @@ import (
 	"time"
 
 	"github.com/spf13/cobra"
+
+	"go.etcd.io/etcd/client/pkg/v3/fileutil"
 )
 
 var (
@@ -85,7 +87,7 @@ func listBucketCommandFunc(_ *cobra.Command, args []string) {
 	if !strings.HasSuffix(dp, "db") {
 		dp = filepath.Join(snapDir(dp), "db")
 	}
-	if !existFileOrDir(dp) {
+	if !fileutil.Exist(dp) {
 		log.Fatalf("%q does not exist", dp)
 	}
 
@@ -106,7 +108,7 @@ func iterateBucketCommandFunc(_ *cobra.Command, args []string) {
 	if !strings.HasSuffix(dp, "db") {
 		dp = filepath.Join(snapDir(dp), "db")
 	}
-	if !existFileOrDir(dp) {
+	if !fileutil.Exist(dp) {
 		log.Fatalf("%q does not exist", dp)
 	}
 	bucket := args[1]
@@ -124,7 +126,7 @@ func scanKeysCommandFunc(_ *cobra.Command, args []string) {
 	if !strings.HasSuffix(dp, "db") {
 		dp = filepath.Join(snapDir(dp), "db")
 	}
-	if !existFileOrDir(dp) {
+	if !fileutil.Exist(dp) {
 		log.Fatalf("%q does not exist", dp)
 	}
 	startRev, err := strconv.ParseInt(args[1], 10, 64)
@@ -145,7 +147,7 @@ func getHashCommandFunc(_ *cobra.Command, args []string) {
 	if !strings.HasSuffix(dp, "db") {
 		dp = filepath.Join(snapDir(dp), "db")
 	}
-	if !existFileOrDir(dp) {
+	if !fileutil.Exist(dp) {
 		log.Fatalf("%q does not exist", dp)
 	}
 
diff --git a/tools/etcd-dump-db/utils.go b/tools/etcd-dump-db/utils.go
index 184cb5181..35ba3d5c1 100644
--- a/tools/etcd-dump-db/utils.go
+++ b/tools/etcd-dump-db/utils.go
@@ -15,15 +15,9 @@
 package main
 
 import (
-	"os"
 	"unsafe"
 )
 
-func existFileOrDir(name string) bool {
-	_, err := os.Stat(name)
-	return err == nil
-}
-
 func unsafeAdd(base unsafe.Pointer, offset uintptr) unsafe.Pointer {
 	return unsafe.Pointer(uintptr(base) + offset)
 }
diff --git a/tools/mod/go.mod b/tools/mod/go.mod
index b435a08ae..28ce27139 100644
--- a/tools/mod/go.mod
+++ b/tools/mod/go.mod
@@ -2,22 +2,22 @@ module go.etcd.io/etcd/tools/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 require (
-	github.com/alexfalkowski/gocovmerge v1.9.0
+	github.com/alexfalkowski/gocovmerge v1.11.0
 	github.com/appscodelabs/license-bill-of-materials v0.0.0-20220707232035-6018e0c5287c
 	github.com/chzchzchz/goword v0.0.0-20170907005317-a9744cb52b03
 	github.com/cloudflare/cfssl v1.6.5
 	github.com/gogo/protobuf v1.3.2
 	github.com/golangci/golangci-lint v1.64.8
 	github.com/google/addlicense v1.1.1
-	github.com/google/yamlfmt v0.17.0
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
+	github.com/google/yamlfmt v0.17.2
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
 	go.etcd.io/gofail v0.2.0
 	go.etcd.io/protodoc v0.0.0-20180829002748-484ab544e116
 	go.etcd.io/raft/v3 v3.6.0
-	gotest.tools/gotestsum v1.12.2
+	gotest.tools/gotestsum v1.12.3
 	gotest.tools/v3 v3.5.2
 	honnef.co/go/tools v0.6.1
 )
@@ -74,7 +74,7 @@ require (
 	github.com/fzipp/gocyclo v0.6.0 // indirect
 	github.com/ghostiam/protogetter v0.3.9 // indirect
 	github.com/go-critic/go-critic v0.12.0 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-sql-driver/mysql v1.8.1 // indirect
 	github.com/go-toolsmith/astcast v1.1.0 // indirect
 	github.com/go-toolsmith/astcopy v1.1.0 // indirect
@@ -147,14 +147,14 @@ require (
 	github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
 	github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
 	github.com/olekukonko/ll v0.0.8 // indirect
-	github.com/olekukonko/tablewriter v1.0.7 // indirect
+	github.com/olekukonko/tablewriter v1.0.8 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/polyfloyd/go-errorlint v1.7.1 // indirect
 	github.com/prometheus/client_golang v1.22.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
-	github.com/prometheus/common v0.64.0 // indirect
+	github.com/prometheus/common v0.65.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
 	github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
@@ -223,8 +223,8 @@ require (
 	golang.org/x/term v0.32.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
 	golang.org/x/tools v0.34.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/grpc v1.73.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/tools/mod/go.sum b/tools/mod/go.sum
index 74ddd77c8..9948eaed4 100644
--- a/tools/mod/go.sum
+++ b/tools/mod/go.sum
@@ -36,8 +36,8 @@ github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsr
 github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
-github.com/alexfalkowski/gocovmerge v1.9.0 h1:ozu15S3V+muHM72WOi+/lXgJZ/zSzjB6CRfBV7HnJDc=
-github.com/alexfalkowski/gocovmerge v1.9.0/go.mod h1:gkfERgPiozeXq7FlJBQDmTeXo8FnrNxZwabB4uSGZ3M=
+github.com/alexfalkowski/gocovmerge v1.11.0 h1:mHYRBKEBHxjTWveV6RCAnCAhF6l1evO9JYboZRvZmuU=
+github.com/alexfalkowski/gocovmerge v1.11.0/go.mod h1:gkfERgPiozeXq7FlJBQDmTeXo8FnrNxZwabB4uSGZ3M=
 github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU=
 github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU=
 github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
@@ -126,8 +126,8 @@ github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYF
 github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
 github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w=
 github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
@@ -203,8 +203,8 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/Z
 github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
-github.com/google/yamlfmt v0.17.0 h1:/tdp01rIlvLz3LgJ2NtMLnqgAadZm33P7GcPU680b+w=
-github.com/google/yamlfmt v0.17.0/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8=
+github.com/google/yamlfmt v0.17.2 h1:TkXxhmj7dnpmOnlWGOXog92Gs6MWcTZqnf3kuyp8yFQ=
+github.com/google/yamlfmt v0.17.2/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8=
 github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
 github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
 github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
@@ -220,8 +220,8 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW
 github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
 github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=
 github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
 github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
 github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -328,8 +328,8 @@ github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hS
 github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
 github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
 github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
-github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
+github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
 github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
 github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
@@ -357,8 +357,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/
 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo=
@@ -685,10 +685,10 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
 google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
@@ -708,8 +708,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools/gotestsum v1.12.2 h1:eli4tu9Q2D/ogDsEGSr8XfQfl7mT0JsGOG6DFtUiZ/Q=
-gotest.tools/gotestsum v1.12.2/go.mod h1:kjRtCglPZVsSU0hFHX3M5VWBM6Y63emHuB14ER1/sow=
+gotest.tools/gotestsum v1.12.3 h1:jFwenGJ0RnPkuKh2VzAYl1mDOJgbhobBDeL2W1iEycs=
+gotest.tools/gotestsum v1.12.3/go.mod h1:Y1+e0Iig4xIRtdmYbEV7K7H6spnjc1fX4BOuUhWw2Wk=
 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
 honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
diff --git a/tools/rw-heatmaps/go.mod b/tools/rw-heatmaps/go.mod
index 06d52f630..dd21ebb94 100644
--- a/tools/rw-heatmaps/go.mod
+++ b/tools/rw-heatmaps/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/tools/rw-heatmaps/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 require (
 	github.com/spf13/cobra v1.9.1
diff --git a/tools/testgrid-analysis/go.mod b/tools/testgrid-analysis/go.mod
index b8ab75bad..47d16785f 100644
--- a/tools/testgrid-analysis/go.mod
+++ b/tools/testgrid-analysis/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/tools/testgrid-analysis/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 require (
 	github.com/GoogleCloudPlatform/testgrid v0.0.173
@@ -15,11 +15,11 @@ require (
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
-	go.opentelemetry.io/otel v1.36.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.36.0 // indirect
+	go.opentelemetry.io/otel v1.37.0 // indirect
+	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
 	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	google.golang.org/grpc v1.73.0 // indirect
 )
diff --git a/tools/testgrid-analysis/go.sum b/tools/testgrid-analysis/go.sum
index 72eab19cb..233708159 100644
--- a/tools/testgrid-analysis/go.sum
+++ b/tools/testgrid-analysis/go.sum
@@ -863,8 +863,8 @@ github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg
 github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
@@ -1180,16 +1180,16 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
 go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
@@ -1841,8 +1841,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230720185612-659f7aaaa771/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=



Test patch:
diff --git a/cache/demux_test.go b/cache/demux_test.go
new file mode 100644
index 000000000..abfa6219a
--- /dev/null
+++ b/cache/demux_test.go
@@ -0,0 +1,165 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+func TestBroadcastBatching(t *testing.T) {
+	tests := []struct {
+		name      string
+		input     []int64
+		wantRevs  []int64
+		wantSizes []int
+	}{
+		{
+			name:      "two groups",
+			input:     []int64{14, 14, 15, 15, 15},
+			wantRevs:  []int64{14},
+			wantSizes: []int{5},
+		},
+		{
+			name:      "single group",
+			input:     []int64{7, 7, 7},
+			wantRevs:  []int64{7},
+			wantSizes: []int{3},
+		},
+		{
+			name:      "all distinct",
+			input:     []int64{1, 2, 3},
+			wantRevs:  []int64{1},
+			wantSizes: []int{3},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			d := newDemux(16, 10*time.Millisecond)
+			w := newWatcher(len(tt.input)+1, nil)
+			d.Register(w, 0)
+
+			d.Broadcast(eventRevs(tt.input...))
+
+			gotRevs, gotSizes := readBatches(t, w, len(tt.wantRevs))
+
+			if diff := cmp.Diff(tt.wantRevs, gotRevs); diff != "" {
+				t.Fatalf("revision mismatch (-want +got):\n%s", diff)
+			}
+			if diff := cmp.Diff(tt.wantSizes, gotSizes); diff != "" {
+				t.Fatalf("batch size mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestSlowWatcherResync(t *testing.T) {
+	tests := []struct {
+		name             string
+		input            []int64
+		wantInitialRevs  []int64
+		wantInitialSizes []int
+		wantResyncRevs   []int64
+		wantResyncSizes  []int
+	}{
+		{
+			name:             "single event overflow",
+			input:            []int64{1, 2, 3},
+			wantInitialRevs:  []int64{1},
+			wantInitialSizes: []int{3},
+			wantResyncRevs:   []int64{},
+			wantResyncSizes:  []int{},
+		},
+		{
+			name:             "multi events batch overflow",
+			input:            []int64{10, 10, 11, 12, 12},
+			wantInitialRevs:  []int64{10},
+			wantInitialSizes: []int{5},
+			wantResyncRevs:   []int64{},
+			wantResyncSizes:  []int{},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			d := newDemux(16, 10*time.Millisecond)
+			w := newWatcher(1, nil)
+			d.Register(w, 0)
+
+			d.Broadcast(eventRevs(tt.input...))
+
+			gotInitRevs, gotInitSizes := readBatches(t, w, len(tt.wantInitialRevs))
+			if diff := cmp.Diff(tt.wantInitialRevs, gotInitRevs); diff != "" {
+				t.Fatalf("initial revs mismatch (-want +got):\n%s", diff)
+			}
+			if diff := cmp.Diff(tt.wantInitialSizes, gotInitSizes); diff != "" {
+				t.Fatalf("initial batch sizes mismatch (-want +got):\n%s", diff)
+			}
+
+			gotRevs, gotSizes := make([]int64, 0, len(tt.wantResyncRevs)), make([]int, 0, len(tt.wantResyncRevs))
+			for len(gotRevs) < len(tt.wantResyncRevs) {
+				d.resyncLaggingWatchers()
+				revs, sizes := readBatches(t, w, 1)
+				gotRevs = append(gotRevs, revs...)
+				gotSizes = append(gotSizes, sizes...)
+			}
+			if diff := cmp.Diff(tt.wantResyncRevs, gotRevs); diff != "" {
+				t.Fatalf("resync revs mismatch (-want +got):\n%s", diff)
+			}
+			if diff := cmp.Diff(tt.wantResyncSizes, gotSizes); diff != "" {
+				t.Fatalf("resync batch sizes mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func eventRevs(revs ...int64) []*clientv3.Event {
+	events := make([]*clientv3.Event, 0, len(revs))
+	for _, r := range revs {
+		kv := &mvccpb.KeyValue{
+			Key:         []byte("k"),
+			Value:       []byte("v"),
+			ModRevision: r,
+		}
+		events = append(events, &clientv3.Event{
+			Type: clientv3.EventTypePut,
+			Kv:   kv,
+		})
+	}
+	return events
+}
+
+func readBatches(t *testing.T, w *watcher, n int) (revs []int64, sizes []int) {
+	t.Helper()
+	timeout := time.After(2 * time.Second)
+	for len(revs) < n {
+		select {
+		case batch := <-w.eventQueue:
+			revs = append(revs, batch[0].Kv.ModRevision)
+			sizes = append(sizes, len(batch))
+		case <-timeout:
+			t.Fatalf("timed out waiting for %d batches; got %d", n, len(revs))
+		}
+	}
+	return revs, sizes
+}
diff --git a/cache/ringbuffer_test.go b/cache/ringbuffer_test.go
new file mode 100644
index 000000000..0e7e3b010
--- /dev/null
+++ b/cache/ringbuffer_test.go
@@ -0,0 +1,308 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cache
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+func TestPeekLatestAndOldest(t *testing.T) {
+	tests := []struct {
+		name          string
+		capacity      int
+		revs          []int64
+		wantLatestRev int64
+		wantOldestRev int64
+	}{
+		{
+			name:          "empty_buffer",
+			capacity:      4,
+			revs:          nil,
+			wantLatestRev: 0,
+			wantOldestRev: 0,
+		},
+		{
+			name:          "single_element",
+			capacity:      8,
+			revs:          []int64{1},
+			wantLatestRev: 1,
+			wantOldestRev: 1,
+		},
+		{
+			name:          "ascending_fill",
+			capacity:      4,
+			revs:          []int64{1, 2, 3, 4},
+			wantLatestRev: 4,
+			wantOldestRev: 1,
+		},
+		{
+			name:          "overwrite_when_full",
+			capacity:      3,
+			revs:          []int64{5, 6, 7, 8},
+			wantLatestRev: 8,
+			wantOldestRev: 6,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			rb := newRingBuffer(tt.capacity)
+			for _, r := range tt.revs {
+				batch, err := makeEventBatch(r, "k", 1)
+				if err != nil {
+					t.Fatalf("makeEventBatch(%d, k, 1) failed: %v", r, err)
+				}
+				rb.Append(batch)
+			}
+
+			latestRev := rb.PeekLatest()
+			oldestRev := rb.PeekOldest()
+
+			gotLatestRev := latestRev
+			gotOldestRev := oldestRev
+
+			if tt.wantLatestRev != gotLatestRev {
+				t.Fatalf("PeekLatest()=%d, want=%d", gotLatestRev, tt.wantLatestRev)
+			}
+			if tt.wantOldestRev != gotOldestRev {
+				t.Fatalf("PeekOldest()=%d, want=%d", gotOldestRev, tt.wantOldestRev)
+			}
+		})
+	}
+}
+
+func TestFilter(t *testing.T) {
+	tests := []struct {
+		name             string
+		capacity         int
+		revs             []int64
+		minRev           int64
+		wantFilteredRevs []int64
+		wantLatestRev    int64
+	}{
+		{
+			name:             "no_filter",
+			capacity:         5,
+			revs:             []int64{1, 2, 3},
+			minRev:           0,
+			wantFilteredRevs: []int64{1, 2, 3},
+			wantLatestRev:    3,
+		},
+		{
+			name:             "partial_match",
+			capacity:         5,
+			revs:             []int64{10, 11, 12, 13},
+			minRev:           12,
+			wantFilteredRevs: []int64{12, 13},
+			wantLatestRev:    13,
+		},
+		{
+			name:             "filter_when_full",
+			capacity:         3,
+			revs:             []int64{20, 21, 22, 23, 24},
+			minRev:           23,
+			wantFilteredRevs: []int64{23, 24},
+			wantLatestRev:    24,
+		},
+		{
+			name:             "none_match",
+			capacity:         4,
+			revs:             []int64{30, 31},
+			minRev:           100,
+			wantFilteredRevs: []int64{},
+			wantLatestRev:    31,
+		},
+		{
+			name:             "empty_buffer",
+			capacity:         3,
+			revs:             nil,
+			minRev:           0,
+			wantFilteredRevs: []int64{},
+			wantLatestRev:    0,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			rb := newRingBuffer(tt.capacity)
+			for _, r := range tt.revs {
+				batch, err := makeEventBatch(r, "k", 11)
+				if err != nil {
+					t.Fatalf("makeEventBatch(%d, k, 11) failed: %v", r, err)
+				}
+				rb.Append(batch)
+			}
+
+			gotBatches := rb.Filter(tt.minRev)
+			gotRevs := make([]int64, len(gotBatches))
+			for i, b := range gotBatches {
+				gotRevs[i] = b[0].Kv.ModRevision
+			}
+
+			if diff := cmp.Diff(tt.wantFilteredRevs, gotRevs); diff != "" {
+				t.Fatalf("Filter() revisions mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestAtomicOrdered(t *testing.T) {
+	tests := []struct {
+		name     string
+		capacity int
+		inputs   []struct {
+			rev  int64
+			key  string
+			size int
+		}
+		wantRev  []int64
+		wantSize []int
+	}{
+		{
+			name:     "unfiltered",
+			capacity: 5,
+			inputs: []struct {
+				rev  int64
+				key  string
+				size int
+			}{
+				{5, "a", 1},
+				{10, "b", 3},
+				{15, "c", 7},
+				{20, "d", 11},
+			},
+			wantRev:  []int64{5, 10, 15, 20},
+			wantSize: []int{1, 3, 7, 11},
+		},
+		{
+			name:     "across_wrap",
+			capacity: 3,
+			inputs: []struct {
+				rev  int64
+				key  string
+				size int
+			}{
+				{1, "a", 2},
+				{2, "b", 1},
+				{3, "c", 3},
+				{4, "d", 7},
+			},
+			wantRev:  []int64{2, 3, 4},
+			wantSize: []int{1, 3, 7},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			rb := newRingBuffer(tt.capacity)
+			for _, in := range tt.inputs {
+				batch, err := makeEventBatch(in.rev, in.key, in.size)
+				if err != nil {
+					t.Fatalf("makeEventBatch(%d, k, 1) failed: %v", in.rev, err)
+				}
+				rb.Append(batch)
+			}
+
+			gotBatches := rb.Filter(0)
+			if len(gotBatches) != len(tt.wantRev) {
+				t.Fatalf("len(got) = %d, want %d", len(gotBatches), len(tt.wantRev))
+			}
+			for i, b := range gotBatches {
+				if b[0].Kv.ModRevision != tt.wantRev[i] {
+					t.Errorf("at idx %d: rev = %d, want %d", i, b[0].Kv.ModRevision, tt.wantRev[i])
+				}
+				if batchSize := len(b); batchSize != tt.wantSize[i] {
+					t.Errorf("at rev %d: events.len = %d, want %d", b[0].Kv.ModRevision, batchSize, tt.wantSize[i])
+				}
+			}
+		})
+	}
+}
+
+func TestRebaseHistory(t *testing.T) {
+	tests := []struct {
+		name string
+		revs []int64
+	}{
+		{
+			name: "rebase_empty_buffer",
+			revs: nil,
+		},
+		{
+			name: "rebase_after_data",
+			revs: []int64{7, 8, 9},
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			rb := newRingBuffer(4)
+			for _, r := range tt.revs {
+				batch, err := makeEventBatch(r, "k", 1)
+				if err != nil {
+					t.Fatalf("makeEventBatch(%d, k, 1) failed: %v", r, err)
+				}
+				rb.Append(batch)
+			}
+
+			rb.RebaseHistory()
+
+			oldestRev := rb.PeekOldest()
+
+			latestRev := rb.PeekLatest()
+
+			if oldestRev != 0 {
+				t.Fatalf("PeekOldest()=%d, want=%d", oldestRev, 0)
+			}
+			if latestRev != 0 {
+				t.Fatalf("PeekLatest()=%d, want=%d", latestRev, 0)
+			}
+
+			batches := rb.Filter(0)
+			if len(batches) != 0 {
+				t.Fatalf("Filter() len(events)=%d, want=%d", len(batches), 0)
+			}
+		})
+	}
+}
+
+func makeEventBatch(rev int64, key string, batchSize int) ([]*clientv3.Event, error) {
+	if batchSize < 0 {
+		return nil, fmt.Errorf("invalid batchSize %d", batchSize)
+	}
+	events := make([]*clientv3.Event, batchSize)
+	for i := range events {
+		events[i] = &clientv3.Event{
+			Kv: &mvccpb.KeyValue{
+				Key:         []byte(fmt.Sprintf("%s-%d", key, i)),
+				ModRevision: rev,
+			},
+		}
+	}
+	return events, nil
+}
diff --git a/etcdctl/ctlv3/command/printer_json_test.go b/etcdctl/ctlv3/command/printer_json_test.go
new file mode 100644
index 000000000..98296e4f6
--- /dev/null
+++ b/etcdctl/ctlv3/command/printer_json_test.go
@@ -0,0 +1,161 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"math"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+const (
+	keyHeader  = "header"
+	keyMember  = "member"
+	keyMembers = "members"
+
+	keyClusterID = "cluster_id"
+	keyMemberID  = "member_id"
+	keyRaftTerm  = "raft_term"
+	keyRevision  = "revision"
+	keyID        = "ID"
+)
+
+func assertNumericFieldEqual(t *testing.T, obj map[string]any, key string, want int64) {
+	raw, ok := obj[key]
+	require.Truef(t, ok, "missing key %q in map %v", key, obj)
+
+	n, ok := raw.(json.Number)
+	require.Truef(t, ok, "field %q is not json.Number: %v", key, raw)
+
+	val, err := n.Int64()
+	require.NoErrorf(t, err, "failed to convert field %q to int64: %v", key, n)
+
+	assert.Equalf(t, want, val, "unexpected value for field %q", key)
+}
+
+func assertHexFieldEqual(t *testing.T, obj map[string]any, key string, want string) {
+	raw, ok := obj[key]
+	require.Truef(t, ok, "missing key %q in map %v", key, obj)
+
+	str, ok := raw.(string)
+	require.Truef(t, ok, "field %q is not a string: %v", key, str)
+
+	assert.Equalf(t, want, str, "unexpected value for hex field %q", key)
+}
+
+type testCase struct {
+	number            uint64
+	wantHexString     string
+	wantDecimalNumber int64
+}
+
+var testCases = []testCase{
+	{1, "1", 1},
+	{100, "64", 100},
+	{1234567890, "499602d2", 1234567890},
+	{math.MaxInt64, "7fffffffffffffff", math.MaxInt64},
+}
+
+func TestMemberAdd(t *testing.T) {
+	tests := []struct {
+		name  string
+		isHex bool
+		cases []testCase
+	}{
+		{name: "decimal", isHex: false, cases: testCases},
+		{name: "hex", isHex: true, cases: testCases},
+	}
+
+	for _, testGroup := range tests {
+		t.Run(testGroup.name, func(t *testing.T) {
+			var buffer bytes.Buffer
+			p := &jsonPrinter{writer: &buffer, isHex: testGroup.isHex}
+
+			for _, tt := range testGroup.cases {
+				t.Run(fmt.Sprintf("number=%d", tt.number), func(t *testing.T) {
+					buffer.Reset()
+					decoder := json.NewDecoder(&buffer)
+					decoder.UseNumber()
+
+					response := clientv3.MemberAddResponse{
+						Header: &pb.ResponseHeader{
+							ClusterId: tt.number,
+							MemberId:  tt.number,
+							Revision:  int64(tt.number),
+							RaftTerm:  tt.number,
+						},
+						Member:  &pb.Member{ID: tt.number},
+						Members: []*pb.Member{{ID: tt.number}},
+					}
+					p.MemberAdd(response)
+
+					var got map[string]any
+					err := decoder.Decode(&got)
+					require.NoErrorf(t, err, "failed to decode JSON")
+
+					rawHeader, ok := got[keyHeader]
+					require.Truef(t, ok, "output does not contain %s field: %v", keyHeader, got)
+					header, ok := rawHeader.(map[string]any)
+					require.Truef(t, ok, "field %s is not map[string]any: %v", keyHeader, rawHeader)
+
+					if testGroup.isHex {
+						assertHexFieldEqual(t, header, keyClusterID, tt.wantHexString)
+						assertHexFieldEqual(t, header, keyMemberID, tt.wantHexString)
+					} else {
+						assertNumericFieldEqual(t, header, keyClusterID, tt.wantDecimalNumber)
+						assertNumericFieldEqual(t, header, keyMemberID, tt.wantDecimalNumber)
+					}
+					assertNumericFieldEqual(t, header, keyRaftTerm, tt.wantDecimalNumber)
+					assertNumericFieldEqual(t, header, keyRevision, tt.wantDecimalNumber)
+
+					rawMember, ok := got[keyMember]
+					require.Truef(t, ok, "output does not contain %s field: %v", keyMember, got)
+					member, ok := rawMember.(map[string]any)
+					require.Truef(t, ok, "field %s is not map[string]any: %v", keyMember, rawMember)
+
+					if testGroup.isHex {
+						assertHexFieldEqual(t, member, keyID, tt.wantHexString)
+					} else {
+						assertNumericFieldEqual(t, member, keyID, tt.wantDecimalNumber)
+					}
+
+					rawMembers, ok := got[keyMembers]
+					require.Truef(t, ok, "output does not contain %s field: %v", keyMembers, got)
+					members, ok := rawMembers.([]any)
+					require.Truef(t, ok, "field %s is not []any: %v", keyMembers, rawMembers)
+
+					for _, rawMemberItem := range members {
+						memberItem, ok := rawMemberItem.(map[string]any)
+						require.Truef(t, ok, "%s item is not map[string]any: %v", keyMembers, rawMemberItem)
+
+						if testGroup.isHex {
+							assertHexFieldEqual(t, memberItem, keyID, tt.wantHexString)
+						} else {
+							assertNumericFieldEqual(t, memberItem, keyID, tt.wantDecimalNumber)
+						}
+					}
+				})
+			}
+		})
+	}
+}
diff --git a/pkg/report/report_test.go b/pkg/report/report_test.go
index 0c9e37028..1d5a49e07 100644
--- a/pkg/report/report_test.go
+++ b/pkg/report/report_test.go
@@ -37,7 +37,7 @@ func TestPercentiles(t *testing.T) {
 }
 
 func TestReport(t *testing.T) {
-	r := NewReportSample("%f")
+	r := NewReportSample("%f", "", false)
 	go func() {
 		start := time.Now()
 		for i := 0; i < 5; i++ {
@@ -77,7 +77,7 @@ func TestReport(t *testing.T) {
 }
 
 func TestWeightedReport(t *testing.T) {
-	r := NewWeightedReport(NewReport("%f"), "%f")
+	r := NewWeightedReport(NewReport("%f", "", false), "%f", "", false)
 	go func() {
 		start := time.Now()
 		for i := 0; i < 5; i++ {
diff --git a/pkg/traceutil/trace_test.go b/pkg/traceutil/trace_test.go
index 508067197..395a1e0bb 100644
--- a/pkg/traceutil/trace_test.go
+++ b/pkg/traceutil/trace_test.go
@@ -73,7 +73,7 @@ func TestCreate(t *testing.T) {
 		}
 	)
 
-	trace := New(op, nil, fields[0], fields[1])
+	_, trace := EnsureTrace(t.Context(), nil, op, fields[0], fields[1])
 	assert.Equalf(t, trace.operation, op, "Expected %v; Got %v", op, trace.operation)
 	for i, f := range trace.fields {
 		assert.Equalf(t, f.Key, fields[i].Key, "Expected %v; Got %v", fields[i].Key, f.Key)
diff --git a/server/storage/mvcc/watchable_store_test.go b/server/storage/mvcc/watchable_store_test.go
index e888447e0..d7b69adb9 100644
--- a/server/storage/mvcc/watchable_store_test.go
+++ b/server/storage/mvcc/watchable_store_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/prometheus/client_golang/prometheus/testutil"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -572,105 +573,115 @@ func TestWatchFutureRev(t *testing.T) {
 }
 
 func TestWatchRestore(t *testing.T) {
-	test := func(delay time.Duration) func(t *testing.T) {
-		return func(t *testing.T) {
-			b, _ := betesting.NewDefaultTmpBackend(t)
-			s := New(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
-			defer cleanup(s, b)
+	resyncDelay := watchResyncPeriod * 3 / 2
 
-			testKey := []byte("foo")
-			testValue := []byte("bar")
-			w := s.NewWatchStream()
-			defer w.Close()
-			w.Watch(0, testKey, nil, 1)
+	t.Run("NoResync", func(t *testing.T) {
+		testWatchRestore(t, 0, 0)
+	})
+	t.Run("ResyncBefore", func(t *testing.T) {
+		testWatchRestore(t, resyncDelay, 0)
+	})
+	t.Run("ResyncAfter", func(t *testing.T) {
+		testWatchRestore(t, 0, resyncDelay)
+	})
+
+	t.Run("ResyncBeforeAndAfter", func(t *testing.T) {
+		testWatchRestore(t, resyncDelay, resyncDelay)
+	})
+}
 
-			time.Sleep(delay)
-			wantRev := s.Put(testKey, testValue, lease.NoLease)
+func testWatchRestore(t *testing.T, delayBeforeRestore, delayAfterRestore time.Duration) {
+	b, _ := betesting.NewDefaultTmpBackend(t)
+	s := New(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
+	defer cleanup(s, b)
 
-			s.Restore(b)
-			events := readEventsForSecond(w.Chan())
-			if len(events) != 1 {
-				t.Errorf("Expected only one event, got %d", len(events))
-			}
-			if events[0].Kv.ModRevision != wantRev {
-				t.Errorf("Expected revision to match, got %d, want %d", events[0].Kv.ModRevision, wantRev)
-			}
-		}
+	testKey := []byte("foo")
+	testValue := []byte("bar")
+
+	tcs := []struct {
+		name          string
+		startRevision int64
+		wantEvents    []mvccpb.Event
+	}{
+		{
+			name:          "zero revision",
+			startRevision: 0,
+			wantEvents: []mvccpb.Event{
+				{Type: mvccpb.PUT, Kv: &mvccpb.KeyValue{Key: testKey, Value: testValue, CreateRevision: 2, ModRevision: 2, Version: 1}},
+				{Type: mvccpb.DELETE, Kv: &mvccpb.KeyValue{Key: testKey, ModRevision: 3}},
+			},
+		},
+		{
+			name:          "revsion before first write",
+			startRevision: 1,
+			wantEvents: []mvccpb.Event{
+				{Type: mvccpb.PUT, Kv: &mvccpb.KeyValue{Key: testKey, Value: testValue, CreateRevision: 2, ModRevision: 2, Version: 1}},
+				{Type: mvccpb.DELETE, Kv: &mvccpb.KeyValue{Key: testKey, ModRevision: 3}},
+			},
+		},
+		{
+			name:          "revision of first write",
+			startRevision: 2,
+			wantEvents: []mvccpb.Event{
+				{Type: mvccpb.PUT, Kv: &mvccpb.KeyValue{Key: testKey, Value: testValue, CreateRevision: 2, ModRevision: 2, Version: 1}},
+				{Type: mvccpb.DELETE, Kv: &mvccpb.KeyValue{Key: testKey, ModRevision: 3}},
+			},
+		},
+		{
+			name:          "current revision",
+			startRevision: 3,
+			wantEvents: []mvccpb.Event{
+				{Type: mvccpb.DELETE, Kv: &mvccpb.KeyValue{Key: testKey, ModRevision: 3}},
+			},
+		},
+		{
+			name:          "future revision",
+			startRevision: 4,
+			wantEvents:    []mvccpb.Event{},
+		},
+	}
+	watchers := []WatchStream{}
+	for i, tc := range tcs {
+		w := s.NewWatchStream()
+		defer w.Close()
+		watchers = append(watchers, w)
+		w.Watch(WatchID(i+1), testKey, nil, tc.startRevision)
 	}
 
-	t.Run("Normal", test(0))
-	t.Run("RunSyncWatchLoopBeforeRestore", test(time.Millisecond*120)) // longer than default waitDuration
+	s.Put(testKey, testValue, lease.NoLease)
+	time.Sleep(delayBeforeRestore)
+	s.Restore(b)
+	time.Sleep(delayAfterRestore)
+	s.DeleteRange(testKey, nil)
+
+	for i, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			events := readEventsForSecond(t, watchers[i].Chan())
+			if diff := cmp.Diff(tc.wantEvents, events); diff != "" {
+				t.Errorf("unexpected events (-want +got):\n%s", diff)
+			}
+		})
+	}
 }
 
-func readEventsForSecond(ws <-chan WatchResponse) (events []mvccpb.Event) {
+func readEventsForSecond(t *testing.T, ws <-chan WatchResponse) []mvccpb.Event {
+	events := []mvccpb.Event{}
+	deadline := time.After(time.Second)
 	for {
 		select {
 		case resp := <-ws:
+			if len(resp.Events) == 0 {
+				t.Fatalf("Events should never be empty, resp: %+v", resp)
+			}
 			events = append(events, resp.Events...)
-		case <-time.After(time.Second):
+		case <-deadline:
+			return events
+		case <-time.After(watchResyncPeriod * 3 / 2):
 			return events
 		}
 	}
 }
 
-// TestWatchRestoreSyncedWatcher tests such a case that:
-//  1. watcher is created with a future revision "math.MaxInt64 - 2"
-//  2. watcher with a future revision is added to "synced" watcher group
-//  3. restore/overwrite storage with snapshot of a higher lasat revision
-//  4. restore operation moves "synced" to "unsynced" watcher group
-//  5. choose the watcher from step 1, without panic
-func TestWatchRestoreSyncedWatcher(t *testing.T) {
-	b1, _ := betesting.NewDefaultTmpBackend(t)
-	s1 := New(zaptest.NewLogger(t), b1, &lease.FakeLessor{}, StoreConfig{})
-	defer cleanup(s1, b1)
-
-	b2, _ := betesting.NewDefaultTmpBackend(t)
-	s2 := New(zaptest.NewLogger(t), b2, &lease.FakeLessor{}, StoreConfig{})
-	defer cleanup(s2, b2)
-
-	testKey, testValue := []byte("foo"), []byte("bar")
-	rev := s1.Put(testKey, testValue, lease.NoLease)
-	startRev := rev + 2
-
-	// create a watcher with a future revision
-	// add to "synced" watcher group (startRev > s.store.currentRev)
-	w1 := s1.NewWatchStream()
-	defer w1.Close()
-
-	w1.Watch(0, testKey, nil, startRev)
-
-	// make "s2" ends up with a higher last revision
-	s2.Put(testKey, testValue, lease.NoLease)
-	s2.Put(testKey, testValue, lease.NoLease)
-
-	// overwrite storage with higher revisions
-	if err := s1.Restore(b2); err != nil {
-		t.Fatal(err)
-	}
-
-	// wait for next "syncWatchersLoop" iteration
-	// and the unsynced watcher should be chosen
-	time.Sleep(2 * time.Second)
-
-	// trigger events for "startRev"
-	s1.Put(testKey, testValue, lease.NoLease)
-
-	select {
-	case resp := <-w1.Chan():
-		if resp.Revision != startRev {
-			t.Fatalf("resp.Revision expect %d, got %d", startRev, resp.Revision)
-		}
-		if len(resp.Events) != 1 {
-			t.Fatalf("len(resp.Events) expect 1, got %d", len(resp.Events))
-		}
-		if resp.Events[0].Kv.ModRevision != startRev {
-			t.Fatalf("resp.Events[0].Kv.ModRevision expect %d, got %d", startRev, resp.Events[0].Kv.ModRevision)
-		}
-	case <-time.After(time.Second):
-		t.Fatal("failed to receive event in 1 second")
-	}
-}
-
 // TestWatchBatchUnsynced tests batching on unsynced watchers
 func TestWatchBatchUnsynced(t *testing.T) {
 	tcs := []struct {
diff --git a/server/storage/mvcc/watcher_test.go b/server/storage/mvcc/watcher_test.go
index e774c70cf..9a9f4f13a 100644
--- a/server/storage/mvcc/watcher_test.go
+++ b/server/storage/mvcc/watcher_test.go
@@ -289,21 +289,12 @@ func TestWatchStreamCancelWatcherByID(t *testing.T) {
 	}
 }
 
-// TestWatcherRequestProgress ensures synced watcher can correctly
-// report its correct progress.
-func TestWatcherRequestProgress(t *testing.T) {
+func TestWatcherRequestProgressBadId(t *testing.T) {
 	b, _ := betesting.NewDefaultTmpBackend(t)
 	s := newWatchableStore(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
 
 	defer cleanup(s, b)
-
-	testKey := []byte("foo")
-	notTestKey := []byte("bad")
-	testValue := []byte("bar")
-	s.Put(testKey, testValue, lease.NoLease)
-
 	w := s.NewWatchStream()
-
 	badID := WatchID(1000)
 	w.RequestProgress(badID)
 	select {
@@ -311,26 +302,79 @@ func TestWatcherRequestProgress(t *testing.T) {
 		t.Fatalf("unexpected %+v", resp)
 	default:
 	}
+}
 
-	id, _ := w.Watch(0, notTestKey, nil, 1)
-	w.RequestProgress(id)
-	select {
-	case resp := <-w.Chan():
-		t.Fatalf("unexpected %+v", resp)
-	default:
+func TestWatcherRequestProgress(t *testing.T) {
+	testKey := []byte("foo")
+	notTestKey := []byte("bad")
+	testValue := []byte("bar")
+	tcs := []struct {
+		name                     string
+		startRev                 int64
+		expectProgressBeforeSync bool
+		expectProgressAfterSync  bool
+	}{
+		{
+			name:                     "Zero revision",
+			startRev:                 0,
+			expectProgressBeforeSync: true,
+			expectProgressAfterSync:  true,
+		},
+		{
+			name:                    "Old revision",
+			startRev:                1,
+			expectProgressAfterSync: true,
+		},
+		{
+			name:                    "Current revision",
+			startRev:                2,
+			expectProgressAfterSync: true,
+		},
+		{
+			name:     "Current revision plus one",
+			startRev: 3,
+		},
+		{
+			name:     "Current revision plus two",
+			startRev: 4,
+		},
 	}
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			b, _ := betesting.NewDefaultTmpBackend(t)
+			s := newWatchableStore(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
 
-	s.syncWatchers([]mvccpb.Event{})
+			defer cleanup(s, b)
+
+			s.Put(testKey, testValue, lease.NoLease)
+
+			w := s.NewWatchStream()
+
+			id, _ := w.Watch(0, notTestKey, nil, tc.startRev)
+			w.RequestProgress(id)
+			asssertProgressSent(t, w, id, tc.expectProgressBeforeSync)
+			s.syncWatchers([]mvccpb.Event{})
+			w.RequestProgress(id)
+			asssertProgressSent(t, w, id, tc.expectProgressAfterSync)
+		})
+	}
+}
 
-	w.RequestProgress(id)
-	wrs := WatchResponse{WatchID: id, Revision: 2}
+func asssertProgressSent(t *testing.T, stream WatchStream, id WatchID, expectProgress bool) {
 	select {
-	case resp := <-w.Chan():
-		if !reflect.DeepEqual(resp, wrs) {
-			t.Fatalf("got %+v, expect %+v", resp, wrs)
+	case resp := <-stream.Chan():
+		if expectProgress {
+			wrs := WatchResponse{WatchID: id, Revision: 2}
+			if !reflect.DeepEqual(resp, wrs) {
+				t.Fatalf("got %+v, expect %+v", resp, wrs)
+			}
+		} else {
+			t.Fatalf("unexpected response %+v", resp)
+		}
+	default:
+		if expectProgress {
+			t.Fatalf("failed to receive progress")
 		}
-	case <-time.After(time.Second):
-		t.Fatal("failed to receive progress")
 	}
 }
 
diff --git a/tests/antithesis/Makefile b/tests/antithesis/Makefile
index 5b2ee7a45..835f5a5be 100644
--- a/tests/antithesis/Makefile
+++ b/tests/antithesis/Makefile
@@ -36,21 +36,24 @@ antithesis-build-etcd-image-main: antithesis-build-etcd-image
 
 .PHONY: antithesis-build-config-image
 antithesis-build-config-image: validate-node-count
-	docker build -f ./Dockerfile.config . -t etcd-config:latest \
+	docker build -f config/Dockerfile config -t etcd-config:latest \
 		--build-arg IMAGE_TAG=$(IMAGE_TAG) \
 		--build-arg NODE_COUNT=$(CFG_NODE_COUNT)
 
 .PHONY: antithesis-docker-compose-up
 antithesis-docker-compose-up: validate-node-count
-	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker compose -f docker-compose-$(CFG_NODE_COUNT)-node.yml up
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && \
+		docker compose -f config/docker-compose-$(CFG_NODE_COUNT)-node.yml up
 
 .PHONY: antithesis-run-container-traffic
 antithesis-run-container-traffic: validate-node-count
-	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker compose -f docker-compose-$(CFG_NODE_COUNT)-node.yml exec client /opt/antithesis/test/v1/robustness/singleton_driver_traffic
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && \
+		docker compose -f config/docker-compose-$(CFG_NODE_COUNT)-node.yml exec client /opt/antithesis/test/v1/robustness/singleton_driver_traffic
 
 .PHONY: antithesis-run-container-validation
 antithesis-run-container-validation: validate-node-count
-	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker compose -f docker-compose-$(CFG_NODE_COUNT)-node.yml exec client /opt/antithesis/test/v1/robustness/finally_validation
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && \
+		docker compose -f config/docker-compose-$(CFG_NODE_COUNT)-node.yml exec client /opt/antithesis/test/v1/robustness/finally_validation
 
 .PHONY: antithesis-run-local-traffic
 antithesis-run-local-traffic:
@@ -62,7 +65,8 @@ antithesis-run-local-validation:
 
 .PHONY: antithesis-clean
 antithesis-clean: validate-node-count
-	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker compose -f docker-compose-$(CFG_NODE_COUNT)-node.yml down --remove-orphans
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && \
+		docker compose -f config/docker-compose-$(CFG_NODE_COUNT)-node.yml down --remove-orphans
 	rm -rf /tmp/etcddata0 /tmp/etcddata1 /tmp/etcddata2 /tmp/etcdreport
 
 .PHONY: validate-node-count
diff --git a/tests/antithesis/Dockerfile.config b/tests/antithesis/config/Dockerfile
similarity index 94%
rename from tests/antithesis/Dockerfile.config
rename to tests/antithesis/config/Dockerfile
index 9c796f6f3..4ae172f66 100644
--- a/tests/antithesis/Dockerfile.config
+++ b/tests/antithesis/config/Dockerfile
@@ -1,4 +1,4 @@
-ARG GO_VERSION=1.24.4
+ARG GO_VERSION=1.24.5
 
 FROM golang:$GO_VERSION AS build
 RUN go install github.com/a8m/envsubst/cmd/envsubst@v1.4.3
diff --git a/tests/antithesis/docker-compose-1-node.yml b/tests/antithesis/config/docker-compose-1-node.yml
similarity index 94%
rename from tests/antithesis/docker-compose-1-node.yml
rename to tests/antithesis/config/docker-compose-1-node.yml
index 37f8fe9ce..646c04a3d 100644
--- a/tests/antithesis/docker-compose-1-node.yml
+++ b/tests/antithesis/config/docker-compose-1-node.yml
@@ -32,6 +32,9 @@ services:
       ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380"
       ETCD_INITIAL_CLUSTER_STATE: "new"
       ETCD_DATA_DIR: "/var/etcd/data"
+      ETCD_SNAPSHOT_CATCHUP_ENTRIES: 100
+      ETCD_SNAPSHOT_COUNT: 50
+      ETCD_COMPACTION_BATCH_LIMIT: 10
     user: "${USER_ID:-root}:${GROUP_ID:-root}"
     depends_on:
       init:
diff --git a/tests/antithesis/docker-compose-3-node.yml b/tests/antithesis/config/docker-compose-3-node.yml
similarity index 92%
rename from tests/antithesis/docker-compose-3-node.yml
rename to tests/antithesis/config/docker-compose-3-node.yml
index e3ea9df8f..002f58737 100644
--- a/tests/antithesis/docker-compose-3-node.yml
+++ b/tests/antithesis/config/docker-compose-3-node.yml
@@ -34,6 +34,9 @@ services:
       ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"
       ETCD_INITIAL_CLUSTER_STATE: "new"
       ETCD_DATA_DIR: "/var/etcd/data"
+      ETCD_SNAPSHOT_CATCHUP_ENTRIES: 100
+      ETCD_SNAPSHOT_COUNT: 50
+      ETCD_COMPACTION_BATCH_LIMIT: 10
     user: "${USER_ID:-root}:${GROUP_ID:-root}"
     depends_on:
       init:
@@ -57,6 +60,9 @@ services:
       ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"
       ETCD_INITIAL_CLUSTER_STATE: "new"
       ETCD_DATA_DIR: "/var/etcd/data"
+      ETCD_SNAPSHOT_CATCHUP_ENTRIES: 100
+      ETCD_SNAPSHOT_COUNT: 50
+      ETCD_COMPACTION_BATCH_LIMIT: 10
     user: "${USER_ID:-root}:${GROUP_ID:-root}"
     depends_on:
       init:
@@ -80,6 +86,9 @@ services:
       ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"
       ETCD_INITIAL_CLUSTER_STATE: "new"
       ETCD_DATA_DIR: "/var/etcd/data"
+      ETCD_SNAPSHOT_CATCHUP_ENTRIES: 100
+      ETCD_SNAPSHOT_COUNT: 50
+      ETCD_COMPACTION_BATCH_LIMIT: 10
     user: "${USER_ID:-root}:${GROUP_ID:-root}"
     depends_on:
       init:
diff --git a/tests/antithesis/server/Dockerfile b/tests/antithesis/server/Dockerfile
index 415811a64..5bec3510d 100644
--- a/tests/antithesis/server/Dockerfile
+++ b/tests/antithesis/server/Dockerfile
@@ -1,4 +1,4 @@
-ARG GO_VERSION=1.24.4
+ARG GO_VERSION=1.24.5
 ARG ARCH=amd64
 
 FROM golang:$GO_VERSION
diff --git a/tests/antithesis/test-template/Dockerfile b/tests/antithesis/test-template/Dockerfile
index 04f9ee66e..55d43feab 100644
--- a/tests/antithesis/test-template/Dockerfile
+++ b/tests/antithesis/test-template/Dockerfile
@@ -1,4 +1,4 @@
-ARG GO_VERSION=1.24.4
+ARG GO_VERSION=1.24.5
 ARG ARCH=amd64
 
 FROM golang:$GO_VERSION
diff --git a/tests/antithesis/test-template/robustness/traffic/main.go b/tests/antithesis/test-template/robustness/traffic/main.go
index b067c8994..75424b92b 100644
--- a/tests/antithesis/test-template/robustness/traffic/main.go
+++ b/tests/antithesis/test-template/robustness/traffic/main.go
@@ -43,7 +43,8 @@ var (
 		MinimalQPS:                     100,
 		MaximalQPS:                     1000,
 		BurstableQPS:                   1000,
-		ClientCount:                    3,
+		MemberClientCount:              3,
+		ClusterClientCount:             1,
 		MaxNonUniqueRequestConcurrency: 3,
 	}
 	trafficNames = []string{
@@ -145,7 +146,7 @@ func simulateTraffic(ctx context.Context, tf traffic.Traffic, hosts []string, cl
 	concurrencyLimiter := traffic.NewConcurrencyLimiter(profile.MaxNonUniqueRequestConcurrency)
 	finish := closeAfter(ctx, duration)
 	keyStore := traffic.NewKeyStore(10, "key")
-	for i := range profile.ClientCount {
+	for i := range profile.MemberClientCount {
 		c := connect(clientSet, []string{hosts[i%len(hosts)]})
 		wg.Add(1)
 		go func(c *client.RecordingClient) {
@@ -160,6 +161,21 @@ func simulateTraffic(ctx context.Context, tf traffic.Traffic, hosts []string, cl
 			)
 		}(c)
 	}
+	for range profile.ClusterClientCount {
+		c := connect(clientSet, hosts)
+		wg.Add(1)
+		go func(c *client.RecordingClient) {
+			defer wg.Done()
+			defer c.Close()
+			tf.RunTrafficLoop(ctx, c, limiter,
+				clientSet.IdentityProvider(),
+				storage,
+				concurrencyLimiter,
+				keyStore,
+				finish,
+			)
+		}(c)
+	}
 	wg.Add(1)
 	compactClient := connect(clientSet, hosts)
 	go func(c *client.RecordingClient) {
diff --git a/tests/go.mod b/tests/go.mod
index 11d7900dc..f493da43f 100644
--- a/tests/go.mod
+++ b/tests/go.mod
@@ -2,10 +2,11 @@ module go.etcd.io/etcd/tests/v3
 
 go 1.24
 
-toolchain go1.24.4
+toolchain go1.24.5
 
 replace (
 	go.etcd.io/etcd/api/v3 => ../api
+	go.etcd.io/etcd/cache/v3 => ../cache
 	go.etcd.io/etcd/client/pkg/v3 => ../client/pkg
 	go.etcd.io/etcd/client/v3 => ../client/v3
 	go.etcd.io/etcd/etcdctl/v3 => ../etcdctl
@@ -22,15 +23,17 @@ require (
 	github.com/golang/protobuf v1.5.4
 	github.com/google/go-cmp v0.7.0
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
-	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
+	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
+	github.com/olekukonko/tablewriter v1.0.8
 	github.com/prometheus/client_golang v1.22.0
 	github.com/prometheus/client_model v0.6.2
-	github.com/prometheus/common v0.64.0
+	github.com/prometheus/common v0.65.0
 	github.com/soheilhy/cmux v0.1.5
 	github.com/stretchr/testify v1.10.0
-	go.etcd.io/bbolt v1.4.1
+	go.etcd.io/bbolt v1.4.2
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
+	go.etcd.io/etcd/cache/v3 v3.6.1
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0
@@ -39,10 +42,10 @@ require (
 	go.etcd.io/etcd/server/v3 v3.6.0-alpha.0
 	go.etcd.io/gofail v0.2.0
 	go.etcd.io/raft/v3 v3.6.0
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0
-	go.opentelemetry.io/otel v1.36.0
-	go.opentelemetry.io/otel/sdk v1.36.0
-	go.opentelemetry.io/otel/trace v1.36.0
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0
+	go.opentelemetry.io/otel v1.37.0
+	go.opentelemetry.io/otel/sdk v1.37.0
+	go.opentelemetry.io/otel/trace v1.37.0
 	go.opentelemetry.io/proto/otlp v1.7.0
 	go.uber.org/zap v1.27.0
 	golang.org/x/crypto v0.39.0
@@ -64,7 +67,7 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/fatih/color v1.18.0 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
@@ -80,7 +83,6 @@ require (
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
 	github.com/olekukonko/ll v0.0.8 // indirect
-	github.com/olekukonko/tablewriter v1.0.7 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
@@ -90,16 +92,17 @@ require (
 	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
-	go.opentelemetry.io/otel/metric v1.36.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
+	go.opentelemetry.io/otel/metric v1.37.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/net v0.41.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.26.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/yaml v1.4.0 // indirect
+	sigs.k8s.io/yaml v1.5.0 // indirect
 )
diff --git a/tests/go.sum b/tests/go.sum
index 30546f641..04864ac69 100644
--- a/tests/go.sum
+++ b/tests/go.sum
@@ -43,8 +43,8 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -65,7 +65,6 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -74,12 +73,12 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
-github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
+github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
@@ -108,8 +107,8 @@ github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hS
 github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
 github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
 github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
-github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
+github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -120,8 +119,8 @@ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
-github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
+github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
+github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -152,30 +151,30 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.4.1 h1:5mOV+HWjIPLEAlUGMsveaUvK2+byZMFOzojoi7bh7uI=
-go.etcd.io/bbolt v1.4.1/go.mod h1:c8zu2BnXWTu2XM4XcICtbGSl9cFwsXtcf9zLt2OncM8=
+go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
+go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
 go.etcd.io/gofail v0.2.0 h1:p19drv16FKK345a09a1iubchlw/vmRuksmRzgBIGjcA=
 go.etcd.io/gofail v0.2.0/go.mod h1:nL3ILMGfkXTekKI3clMBNazKnjUZjYLKmBHzsVAnC1o=
 go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
-go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
-go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
-go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
-go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
-go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
-go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
-go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
 go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
 go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -187,6 +186,10 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
+go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -249,10 +252,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -273,5 +276,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
-sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
+sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
diff --git a/tests/integration/cache_test.go b/tests/integration/cache_test.go
new file mode 100644
index 000000000..0b7e893ba
--- /dev/null
+++ b/tests/integration/cache_test.go
@@ -0,0 +1,308 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package integration
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	cache "go.etcd.io/etcd/cache/v3"
+	clientv3 "go.etcd.io/etcd/client/v3"
+	"go.etcd.io/etcd/tests/v3/framework/integration"
+)
+
+func TestCacheWatch(t *testing.T) {
+	integration.BeforeTest(t)
+	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
+	t.Cleanup(func() { clus.Terminate(t) })
+	client := clus.Client(0)
+
+	c, err := cache.New(client, "/", cache.WithHistoryWindowSize(32))
+	if err != nil {
+		t.Fatalf("New(...): %v", err)
+	}
+	t.Cleanup(c.Close)
+	if err := c.WaitReady(t.Context()); err != nil {
+		t.Fatalf("cache not ready: %v", err)
+	}
+	testWatch(t, client.KV, c)
+}
+
+func TestWatch(t *testing.T) {
+	integration.BeforeTest(t)
+	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
+	t.Cleanup(func() { clus.Terminate(t) })
+	client := clus.Client(0)
+
+	testWatch(t, client.KV, client.Watcher)
+}
+
+func testWatch(t *testing.T, kv clientv3.KV, watcher Watcher) {
+	ctx := t.Context()
+	event1Put := &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/a"),
+			Value:          []byte("1"),
+			CreateRevision: 2,
+			ModRevision:    2,
+			Version:        1,
+		},
+	}
+	event2Put := &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/b"),
+			Value:          []byte("2"),
+			CreateRevision: 3,
+			ModRevision:    3,
+			Version:        1,
+		},
+	}
+	event3Delete := &clientv3.Event{
+		Type: clientv3.EventTypeDelete,
+		Kv: &mvccpb.KeyValue{
+			Key:         []byte("/a"),
+			ModRevision: 4,
+		},
+	}
+	event4Put := &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/a"),
+			Value:          []byte("3"),
+			CreateRevision: 5,
+			ModRevision:    5,
+			Version:        1,
+		},
+	}
+	event5Delete := &clientv3.Event{
+		Type: clientv3.EventTypeDelete,
+		Kv: &mvccpb.KeyValue{
+			Key:         []byte("/b"),
+			ModRevision: 5,
+		},
+	}
+	tcs := []struct {
+		name       string
+		key        string
+		opts       []clientv3.OpOption
+		wantEvents []*clientv3.Event
+	}{
+		{
+			name:       "Watch all events",
+			key:        "/",
+			opts:       []clientv3.OpOption{clientv3.WithPrefix()},
+			wantEvents: []*clientv3.Event{event1Put, event2Put, event3Delete, event4Put, event5Delete},
+		},
+	}
+	t.Log("Open test watchers")
+	watches := make([]clientv3.WatchChan, len(tcs))
+	for i, tc := range tcs {
+		watches[i] = watcher.Watch(ctx, tc.key, tc.opts...)
+	}
+	t.Log("Setup data")
+	if _, err := kv.Put(ctx, string(event1Put.Kv.Key), string(event1Put.Kv.Value)); err != nil {
+		t.Fatalf("Put: %v", err)
+	}
+	if _, err := kv.Put(ctx, string(event2Put.Kv.Key), string(event2Put.Kv.Value)); err != nil {
+		t.Fatalf("Put: %v", err)
+	}
+	if _, err := kv.Delete(ctx, string(event3Delete.Kv.Key)); err != nil {
+		t.Fatalf("Delete: %v", err)
+	}
+	if _, err := kv.Txn(ctx).Then(clientv3.OpPut(string(event4Put.Kv.Key), string(event4Put.Kv.Value)), clientv3.OpDelete(string(event5Delete.Kv.Key))).Commit(); err != nil {
+		t.Fatalf("Txn: %v", err)
+	}
+	t.Log("Validate")
+	for i, tc := range tcs {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			events, _ := readEvents(watches[i])
+			if diff := cmp.Diff(tc.wantEvents, events); diff != "" {
+				t.Errorf("unexpected events (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestCacheLaggingWatcher(t *testing.T) {
+	const prefix = "/test/"
+	integration.BeforeTest(t)
+	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
+	t.Cleanup(func() { clus.Terminate(t) })
+	client := clus.Client(0)
+
+	tests := []struct {
+		name                string
+		window              int
+		eventCount          int
+		wantExactEventCount int
+		wantAtMaxEventCount int
+		wantClosed          bool
+	}{
+		{
+			name:                "all event fit",
+			window:              10,
+			eventCount:          9,
+			wantExactEventCount: 9,
+			wantClosed:          false,
+		},
+		{
+			name:                "events fill window",
+			window:              10,
+			eventCount:          10,
+			wantExactEventCount: 10,
+			wantClosed:          false,
+		},
+		{
+			name:                "event fill pipeline",
+			window:              10,
+			eventCount:          11,
+			wantExactEventCount: 11,
+			wantClosed:          false,
+		},
+		{
+			name:                "pipeline overflow",
+			window:              10,
+			eventCount:          12,
+			wantAtMaxEventCount: 1, // Either 0 or 1.
+			wantClosed:          true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c, err := cache.New(
+				client, prefix,
+				cache.WithHistoryWindowSize(tt.window),
+				cache.WithPerWatcherBufferSize(0),
+				cache.WithResyncInterval(10*time.Millisecond),
+			)
+			if err != nil {
+				t.Fatalf("New(...): %v", err)
+			}
+			defer c.Close()
+
+			if err := c.WaitReady(t.Context()); err != nil {
+				t.Fatalf("cache not ready: %v", err)
+			}
+			ch := c.Watch(t.Context(), prefix, clientv3.WithPrefix())
+
+			generateEvents(t, client, prefix, tt.eventCount)
+			gotEvents, ok := readEvents(ch)
+			closed := !ok
+
+			if tt.wantExactEventCount != 0 && tt.wantExactEventCount != len(gotEvents) {
+				t.Errorf("gotEvents=%v, wantEvents=%v", len(gotEvents), tt.wantExactEventCount)
+			}
+			if tt.wantAtMaxEventCount != 0 && len(gotEvents) > tt.wantAtMaxEventCount {
+				t.Errorf("gotEvents=%v, wantEvents<%v", len(gotEvents), tt.wantAtMaxEventCount)
+			}
+			if closed != tt.wantClosed {
+				t.Errorf("closed=%v, wantClosed=%v", closed, tt.wantClosed)
+			}
+		})
+	}
+}
+
+func TestCacheRejectsUnsupportedWatch(t *testing.T) {
+	integration.BeforeTest(t)
+	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
+	t.Cleanup(func() { clus.Terminate(t) })
+	client := clus.Client(0)
+
+	ctx := t.Context()
+
+	c, err := cache.New(client, "")
+	if err != nil {
+		t.Fatalf("New(...): %v", err)
+	}
+	t.Cleanup(c.Close)
+	if err := c.WaitReady(ctx); err != nil {
+		t.Fatal(err)
+	}
+
+	tests := []struct {
+		name string
+		key  string
+		opts []clientv3.OpOption
+	}{
+		{
+			name: "non_zero_start_revision",
+			key:  "",
+			opts: []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(123)},
+		},
+		{
+			name: "subprefix_watch",
+			key:  "foo/",
+			opts: []clientv3.OpOption{clientv3.WithPrefix()},
+		},
+		{
+			name: "single_key_watch",
+			key:  "foo/bar",
+			opts: nil, // exact-key watch (no WithPrefix)
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			watchCh := c.Watch(ctx, tt.key, tt.opts...)
+			resp, ok := <-watchCh
+
+			if !ok || !resp.Canceled {
+				t.Errorf("expected canceled response, got %#v (closed=%v)", resp, !ok)
+			}
+		})
+	}
+}
+
+func generateEvents(t *testing.T, client *clientv3.Client, prefix string, n int) {
+	t.Helper()
+	for i := 0; i < n; i++ {
+		key := fmt.Sprintf("%s%d", prefix, i)
+		if _, err := client.Put(t.Context(), key, fmt.Sprintf("%d", i)); err != nil {
+			t.Fatalf("Put(%q): %v", key, err)
+		}
+	}
+}
+
+type Watcher interface {
+	Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan
+}
+
+func readEvents(watch clientv3.WatchChan) (events []*clientv3.Event, ok bool) {
+	deadline := time.After(time.Second)
+	for {
+		select {
+		case resp, ok := <-watch:
+			if !ok {
+				return events, false
+			}
+			events = append(events, resp.Events...)
+		case <-deadline:
+			return events, true
+		case <-time.After(100 * time.Millisecond):
+			return events, true
+		}
+	}
+}
diff --git a/tests/integration/tracing_test.go b/tests/integration/tracing_test.go
index 503c224ac..00e026361 100644
--- a/tests/integration/tracing_test.go
+++ b/tests/integration/tracing_test.go
@@ -50,6 +50,28 @@ func TestTracing(t *testing.T) {
 		})
 	})
 
+	t.Run("UnaryGetRPC", func(t *testing.T) {
+		testRPCTracing(t, "UnaryGetWithCountOnlyRPC", containsRangeSpan, func(cli *clientv3.Client) error {
+			// make a request with the instrumented client
+			resp, err := cli.Get(t.Context(), "key", clientv3.WithCountOnly())
+			require.NoError(t, err)
+			require.Empty(t, resp.Kvs)
+			return nil
+		})
+	})
+
+	t.Run("UnaryTxnRPC", func(t *testing.T) {
+		testRPCTracing(t, "UnaryTxnRPC", containsTxnSpan, func(cli *clientv3.Client) error {
+			// make a request with the instrumented client
+			_, err := cli.Txn(t.Context()).
+				If(clientv3.Compare(clientv3.ModRevision("key"), "=", 1)).
+				Then(clientv3.OpGet("key")).
+				Commit()
+			require.NoError(t, err)
+			return nil
+		})
+	})
+
 	// Test Stream RPC tracing
 	t.Run("StreamRPC", func(t *testing.T) {
 		testRPCTracing(t, "StreamRPC", containsStreamRPCSpan, func(cli *clientv3.Client) error {
@@ -173,6 +195,52 @@ func containsUnaryRPCSpan(req *traceservice.ExportTraceServiceRequest) bool {
 	return false
 }
 
+func containsRangeSpan(req *traceservice.ExportTraceServiceRequest) bool {
+	for _, resourceSpans := range req.GetResourceSpans() {
+		for _, attr := range resourceSpans.GetResource().GetAttributes() {
+			if attr.GetKey() != "service.name" && attr.GetValue().GetStringValue() != "integration-test-tracing" {
+				continue
+			}
+			for _, scoped := range resourceSpans.GetScopeSpans() {
+				for _, span := range scoped.GetSpans() {
+					if span.GetName() == "range" {
+						for _, spanAttr := range span.GetAttributes() {
+							if spanAttr.GetKey() == "count_only" && spanAttr.GetValue().GetBoolValue() {
+								return true
+							}
+						}
+						return false
+					}
+				}
+			}
+		}
+	}
+	return false
+}
+
+func containsTxnSpan(req *traceservice.ExportTraceServiceRequest) bool {
+	for _, resourceSpans := range req.GetResourceSpans() {
+		for _, attr := range resourceSpans.GetResource().GetAttributes() {
+			if attr.GetKey() != "service.name" && attr.GetValue().GetStringValue() != "integration-test-tracing" {
+				continue
+			}
+			for _, scoped := range resourceSpans.GetScopeSpans() {
+				for _, span := range scoped.GetSpans() {
+					if span.GetName() == "txn" {
+						for _, spanAttr := range span.GetAttributes() {
+							if spanAttr.GetKey() == "compare_first_key" && spanAttr.GetValue().GetStringValue() == "key" {
+								return true
+							}
+						}
+						return false
+					}
+				}
+			}
+		}
+	}
+	return false
+}
+
 // containsStreamRPCSpan checks for Watch/Watch spans in trace data
 func containsStreamRPCSpan(req *traceservice.ExportTraceServiceRequest) bool {
 	for _, resourceSpans := range req.GetResourceSpans() {
diff --git a/tests/robustness/README.md b/tests/robustness/README.md
index b1ea6bae6..4b33960be 100644
--- a/tests/robustness/README.md
+++ b/tests/robustness/README.md
@@ -22,6 +22,7 @@ The purpose of these tests is to rigorously validate that etcd maintains its [KV
 | Watch dropping an event when compacting on delete [#18089]                   | May 2024   | v3.4 or earlier   | Robustness      | Yes, after covering of compaction                 | `make test-robustness-issue18089`   |
 | Inconsistency when reading compacted revision in TXN [#18667]                | Oct 2024   | v3.4 or earlier   | User            |                                                   |                                     |
 | Missing delete event on watch opened on same revision as compaction [#19179] | Jan 2025   | v3.4 or earlier   | Robustness      | Yes, after covering of compaction                 | `make test-robustness-issue19179`   |
+| Watch on future revision returns old events or notifications [#20221]        | Jun 2025   | v3.4 or earlier   | Robustness      | Yes, after covering connection to multiple members|                                     |
 
 [#13766]: https://github.com/etcd-io/etcd/issues/13766
 [#14370]: https://github.com/etcd-io/etcd/issues/14370
@@ -35,7 +36,7 @@ The purpose of these tests is to rigorously validate that etcd maintains its [KV
 [#18089]: https://github.com/etcd-io/etcd/issues/18089
 [#18667]: https://github.com/etcd-io/etcd/issues/18667
 [#19179]: https://github.com/etcd-io/etcd/issues/19179
-
+[#20221]: https://github.com/etcd-io/etcd/issues/20221
 
 ## How Robustness Tests Work
 
diff --git a/tests/robustness/client/watch.go b/tests/robustness/client/watch.go
index f71febc57..dd35de89c 100644
--- a/tests/robustness/client/watch.go
+++ b/tests/robustness/client/watch.go
@@ -18,7 +18,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"testing"
 
 	"go.uber.org/zap"
 	"golang.org/x/sync/errgroup"
@@ -123,21 +122,3 @@ resetWatch:
 		}
 	}
 }
-
-func ValidateGotAtLeastOneProgressNotify(t *testing.T, reports []report.ClientReport, expectProgressNotify bool) {
-	gotProgressNotify := false
-external:
-	for _, r := range reports {
-		for _, op := range r.Watch {
-			for _, resp := range op.Responses {
-				if resp.IsProgressNotify {
-					gotProgressNotify = true
-					break external
-				}
-			}
-		}
-	}
-	if gotProgressNotify != expectProgressNotify {
-		t.Errorf("Progress notify does not match, expect: %v, got: %v", expectProgressNotify, gotProgressNotify)
-	}
-}
diff --git a/tests/robustness/coverage/README.md b/tests/robustness/coverage/README.md
new file mode 100644
index 000000000..7f0c54f2a
--- /dev/null
+++ b/tests/robustness/coverage/README.md
@@ -0,0 +1,60 @@
+## Overview
+
+Go tests in this directory analyze the usage of Etcd API based on collected
+traces from a Kubernetes cluster. They output information on:
+
+1. Number of calls per gRPC method used by Kubernetes
+
+This information can be used to track the coverage of k8s-etcd contract.
+
+### Manual test execution
+
+At first we will manually set up the cluster, run e2e tests, download traces and
+then execute the test.
+
+1\. Set up [KIND
+cluster](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) with
+tracing exporting to [Jaeger](https://www.jaegertracing.io/)
+
+```
+export KUBECONFIG="$(pwd)/kind-with-tracing-config"
+kind create cluster --config kind-with-tracing.yaml
+kubectl run jaeger --overrides='{ "apiVersion": "v1", "spec": { "hostNetwork": true, "nodeName": "kind-control-plane", "tolerations": [{"effect": "NoExecute", "operator": "Exists"}]} }' \
+  --labels='tier=control-plane' \
+  --image jaegertracing/jaeger:2.6.0 \
+  -- --set=extensions.jaeger_storage.backends.some_storage.memory.max_traces=2000000
+```
+
+2\. Exercise Kubernetes API. For example, build and run Conformance tests from
+Kubernetes repository (this usually takes 30-40m or will time out after 1 hour):
+
+```
+export KUBECONFIG="$(pwd)/kind-with-tracing-config"
+kind export kubeconfig
+make WHAT="test/e2e/e2e.test"
+./_output/bin/e2e.test -context kind-kind -ginkgo.focus=".*Conformance" -num-nodes 2
+```
+
+3\. Download traces and put them into `tests/robustness/coverage/testdata`
+directory in Etcd git repository:
+
+```
+kubectl port-forward jaeger --address localhost --address :: 16686:16686 &
+curl -v --get --retry 10 --retry-connrefused -o testdata/demo_traces.json \
+  -H "Content-Type: application/json" \
+  --data-urlencode "query.start_time_min=$(date --date="5 days ago" -Ins)" \
+  --data-urlencode "query.start_time_max=$(date -Ins)" \
+  --data-urlencode "query.service_name=etcd" \
+  "http://127.0.0.1:16686/api/v3/traces"
+kill $!
+```
+
+4\. Run Go test
+
+```
+go test -v -timeout 60s go.etcd.io/etcd/tests/v3/robustness/coverage
+```
+
+### Automated test execution
+
+Work on improving these tests is tracked in https://github.com/etcd-io/etcd/issues/20182
diff --git a/tests/robustness/coverage/coverage_test.go b/tests/robustness/coverage/coverage_test.go
new file mode 100644
index 000000000..9faf97eda
--- /dev/null
+++ b/tests/robustness/coverage/coverage_test.go
@@ -0,0 +1,149 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package coverage_test
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/olekukonko/tablewriter"
+	"github.com/olekukonko/tablewriter/tw"
+	traceservice "go.opentelemetry.io/proto/otlp/collector/trace/v1"
+	tracev1 "go.opentelemetry.io/proto/otlp/trace/v1"
+	"google.golang.org/protobuf/encoding/protojson"
+)
+
+func TestInterfaceUse(t *testing.T) {
+	files, err := os.ReadDir("testdata")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for _, file := range files {
+		filename := file.Name()
+		if filename == ".gitignore" {
+			continue
+		}
+		t.Run(filename, func(t *testing.T) { testInterfaceUse(t, filename) })
+	}
+}
+
+func testInterfaceUse(t *testing.T, filename string) {
+	b, err := os.ReadFile(filepath.Join("testdata", filename))
+	if err != nil {
+		t.Fatalf("read test data: %v", err)
+	}
+	var dump Dump
+	err = json.Unmarshal(b, &dump)
+	if err != nil {
+		t.Fatalf("unmarshal testdata %s: %v", filename, err)
+	}
+	if dump.Result == nil {
+		t.Fatalf("missing result data")
+	}
+	traces := dump.Result
+
+	callsByOperationName := make(map[string]int)
+	for _, trace := range traces.GetResourceSpans() {
+		serviceName := getServiceName(trace)
+		if serviceName != "etcd" {
+			continue
+		}
+		opName := getOperationName(trace)
+		callsByOperationName[opName]++
+	}
+	t.Logf("\n%s", printableCallTable(callsByOperationName))
+
+	knownMethodsUsedByKubernetes := map[string]bool{
+		"etcdserverpb.KV/Range":           true, // All calls should go through etcd-k8s interface
+		"etcdserverpb.KV/Txn":             true, // All calls should go through etcd-k8s interface
+		"etcdserverpb.KV/Compact":         true, // Compaction should move to using internal Etcd mechanism
+		"etcdserverpb.Watch/Watch":        true, // Not part of the contract interface (yet)
+		"etcdserverpb.Lease/LeaseGrant":   true, // Used to manage masterleases and events
+		"etcdserverpb.Maintenance/Status": true, // Used to expose database size on apiserver's metrics endpoint
+	}
+	for method := range knownMethodsUsedByKubernetes {
+		t.Run(method, func(t *testing.T) {
+			if _, ok := callsByOperationName[method]; !ok {
+				t.Errorf("expected %q method to be called at least once", method)
+			}
+		})
+	}
+	t.Run("only_expected_methods_were_called", func(t *testing.T) {
+		for opName := range callsByOperationName {
+			if !knownMethodsUsedByKubernetes[opName] {
+				t.Errorf("method called outside the list: %s", opName)
+			}
+		}
+	})
+}
+
+type Traces struct {
+	traceservice.ExportTraceServiceRequest
+}
+
+func (t *Traces) UnmarshalJSON(b []byte) error {
+	return protojson.Unmarshal(b, &t.ExportTraceServiceRequest)
+}
+
+type Dump struct {
+	Result *Traces `json:"result"`
+}
+
+func getServiceName(trace *tracev1.ResourceSpans) string {
+	for _, kv := range trace.GetResource().GetAttributes() {
+		if kv.GetKey() == "service.name" {
+			return kv.GetValue().GetStringValue()
+		}
+	}
+	return ""
+}
+
+func getOperationName(trace *tracev1.ResourceSpans) string {
+	for _, scopeSpan := range trace.GetScopeSpans() {
+		for _, span := range scopeSpan.GetSpans() {
+			name := span.GetName()
+			if strings.HasPrefix(name, "etcdserverpb") {
+				return name
+			}
+		}
+	}
+	return ""
+}
+
+func printableCallTable(callsByOperationName map[string]int) string {
+	buf := new(bytes.Buffer)
+	cfgBuilder := tablewriter.NewConfigBuilder().WithRowAlignment(tw.AlignRight)
+	table := tablewriter.NewTable(buf, tablewriter.WithConfig(cfgBuilder.Build()))
+	table.Header("method", "calls", "percent")
+
+	totalCalls := 0
+	for _, c := range callsByOperationName {
+		totalCalls += c
+	}
+
+	for opName, callCount := range callsByOperationName {
+		table.Append(opName, callCount, fmt.Sprintf("%.2f%%", float64(callCount*100)/float64(totalCalls)))
+	}
+	table.Footer("total", totalCalls, "100.00%")
+
+	table.Render()
+	return buf.String()
+}
diff --git a/tests/robustness/coverage/kind-with-tracing.yaml b/tests/robustness/coverage/kind-with-tracing.yaml
new file mode 100644
index 000000000..612452821
--- /dev/null
+++ b/tests/robustness/coverage/kind-with-tracing.yaml
@@ -0,0 +1,17 @@
+---
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+nodes:
+  - role: control-plane
+    kubeadmConfigPatches:
+      - |
+        kind: ClusterConfiguration
+        etcd:
+          local:
+            extraArgs:
+              experimental-enable-distributed-tracing: "true"
+              experimental-distributed-tracing-address: "0.0.0.0:4317"
+              experimental-distributed-tracing-service-name: "etcd"
+              experimental-distributed-tracing-sampling-rate: "1000000"
+  - role: worker
+  - role: worker
diff --git a/tests/robustness/coverage/testdata/.gitignore b/tests/robustness/coverage/testdata/.gitignore
new file mode 100644
index 000000000..c96a04f00
--- /dev/null
+++ b/tests/robustness/coverage/testdata/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/tests/robustness/main_test.go b/tests/robustness/main_test.go
index ba3971327..4d1ef5295 100644
--- a/tests/robustness/main_test.go
+++ b/tests/robustness/main_test.go
@@ -114,11 +114,6 @@ func testRobustness(ctx context.Context, t *testing.T, lg *zap.Logger, s scenari
 		t.Error(err)
 	}
 
-	failpointImpactingWatch := s.Failpoint == failpoint.SleepBeforeSendWatchResponse
-	if !failpointImpactingWatch {
-		watchProgressNotifyEnabled := c.Cfg.ServerConfig.WatchProgressNotifyInterval != 0
-		client.ValidateGotAtLeastOneProgressNotify(t, r.Client, s.Watch.RequestProgress || watchProgressNotifyEnabled)
-	}
 	validateConfig := validate.Config{ExpectRevisionUnique: s.Traffic.ExpectUniqueRevision()}
 	result := validate.ValidateAndReturnVisualize(lg, validateConfig, r.Client, persistedRequests, 5*time.Minute)
 	r.Visualize = result.Linearization.Visualize
diff --git a/tests/robustness/model/deterministic.go b/tests/robustness/model/deterministic.go
index dcadb3851..b4e789296 100644
--- a/tests/robustness/model/deterministic.go
+++ b/tests/robustness/model/deterministic.go
@@ -16,9 +16,7 @@ package model
 
 import (
 	"encoding/json"
-	"errors"
 	"fmt"
-	"hash/fnv"
 	"html"
 	"maps"
 	"reflect"
@@ -29,11 +27,6 @@ import (
 	"go.etcd.io/etcd/server/v3/storage/mvcc"
 )
 
-// RevisionForNonLinearizableResponse is a fake revision value used to
-// separate responses for requests that are not linearizable,
-// thus we need to ignore it in model when checking linearization.
-const RevisionForNonLinearizableResponse = -1
-
 // DeterministicModel assumes a deterministic execution of etcd requests. All
 // requests that client called were executed and persisted by etcd. This
 // assumption is good for simulating etcd behavior (aka writing a fake), but not
@@ -127,127 +120,152 @@ func freshEtcdState() EtcdState {
 
 // Step handles a successful request, returning updated state and response it would generate.
 func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
-	// TODO: Avoid copying when TXN only has read operations
-	if request.Type == Range {
-		if request.Range.Revision == 0 || request.Range.Revision == s.Revision {
-			resp := s.getRange(request.Range.RangeOptions)
-			return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Range: &resp, Revision: s.Revision}}
-		}
-		if request.Range.Revision > s.Revision {
-			return s, MaybeEtcdResponse{Error: ErrEtcdFutureRev.Error()}
-		}
-		if request.Range.Revision < s.CompactRevision {
-			return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
-		}
-		return s, MaybeEtcdResponse{Persisted: true, PersistedRevision: s.Revision}
+	switch request.Type {
+	case Range:
+		return s.stepRange(request)
+	case Txn:
+		return s.stepTxn(request)
+	case LeaseGrant:
+		return s.stepLeaseGrant(request)
+	case LeaseRevoke:
+		return s.stepLeaseRevoke(request)
+	case Defragment:
+		return s.stepDefragment()
+	case Compact:
+		return s.stepCompact(request)
+	default:
+		panic(fmt.Sprintf("Unknown request type: %v", request.Type))
 	}
+}
 
+func (s EtcdState) stepRange(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	if request.Range.Revision == 0 || request.Range.Revision == s.Revision {
+		resp := s.getRange(request.Range.RangeOptions)
+		return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Range: &resp, Revision: s.Revision}}
+	}
+	if request.Range.Revision > s.Revision {
+		return s, MaybeEtcdResponse{Error: ErrEtcdFutureRev.Error()}
+	}
+	if request.Range.Revision < s.CompactRevision {
+		return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
+	}
+	return s, MaybeEtcdResponse{Persisted: true, PersistedRevision: s.Revision}
+}
+
+func (s EtcdState) stepTxn(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	// TODO: Avoid copying when TXN only has read operations
 	newState := s.DeepCopy()
-	switch request.Type {
-	case Txn:
-		failure := false
-		for _, cond := range request.Txn.Conditions {
-			val := newState.KeyValues[cond.Key]
-			if cond.ExpectedVersion > 0 {
-				if val.Version != cond.ExpectedVersion {
-					failure = true
-					break
-				}
-			} else if val.ModRevision != cond.ExpectedRevision {
+	failure := false
+	for _, cond := range request.Txn.Conditions {
+		val := newState.KeyValues[cond.Key]
+		if cond.ExpectedVersion > 0 {
+			if val.Version != cond.ExpectedVersion {
 				failure = true
 				break
 			}
+		} else if val.ModRevision != cond.ExpectedRevision {
+			failure = true
+			break
 		}
-		operations := request.Txn.OperationsOnSuccess
-		if failure {
-			operations = request.Txn.OperationsOnFailure
-		}
-		opResp := make([]EtcdOperationResult, len(operations))
-		increaseRevision := false
-		for i, op := range operations {
-			switch op.Type {
-			case RangeOperation:
-				opResp[i] = EtcdOperationResult{
-					RangeResponse: newState.getRange(op.Range),
-				}
-			case PutOperation:
-				_, leaseExists := newState.Leases[op.Put.LeaseID]
-				if op.Put.LeaseID != 0 && !leaseExists {
-					break
-				}
-				ver := int64(1)
-				if val, exists := newState.KeyValues[op.Put.Key]; exists && val.Version > 0 {
-					ver = val.Version + 1
-				}
-				newState.KeyValues[op.Put.Key] = ValueRevision{
-					Value:       op.Put.Value,
-					ModRevision: newState.Revision + 1,
-					Version:     ver,
-				}
+	}
+	operations := request.Txn.OperationsOnSuccess
+	if failure {
+		operations = request.Txn.OperationsOnFailure
+	}
+	opResp := make([]EtcdOperationResult, len(operations))
+	increaseRevision := false
+	for i, op := range operations {
+		switch op.Type {
+		case RangeOperation:
+			opResp[i] = EtcdOperationResult{
+				RangeResponse: newState.getRange(op.Range),
+			}
+		case PutOperation:
+			_, leaseExists := newState.Leases[op.Put.LeaseID]
+			if op.Put.LeaseID != 0 && !leaseExists {
+				break
+			}
+			ver := int64(1)
+			if val, exists := newState.KeyValues[op.Put.Key]; exists && val.Version > 0 {
+				ver = val.Version + 1
+			}
+			newState.KeyValues[op.Put.Key] = ValueRevision{
+				Value:       op.Put.Value,
+				ModRevision: newState.Revision + 1,
+				Version:     ver,
+			}
+			increaseRevision = true
+			newState = detachFromOldLease(newState, op.Put.Key)
+			if leaseExists {
+				newState = attachToNewLease(newState, op.Put.LeaseID, op.Put.Key)
+			}
+		case DeleteOperation:
+			if _, ok := newState.KeyValues[op.Delete.Key]; ok {
+				delete(newState.KeyValues, op.Delete.Key)
 				increaseRevision = true
-				newState = detachFromOldLease(newState, op.Put.Key)
-				if leaseExists {
-					newState = attachToNewLease(newState, op.Put.LeaseID, op.Put.Key)
-				}
-			case DeleteOperation:
-				if _, ok := newState.KeyValues[op.Delete.Key]; ok {
-					delete(newState.KeyValues, op.Delete.Key)
-					increaseRevision = true
-					newState = detachFromOldLease(newState, op.Delete.Key)
-					opResp[i].Deleted = 1
-				}
-			default:
-				panic("unsupported operation")
+				newState = detachFromOldLease(newState, op.Delete.Key)
+				opResp[i].Deleted = 1
 			}
+		default:
+			panic("unsupported operation")
 		}
-		if increaseRevision {
-			newState.Revision++
-		}
-		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Txn: &TxnResponse{Failure: failure, Results: opResp}, Revision: newState.Revision}}
-	case LeaseGrant:
-		// Empty LeaseID means the request failed and client didn't get response. Ignore it as client cannot use lease without knowing its id.
-		if request.LeaseGrant.LeaseID == 0 {
-			return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseGrant: &LeaseGrantReponse{}}}
-		}
-		lease := EtcdLease{
-			LeaseID: request.LeaseGrant.LeaseID,
-			Keys:    map[string]struct{}{},
-		}
-		newState.Leases[request.LeaseGrant.LeaseID] = lease
+	}
+	if increaseRevision {
+		newState.Revision++
+	}
+	return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Txn: &TxnResponse{Failure: failure, Results: opResp}, Revision: newState.Revision}}
+}
+
+func (s EtcdState) stepLeaseGrant(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	newState := s.DeepCopy()
+	// Empty LeaseID means the request failed and client didn't get response. Ignore it as client cannot use lease without knowing its id.
+	if request.LeaseGrant.LeaseID == 0 {
 		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseGrant: &LeaseGrantReponse{}}}
-	case LeaseRevoke:
-		// Delete the keys attached to the lease
-		keyDeleted := false
-		for key := range newState.Leases[request.LeaseRevoke.LeaseID].Keys {
-			// same as delete.
-			if _, ok := newState.KeyValues[key]; ok {
-				if !keyDeleted {
-					keyDeleted = true
-				}
-				delete(newState.KeyValues, key)
-				delete(newState.KeyLeases, key)
+	}
+	lease := EtcdLease{
+		LeaseID: request.LeaseGrant.LeaseID,
+		Keys:    map[string]struct{}{},
+	}
+	newState.Leases[request.LeaseGrant.LeaseID] = lease
+	return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseGrant: &LeaseGrantReponse{}}}
+}
+
+func (s EtcdState) stepLeaseRevoke(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	newState := s.DeepCopy()
+	// Delete the keys attached to the lease
+	keyDeleted := false
+	for key := range newState.Leases[request.LeaseRevoke.LeaseID].Keys {
+		// same as delete.
+		if _, ok := newState.KeyValues[key]; ok {
+			if !keyDeleted {
+				keyDeleted = true
 			}
+			delete(newState.KeyValues, key)
+			delete(newState.KeyLeases, key)
 		}
-		// delete the lease
-		delete(newState.Leases, request.LeaseRevoke.LeaseID)
-		if keyDeleted {
-			newState.Revision++
-		}
-		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseRevoke: &LeaseRevokeResponse{}}}
-	case Defragment:
-		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Defragment: &DefragmentResponse{}, Revision: RevisionForNonLinearizableResponse}}
-	case Compact:
-		if request.Compact.Revision <= newState.CompactRevision {
-			return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
-		}
-		if request.Compact.Revision > newState.Revision {
-			return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrFutureRev.Error()}}
-		}
-		newState.CompactRevision = request.Compact.Revision
-		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Compact: &CompactResponse{}, Revision: RevisionForNonLinearizableResponse}}
-	default:
-		panic(fmt.Sprintf("Unknown request type: %v", request.Type))
 	}
+	// delete the lease
+	delete(newState.Leases, request.LeaseRevoke.LeaseID)
+	if keyDeleted {
+		newState.Revision++
+	}
+	return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Revision: newState.Revision, LeaseRevoke: &LeaseRevokeResponse{}}}
+}
+
+func (s EtcdState) stepDefragment() (EtcdState, MaybeEtcdResponse) {
+	return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Defragment: &DefragmentResponse{}, Revision: RevisionForNonLinearizableResponse}}
+}
+
+func (s EtcdState) stepCompact(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
+	newState := s.DeepCopy()
+	if request.Compact.Revision <= newState.CompactRevision {
+		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
+	}
+	if request.Compact.Revision > newState.Revision {
+		return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrFutureRev.Error()}}
+	}
+	newState.CompactRevision = request.Compact.Revision
+	return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Compact: &CompactResponse{}, Revision: RevisionForNonLinearizableResponse}}
 }
 
 func (s EtcdState) getRange(options RangeOptions) RangeResponse {
@@ -295,204 +313,3 @@ func attachToNewLease(s EtcdState, leaseID int64, key string) EtcdState {
 	s.Leases[leaseID].Keys[key] = leased
 	return s
 }
-
-type RequestType string
-
-const (
-	Range       RequestType = "range"
-	Txn         RequestType = "txn"
-	LeaseGrant  RequestType = "leaseGrant"
-	LeaseRevoke RequestType = "leaseRevoke"
-	Defragment  RequestType = "defragment"
-	Compact     RequestType = "compact"
-)
-
-type EtcdRequest struct {
-	Type        RequestType
-	LeaseGrant  *LeaseGrantRequest
-	LeaseRevoke *LeaseRevokeRequest
-	Range       *RangeRequest
-	Txn         *TxnRequest
-	Defragment  *DefragmentRequest
-	Compact     *CompactRequest
-}
-
-func (r *EtcdRequest) IsRead() bool {
-	if r.Type == Range {
-		return true
-	}
-	if r.Type != Txn {
-		return false
-	}
-	for _, op := range append(r.Txn.OperationsOnSuccess, r.Txn.OperationsOnFailure...) {
-		if op.Type != RangeOperation {
-			return false
-		}
-	}
-	return true
-}
-
-type RangeRequest struct {
-	RangeOptions
-	Revision int64
-}
-
-type RangeOptions struct {
-	Start string
-	End   string
-	Limit int64
-}
-
-type PutOptions struct {
-	Key     string
-	Value   ValueOrHash
-	LeaseID int64
-}
-
-type DeleteOptions struct {
-	Key string
-}
-
-type TxnRequest struct {
-	Conditions          []EtcdCondition
-	OperationsOnSuccess []EtcdOperation
-	OperationsOnFailure []EtcdOperation
-}
-
-type EtcdCondition struct {
-	Key              string
-	ExpectedRevision int64
-	ExpectedVersion  int64
-}
-
-type EtcdOperation struct {
-	Type   OperationType
-	Range  RangeOptions
-	Put    PutOptions
-	Delete DeleteOptions
-}
-
-type OperationType string
-
-const (
-	RangeOperation  OperationType = "range-operation"
-	PutOperation    OperationType = "put-operation"
-	DeleteOperation OperationType = "delete-operation"
-)
-
-type LeaseGrantRequest struct {
-	LeaseID int64
-}
-type LeaseRevokeRequest struct {
-	LeaseID int64
-}
-type DefragmentRequest struct{}
-
-// MaybeEtcdResponse extends EtcdResponse to include partial information about responses to a request.
-// Possible response state information:
-// * Normal response. Client observed response. Only EtcdResponse is set.
-// * Persisted. Client didn't observe response, but we know it was persisted by etcd. Only Persisted is set
-// * Persisted with Revision. Client didn't observe response, but we know that it was persisted, and it's revision. Both Persisted and PersistedRevision is set.
-// * Error response. Client observed error, but we don't know if it was persisted. Only Error is set.
-type MaybeEtcdResponse struct {
-	EtcdResponse
-	Persisted         bool
-	PersistedRevision int64
-	Error             string
-}
-
-var ErrEtcdFutureRev = errors.New("future rev")
-
-type EtcdResponse struct {
-	Txn         *TxnResponse
-	Range       *RangeResponse
-	LeaseGrant  *LeaseGrantReponse
-	LeaseRevoke *LeaseRevokeResponse
-	Defragment  *DefragmentResponse
-	Compact     *CompactResponse
-	ClientError string
-	Revision    int64
-}
-
-func Match(r1, r2 MaybeEtcdResponse) bool {
-	r1Revision := r1.Revision
-	if r1.Persisted {
-		r1Revision = r1.PersistedRevision
-	}
-	r2Revision := r2.Revision
-	if r2.Persisted {
-		r2Revision = r2.PersistedRevision
-	}
-	return (r1.Persisted && r1.PersistedRevision == 0) || (r2.Persisted && r2.PersistedRevision == 0) || ((r1.Persisted || r2.Persisted) && (r1.Error != "" || r2.Error != "" || r1Revision == r2Revision)) || reflect.DeepEqual(r1, r2)
-}
-
-type TxnResponse struct {
-	Failure bool
-	Results []EtcdOperationResult
-}
-
-type RangeResponse struct {
-	KVs   []KeyValue
-	Count int64
-}
-
-type LeaseGrantReponse struct {
-	LeaseID int64
-}
-type (
-	LeaseRevokeResponse struct{}
-	DefragmentResponse  struct{}
-)
-
-type EtcdOperationResult struct {
-	RangeResponse
-	Deleted int64
-}
-
-type KeyValue struct {
-	Key string
-	ValueRevision
-}
-
-var leased = struct{}{}
-
-type EtcdLease struct {
-	LeaseID int64
-	Keys    map[string]struct{}
-}
-
-func (el EtcdLease) DeepCopy() EtcdLease {
-	return EtcdLease{
-		LeaseID: el.LeaseID,
-		Keys:    maps.Clone(el.Keys),
-	}
-}
-
-type ValueRevision struct {
-	Value       ValueOrHash `json:",omitempty"`
-	ModRevision int64       `json:",omitempty"`
-	Version     int64       `json:",omitempty"`
-}
-
-type ValueOrHash struct {
-	Value string `json:",omitempty"`
-	Hash  uint32 `json:",omitempty"`
-}
-
-func ToValueOrHash(value string) ValueOrHash {
-	v := ValueOrHash{}
-	if len(value) < 20 {
-		v.Value = value
-	} else {
-		h := fnv.New32a()
-		h.Write([]byte(value))
-		v.Hash = h.Sum32()
-	}
-	return v
-}
-
-type CompactResponse struct{}
-
-type CompactRequest struct {
-	Revision int64
-}
diff --git a/tests/robustness/model/types.go b/tests/robustness/model/types.go
new file mode 100644
index 000000000..0f605659a
--- /dev/null
+++ b/tests/robustness/model/types.go
@@ -0,0 +1,228 @@
+// Copyright 2023 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package model
+
+import (
+	"errors"
+	"hash/fnv"
+	"maps"
+	"reflect"
+)
+
+// RevisionForNonLinearizableResponse is a fake revision value used to
+// separate responses for requests that are not linearizable,
+// thus we need to ignore it in model when checking linearization.
+const RevisionForNonLinearizableResponse = -1
+
+type RequestType string
+
+const (
+	Range       RequestType = "range"
+	Txn         RequestType = "txn"
+	LeaseGrant  RequestType = "leaseGrant"
+	LeaseRevoke RequestType = "leaseRevoke"
+	Defragment  RequestType = "defragment"
+	Compact     RequestType = "compact"
+)
+
+type EtcdRequest struct {
+	Type        RequestType
+	LeaseGrant  *LeaseGrantRequest
+	LeaseRevoke *LeaseRevokeRequest
+	Range       *RangeRequest
+	Txn         *TxnRequest
+	Defragment  *DefragmentRequest
+	Compact     *CompactRequest
+}
+
+func (r *EtcdRequest) IsRead() bool {
+	if r.Type == Range {
+		return true
+	}
+	if r.Type != Txn {
+		return false
+	}
+	for _, op := range append(r.Txn.OperationsOnSuccess, r.Txn.OperationsOnFailure...) {
+		if op.Type != RangeOperation {
+			return false
+		}
+	}
+	return true
+}
+
+type RangeRequest struct {
+	RangeOptions
+	Revision int64
+}
+
+type RangeOptions struct {
+	Start string
+	End   string
+	Limit int64
+}
+
+type PutOptions struct {
+	Key     string
+	Value   ValueOrHash
+	LeaseID int64
+}
+
+type DeleteOptions struct {
+	Key string
+}
+
+type TxnRequest struct {
+	Conditions          []EtcdCondition
+	OperationsOnSuccess []EtcdOperation
+	OperationsOnFailure []EtcdOperation
+}
+
+type EtcdCondition struct {
+	Key              string
+	ExpectedRevision int64
+	ExpectedVersion  int64
+}
+
+type EtcdOperation struct {
+	Type   OperationType
+	Range  RangeOptions
+	Put    PutOptions
+	Delete DeleteOptions
+}
+
+type OperationType string
+
+const (
+	RangeOperation  OperationType = "range-operation"
+	PutOperation    OperationType = "put-operation"
+	DeleteOperation OperationType = "delete-operation"
+)
+
+type LeaseGrantRequest struct {
+	LeaseID int64
+}
+type LeaseRevokeRequest struct {
+	LeaseID int64
+}
+type DefragmentRequest struct{}
+
+// MaybeEtcdResponse extends EtcdResponse to include partial information about responses to a request.
+// Possible response state information:
+// * Normal response. Client observed response. Only EtcdResponse is set.
+// * Persisted. Client didn't observe response, but we know it was persisted by etcd. Only Persisted is set
+// * Persisted with Revision. Client didn't observe response, but we know that it was persisted, and it's revision. Both Persisted and PersistedRevision is set.
+// * Error response. Client observed error, but we don't know if it was persisted. Only Error is set.
+type MaybeEtcdResponse struct {
+	EtcdResponse
+	Persisted         bool
+	PersistedRevision int64
+	Error             string
+}
+
+var ErrEtcdFutureRev = errors.New("future rev")
+
+type EtcdResponse struct {
+	Txn         *TxnResponse
+	Range       *RangeResponse
+	LeaseGrant  *LeaseGrantReponse
+	LeaseRevoke *LeaseRevokeResponse
+	Defragment  *DefragmentResponse
+	Compact     *CompactResponse
+	ClientError string
+	Revision    int64
+}
+
+func Match(r1, r2 MaybeEtcdResponse) bool {
+	r1Revision := r1.Revision
+	if r1.Persisted {
+		r1Revision = r1.PersistedRevision
+	}
+	r2Revision := r2.Revision
+	if r2.Persisted {
+		r2Revision = r2.PersistedRevision
+	}
+	return (r1.Persisted && r1.PersistedRevision == 0) || (r2.Persisted && r2.PersistedRevision == 0) || ((r1.Persisted || r2.Persisted) && (r1.Error != "" || r2.Error != "" || r1Revision == r2Revision)) || reflect.DeepEqual(r1, r2)
+}
+
+type TxnResponse struct {
+	Failure bool
+	Results []EtcdOperationResult
+}
+
+type RangeResponse struct {
+	KVs   []KeyValue
+	Count int64
+}
+
+type LeaseGrantReponse struct {
+	LeaseID int64
+}
+type (
+	LeaseRevokeResponse struct{}
+	DefragmentResponse  struct{}
+)
+
+type EtcdOperationResult struct {
+	RangeResponse
+	Deleted int64
+}
+
+type KeyValue struct {
+	Key string
+	ValueRevision
+}
+
+var leased = struct{}{}
+
+type EtcdLease struct {
+	LeaseID int64
+	Keys    map[string]struct{}
+}
+
+func (el EtcdLease) DeepCopy() EtcdLease {
+	return EtcdLease{
+		LeaseID: el.LeaseID,
+		Keys:    maps.Clone(el.Keys),
+	}
+}
+
+type ValueRevision struct {
+	Value       ValueOrHash `json:",omitempty"`
+	ModRevision int64       `json:",omitempty"`
+	Version     int64       `json:",omitempty"`
+}
+
+type ValueOrHash struct {
+	Value string `json:",omitempty"`
+	Hash  uint32 `json:",omitempty"`
+}
+
+func ToValueOrHash(value string) ValueOrHash {
+	v := ValueOrHash{}
+	if len(value) < 20 {
+		v.Value = value
+	} else {
+		h := fnv.New32a()
+		h.Write([]byte(value))
+		v.Hash = h.Sum32()
+	}
+	return v
+}
+
+type CompactResponse struct{}
+
+type CompactRequest struct {
+	Revision int64
+}
diff --git a/tests/robustness/scenarios/scenarios.go b/tests/robustness/scenarios/scenarios.go
index 9be207f82..cac904281 100644
--- a/tests/robustness/scenarios/scenarios.go
+++ b/tests/robustness/scenarios/scenarios.go
@@ -243,7 +243,8 @@ func Regression(t *testing.T) []TestScenario {
 			MinimalQPS:                     50,
 			MaximalQPS:                     100,
 			BurstableQPS:                   100,
-			ClientCount:                    8,
+			MemberClientCount:              6,
+			ClusterClientCount:             2,
 			MaxNonUniqueRequestConcurrency: 3,
 		}.WithoutCompaction(),
 		Failpoint: failpoint.BatchCompactBeforeSetFinishedCompactPanic,
diff --git a/tests/robustness/traffic/traffic.go b/tests/robustness/traffic/traffic.go
index c33f148d8..4ae8710c8 100644
--- a/tests/robustness/traffic/traffic.go
+++ b/tests/robustness/traffic/traffic.go
@@ -43,14 +43,16 @@ var (
 		MinimalQPS:                     100,
 		MaximalQPS:                     200,
 		BurstableQPS:                   1000,
-		ClientCount:                    8,
+		MemberClientCount:              6,
+		ClusterClientCount:             2,
 		MaxNonUniqueRequestConcurrency: 3,
 	}
 	HighTrafficProfile = Profile{
 		MinimalQPS:                     100,
 		MaximalQPS:                     1000,
 		BurstableQPS:                   1000,
-		ClientCount:                    8,
+		MemberClientCount:              6,
+		ClusterClientCount:             2,
 		MaxNonUniqueRequestConcurrency: 3,
 	}
 )
@@ -73,7 +75,7 @@ func SimulateTraffic(ctx context.Context, t *testing.T, lg *zap.Logger, clus *e2
 
 	lg.Info("Start traffic")
 	startTime := time.Since(clientSet.BaseTime())
-	for i := range profile.ClientCount {
+	for i := range profile.MemberClientCount {
 		wg.Add(1)
 
 		c, nerr := clientSet.NewClient([]string{endpoints[i%len(endpoints)]})
@@ -85,6 +87,18 @@ func SimulateTraffic(ctx context.Context, t *testing.T, lg *zap.Logger, clus *e2
 			traffic.RunTrafficLoop(ctx, c, limiter, clientSet.IdentityProvider(), lm, nonUniqueWriteLimiter, keyStore, finish)
 		}(c)
 	}
+	for range profile.ClusterClientCount {
+		wg.Add(1)
+
+		c, nerr := clientSet.NewClient(endpoints)
+		require.NoError(t, nerr)
+		go func(c *client.RecordingClient) {
+			defer wg.Done()
+			defer c.Close()
+
+			traffic.RunTrafficLoop(ctx, c, limiter, clientSet.IdentityProvider(), lm, nonUniqueWriteLimiter, keyStore, finish)
+		}(c)
+	}
 	if !profile.ForbidCompaction {
 		wg.Add(1)
 		c, nerr := clientSet.NewClient(endpoints)
@@ -179,7 +193,8 @@ type Profile struct {
 	MaximalQPS                     float64
 	BurstableQPS                   int
 	MaxNonUniqueRequestConcurrency int
-	ClientCount                    int
+	MemberClientCount              int
+	ClusterClientCount             int
 	ForbidCompaction               bool
 	CompactPeriod                  time.Duration
 }
diff --git a/tests/robustness/validate/patch_history_test.go b/tests/robustness/validate/patch_history_test.go
index 0ab448ea0..45294d3b8 100644
--- a/tests/robustness/validate/patch_history_test.go
+++ b/tests/robustness/validate/patch_history_test.go
@@ -412,7 +412,7 @@ func TestPatchHistory(t *testing.T) {
 					Watch:    tc.watchOperations,
 				},
 			}
-			operations, _ := prepareAndCategorizeOperations(reports)
+			operations, _, _ := prepareAndCategorizeOperations(reports)
 			patched := patchLinearizableOperations(operations, reports, tc.persistedRequest)
 			if diff := cmp.Diff(tc.expectedRemainingOperations, patched,
 				cmpopts.EquateEmpty(),
diff --git a/tests/robustness/validate/result.go b/tests/robustness/validate/result.go
index 16d721c99..f0d18ce8e 100644
--- a/tests/robustness/validate/result.go
+++ b/tests/robustness/validate/result.go
@@ -87,7 +87,7 @@ type LinearizationResult struct {
 	Timeout bool
 }
 
-func (r LinearizationResult) Visualize(lg *zap.Logger, path string) error {
+func (r *LinearizationResult) Visualize(lg *zap.Logger, path string) error {
 	lg.Info("Saving visualization", zap.String("path", path))
 	err := porcupine.VisualizePath(r.Model, r.Info, path)
 	if err != nil {
@@ -95,3 +95,16 @@ func (r LinearizationResult) Visualize(lg *zap.Logger, path string) error {
 	}
 	return nil
 }
+
+func (r *LinearizationResult) AddToVisualization(serializable []porcupine.Operation) {
+	annotations := []porcupine.Annotation{}
+	for _, op := range serializable {
+		annotations = append(annotations, porcupine.Annotation{
+			ClientId:    op.ClientId,
+			Start:       op.Call,
+			End:         op.Return,
+			Description: r.Model.DescribeOperation(op.Input, op.Output),
+		})
+	}
+	r.Info.AddAnnotations(annotations)
+}
diff --git a/tests/robustness/validate/validate.go b/tests/robustness/validate/validate.go
index ca70770f4..3b3af2f8e 100644
--- a/tests/robustness/validate/validate.go
+++ b/tests/robustness/validate/validate.go
@@ -34,7 +34,7 @@ func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.Cli
 	if result.Assumptions.Error() != nil {
 		return result
 	}
-	linearizableOperations, serializableOperations := prepareAndCategorizeOperations(reports)
+	linearizableOperations, serializableOperations, operationsForVisualization := prepareAndCategorizeOperations(reports)
 	// We are passing in the original reports and linearizableOperations with modified return time.
 	// The reason is that linearizableOperations are those dedicated for linearization, which requires them to have returnTime set to infinity as required by pourcupine.
 	// As for the report, the original report is used so the consumer doesn't need to track what patching was done or not.
@@ -43,6 +43,7 @@ func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.Cli
 	}
 
 	result.Linearization = validateLinearizableOperationsAndVisualize(lg, linearizableOperations, timeout)
+	result.Linearization.AddToVisualization(operationsForVisualization)
 	// Skip other validations if model is not linearizable, as they are expected to fail too and obfuscate the logs.
 	if result.Linearization.Error() != nil {
 		lg.Info("Skipping other validations as linearization failed")
@@ -62,21 +63,17 @@ type Config struct {
 	ExpectRevisionUnique bool
 }
 
-func prepareAndCategorizeOperations(reports []report.ClientReport) (linearizable []porcupine.Operation, serializable []porcupine.Operation) {
+func prepareAndCategorizeOperations(reports []report.ClientReport) (linearizable, serializable, forVisualization []porcupine.Operation) {
 	for _, report := range reports {
 		for _, op := range report.KeyValue {
 			request := op.Input.(model.EtcdRequest)
 			response := op.Output.(model.MaybeEtcdResponse)
-			// serializable operations include only Range requests on non-zero revision
-			if request.Type == model.Range && request.Range.Revision != 0 {
+			if isSerializable(request, response) {
 				serializable = append(serializable, op)
 			}
-			// Remove failed read requests as they are not relevant for linearization.
-			if request.IsRead() && response.Error != "" {
-				continue
-			}
-			// Defragment is not linearizable
-			if request.Type == model.Defragment {
+			// Operation that will not be linearized need to be added separetly to visualization.
+			if !isLinearizable(request, response) {
+				forVisualization = append(forVisualization, op)
 				continue
 			}
 			// For linearization, we set the return time of failed requests to MaxInt64.
@@ -87,7 +84,31 @@ func prepareAndCategorizeOperations(reports []report.ClientReport) (linearizable
 			linearizable = append(linearizable, op)
 		}
 	}
-	return linearizable, serializable
+	return linearizable, serializable, forVisualization
+}
+
+func isLinearizable(request model.EtcdRequest, response model.MaybeEtcdResponse) bool {
+	// Cannot test response for request without side effect.
+	if request.IsRead() && response.Error != "" {
+		return false
+	}
+	// Defragment is not linearizable
+	if request.Type == model.Defragment {
+		return false
+	}
+	return true
+}
+
+func isSerializable(request model.EtcdRequest, response model.MaybeEtcdResponse) bool {
+	// Cannot test response for request without side effect.
+	if request.IsRead() && response.Error != "" {
+		return false
+	}
+	// Test range requests about stale revision
+	if request.Type == model.Range && request.Range.Revision != 0 {
+		return true
+	}
+	return false
 }
 
 func checkValidationAssumptions(reports []report.ClientReport) error {