This is an automated email from the ASF dual-hosted git repository.

spetz pushed a commit to branch offset_hardening
in repository https://gitbox.apache.org/repos/asf/iggy.git

commit 7bd65595843b59e15d957141743c1844dab89840
Author: spetz <[email protected]>
AuthorDate: Sat Sep 5 08:27:00 2026 +0200

    fix(partitions): bound consumer offset state per partition
---
 Cargo.lock                                         |  22 +-
 Cargo.toml                                         |  10 +-
 bdd/python/uv.lock                                 |   2 +-
 core/ai/mcp/Cargo.toml                             |   2 +-
 core/bench/Cargo.toml                              |   2 +-
 core/bench/dashboard/frontend/Cargo.toml           |   2 +-
 core/bench/dashboard/server/Cargo.toml             |   2 +-
 core/bench/report/Cargo.toml                       |   2 +-
 core/binary_protocol/Cargo.toml                    |   2 +-
 core/binary_protocol/src/primitives/ack_level.rs   |   6 +-
 .../consumer_offsets/delete_consumer_offset.rs     |   4 +-
 .../consumer_offsets/store_consumer_offset.rs      |   4 +-
 core/cli/Cargo.toml                                |   2 +-
 core/common/Cargo.toml                             |   2 +-
 core/common/src/error/iggy_error.rs                |   2 +-
 core/common/src/traits/message_client.rs           |   4 +
 core/connectors/sdk/Cargo.toml                     |   2 +-
 .../tests/cluster/consumer_offset_quota.rs         |  42 +-
 .../tests/server/consumer_offset_quota_vsr.rs      |  87 +++-
 core/message_bus/src/lib.rs                        |   2 +-
 core/metadata/src/impls/metadata.rs                |   6 +-
 core/metadata/src/stm/user.rs                      |   4 +-
 core/partitions/src/consumer_offset_capacity.rs    | 268 ++++++-------
 core/partitions/src/iggy_partition.rs              | 445 ++++++++++++++++++---
 core/partitions/src/iggy_partitions.rs             |  16 +
 core/partitions/src/poll_plan.rs                   | 142 +++----
 core/partitions/src/state_transfer.rs              |  17 +-
 core/sdk/Cargo.toml                                |   2 +-
 core/sdk/src/clients/consumer.rs                   |   5 +
 core/server/Cargo.toml                             |   2 +-
 core/server/config.toml                            |   3 +
 core/server/src/boot/recovery.rs                   |   2 +-
 core/server/src/consumer_group.rs                  |  14 +-
 core/server/src/dispatch/partition.rs              |  47 +--
 core/server/src/partition_helpers.rs               |  10 +-
 core/server/src/partition_reconciler.rs            | 154 +++++--
 core/server/src/responses.rs                       |  32 +-
 core/shard/src/lib.rs                              |  43 +-
 core/shard/src/metrics.rs                          |  21 +-
 core/shard/src/router.rs                           |   9 +
 core/simulator/src/lib.rs                          |  17 +-
 core/simulator/src/workload/effect.rs              |   2 +-
 core/simulator/src/workload/shadow.rs              |   2 +-
 examples/node/package-lock.json                    |   2 +-
 examples/python/uv.lock                            |   2 +-
 foreign/go/contracts/version.go                    |   2 +-
 foreign/go/errors/errors.yaml                      |   2 +-
 foreign/go/errors/errors_gen.go                    |   2 +-
 .../org/apache/iggy/exception/IggyErrorCode.java   |   2 +
 foreign/node/package-lock.json                     |   4 +-
 foreign/node/package.json                          |   2 +-
 foreign/node/src/wire/error.code.test.ts           |   2 +-
 foreign/node/src/wire/error.code.ts                |   2 +-
 foreign/python/Cargo.toml                          |   4 +-
 foreign/python/pyproject.toml                      |   2 +-
 foreign/python/uv.lock                             |   2 +-
 56 files changed, 1021 insertions(+), 475 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 56ecb71dd..7856dd413 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1862,7 +1862,7 @@ checksum = 
"3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
 
 [[package]]
 name = "bench-dashboard-frontend"
-version = "0.8.0-edge.3"
+version = "0.8.0-edge.4"
 dependencies = [
  "bench-dashboard-shared",
  "bench-report",
@@ -1892,7 +1892,7 @@ dependencies = [
 
 [[package]]
 name = "bench-report"
-version = "0.4.0-edge.3"
+version = "0.4.0-edge.4"
 dependencies = [
  "charming",
  "colored",
@@ -6594,7 +6594,7 @@ checksum = 
"cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb"
 
 [[package]]
 name = "iggy"
-version = "0.11.0-edge.6"
+version = "0.11.0-edge.7"
 dependencies = [
  "async-broadcast",
  "async-dropper",
@@ -6628,7 +6628,7 @@ dependencies = [
 
 [[package]]
 name = "iggy-bench"
-version = "0.6.0-edge.6"
+version = "0.6.0-edge.7"
 dependencies = [
  "async-trait",
  "bench-report",
@@ -6657,7 +6657,7 @@ dependencies = [
 
 [[package]]
 name = "iggy-bench-dashboard-server"
-version = "0.8.0-edge.3"
+version = "0.8.0-edge.4"
 dependencies = [
  "actix-cors",
  "actix-files",
@@ -6685,7 +6685,7 @@ dependencies = [
 
 [[package]]
 name = "iggy-cli"
-version = "0.14.0-edge.6"
+version = "0.14.0-edge.7"
 dependencies = [
  "anyhow",
  "apple-native-keyring-store",
@@ -6791,7 +6791,7 @@ dependencies = [
 
 [[package]]
 name = "iggy-mcp"
-version = "0.5.0-edge.5"
+version = "0.5.0-edge.6"
 dependencies = [
  "axum",
  "axum-server",
@@ -6825,7 +6825,7 @@ dependencies = [
 
 [[package]]
 name = "iggy_binary_protocol"
-version = "0.11.0-edge.6"
+version = "0.11.0-edge.7"
 dependencies = [
  "aligned-vec",
  "bytemuck",
@@ -6838,7 +6838,7 @@ dependencies = [
 
 [[package]]
 name = "iggy_common"
-version = "0.11.0-edge.6"
+version = "0.11.0-edge.7"
 dependencies = [
  "aes-gcm 0.11.1",
  "async-broadcast",
@@ -7203,7 +7203,7 @@ dependencies = [
 
 [[package]]
 name = "iggy_connector_sdk"
-version = "0.4.0-edge.3"
+version = "0.4.0-edge.4"
 dependencies = [
  "anyhow",
  "apache-avro 0.22.0",
@@ -12160,7 +12160,7 @@ dependencies = [
 
 [[package]]
 name = "server"
-version = "0.9.0-edge.6"
+version = "0.9.0-edge.7"
 dependencies = [
  "ahash 0.8.12",
  "argon2",
diff --git a/Cargo.toml b/Cargo.toml
index af7a697ca..beb92e80f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -198,13 +198,13 @@ hyper-util = { version = "0.1.20", features = 
["server-auto", "service"] }
 iceberg = "0.10.1"
 iceberg-catalog-rest = "0.10.1"
 iceberg-storage-opendal = "0.10.1"
-iggy = { path = "core/sdk", version = "0.11.0-edge.6" }
-iggy-cli = { path = "core/cli", version = "0.14.0-edge.6" }
+iggy = { path = "core/sdk", version = "0.11.0-edge.7" }
+iggy-cli = { path = "core/cli", version = "0.14.0-edge.7" }
 iggy-gateway-kafka = { path = "gateways/kafka" }
-iggy_binary_protocol = { path = "core/binary_protocol", version = 
"0.11.0-edge.6" }
-iggy_common = { path = "core/common", version = "0.11.0-edge.6" }
+iggy_binary_protocol = { path = "core/binary_protocol", version = 
"0.11.0-edge.7" }
+iggy_common = { path = "core/common", version = "0.11.0-edge.7" }
 iggy_connector_doris_sink = { path = "core/connectors/sinks/doris_sink" }
-iggy_connector_sdk = { path = "core/connectors/sdk", version = "0.4.0-edge.3" }
+iggy_connector_sdk = { path = "core/connectors/sdk", version = "0.4.0-edge.4" }
 indexmap = "2.14.1"
 integration = { path = "core/integration" }
 ipnet = "2.12.1"
diff --git a/bdd/python/uv.lock b/bdd/python/uv.lock
index 44c64ab55..b10e81b84 100644
--- a/bdd/python/uv.lock
+++ b/bdd/python/uv.lock
@@ -8,7 +8,7 @@ exclude-newer-span = "P7D"
 
 [[package]]
 name = "apache-iggy"
-version = "0.9.0.dev6"
+version = "0.9.0.dev7"
 source = { directory = "../../foreign/python" }
 
 [package.metadata]
diff --git a/core/ai/mcp/Cargo.toml b/core/ai/mcp/Cargo.toml
index 5386c5c4a..1d1953f9f 100644
--- a/core/ai/mcp/Cargo.toml
+++ b/core/ai/mcp/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy-mcp"
-version = "0.5.0-edge.5"
+version = "0.5.0-edge.6"
 description = "MCP Server for Iggy message streaming platform"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/bench/Cargo.toml b/core/bench/Cargo.toml
index e2a3c8d8d..7a3f1ce42 100644
--- a/core/bench/Cargo.toml
+++ b/core/bench/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy-bench"
-version = "0.6.0-edge.6"
+version = "0.6.0-edge.7"
 edition = "2024"
 license = "Apache-2.0"
 repository = "https://github.com/apache/iggy";
diff --git a/core/bench/dashboard/frontend/Cargo.toml 
b/core/bench/dashboard/frontend/Cargo.toml
index 595cec1d0..6b023d631 100644
--- a/core/bench/dashboard/frontend/Cargo.toml
+++ b/core/bench/dashboard/frontend/Cargo.toml
@@ -18,7 +18,7 @@
 [package]
 name = "bench-dashboard-frontend"
 license = "Apache-2.0"
-version = "0.8.0-edge.3"
+version = "0.8.0-edge.4"
 edition = "2024"
 publish = false
 
diff --git a/core/bench/dashboard/server/Cargo.toml 
b/core/bench/dashboard/server/Cargo.toml
index 726df33be..68570b112 100644
--- a/core/bench/dashboard/server/Cargo.toml
+++ b/core/bench/dashboard/server/Cargo.toml
@@ -18,7 +18,7 @@
 [package]
 name = "iggy-bench-dashboard-server"
 license = "Apache-2.0"
-version = "0.8.0-edge.3"
+version = "0.8.0-edge.4"
 edition = "2024"
 publish = false
 
diff --git a/core/bench/report/Cargo.toml b/core/bench/report/Cargo.toml
index a418cdd25..bc5649a57 100644
--- a/core/bench/report/Cargo.toml
+++ b/core/bench/report/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "bench-report"
-version = "0.4.0-edge.3"
+version = "0.4.0-edge.4"
 edition = "2024"
 description = "Benchmark report and chart generation library for iggy-bench 
binary and iggy-benchmarks-dashboard web app"
 license = "Apache-2.0"
diff --git a/core/binary_protocol/Cargo.toml b/core/binary_protocol/Cargo.toml
index 0a83a4c01..be97141e3 100644
--- a/core/binary_protocol/Cargo.toml
+++ b/core/binary_protocol/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_binary_protocol"
-version = "0.11.0-edge.6"
+version = "0.11.0-edge.7"
 description = "Wire protocol types and codec for the Iggy binary protocol. 
Shared between server and SDK."
 edition = "2024"
 rust-version.workspace = true
diff --git a/core/binary_protocol/src/primitives/ack_level.rs 
b/core/binary_protocol/src/primitives/ack_level.rs
index a2ddb2389..a750b57d9 100644
--- a/core/binary_protocol/src/primitives/ack_level.rs
+++ b/core/binary_protocol/src/primitives/ack_level.rs
@@ -20,9 +20,9 @@ use crate::WireError;
 /// Acknowledgement policy for consumer-offset write commands.
 ///
 /// Wire format: single `u8` discriminant.
-/// - `NoAck(0)`:  leader-local write only; respond as soon as the in-memory
-///   and on-disk state have been updated. Matches the fast path used by
-///   `PollMessages` auto-commit.
+/// - `NoAck(0)`: local fast path for a single-replica partition. Replicated
+///   partitions commit offset writes through VSR before replying, including
+///   when this acknowledgement value is selected.
 /// - `Quorum(1)`: submit through the partition VSR consensus pipeline and
 ///   respond only after the write has been committed by a quorum of replicas.
 ///   This is the default for explicit client writes.
diff --git 
a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs 
b/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs
index 014081794..e5ca29d1a 100644
--- 
a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs
+++ 
b/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs
@@ -24,8 +24,8 @@ use bytes::{BufMut, BytesMut};
 
 /// `DeleteConsumerOffset` request.
 ///
-/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
-/// pipeline.
+/// The `ack` byte selects the local fast path only for `NoAck` on a
+/// single-replica partition. Replicated partitions use VSR for both values.
 ///
 /// Wire format:
 /// ```text
diff --git 
a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs 
b/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs
index 5261d5c53..228360798 100644
--- 
a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs
+++ 
b/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs
@@ -24,8 +24,8 @@ use bytes::{BufMut, BytesMut};
 
 /// `StoreConsumerOffset` request.
 ///
-/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
-/// pipeline.
+/// The `ack` byte selects the local fast path only for `NoAck` on a
+/// single-replica partition. Replicated partitions use VSR for both values.
 ///
 /// Wire format:
 /// ```text
diff --git a/core/cli/Cargo.toml b/core/cli/Cargo.toml
index a2f8756bb..f8a2b89f1 100644
--- a/core/cli/Cargo.toml
+++ b/core/cli/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy-cli"
-version = "0.14.0-edge.6"
+version = "0.14.0-edge.7"
 edition = "2024"
 rust-version.workspace = true
 authors = ["[email protected]"]
diff --git a/core/common/Cargo.toml b/core/common/Cargo.toml
index 16aaa621c..2e059fc3e 100644
--- a/core/common/Cargo.toml
+++ b/core/common/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_common"
-version = "0.11.0-edge.6"
+version = "0.11.0-edge.7"
 description = "Iggy is the persistent message streaming platform written in 
Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing 
millions of messages per second."
 edition = "2024"
 rust-version.workspace = true
diff --git a/core/common/src/error/iggy_error.rs 
b/core/common/src/error/iggy_error.rs
index b312d61dc..e77abebc5 100644
--- a/core/common/src/error/iggy_error.rs
+++ b/core/common/src/error/iggy_error.rs
@@ -333,7 +333,7 @@ pub enum IggyError {
     NotResolvedConsumer(Identifier) = 3022,
     #[error("Cannot open consumer offsets file for path: {0}")]
     CannotOpenConsumerOffsetsFile(String) = 3023,
-    #[error("Too many consumer offsets")]
+    #[error("Too many consumer offsets for partition")]
     TooManyConsumerOffsets = 3024,
     #[error("Segment not found")]
     SegmentNotFound = 4000,
diff --git a/core/common/src/traits/message_client.rs 
b/core/common/src/traits/message_client.rs
index 18c6d2294..7e1dcd026 100644
--- a/core/common/src/traits/message_client.rs
+++ b/core/common/src/traits/message_client.rs
@@ -31,6 +31,10 @@ pub trait MessageClient {
     /// Polling a consumer group the client is not (or no longer) a member of 
fails with `ConsumerGroupMemberNotFound` rather than returning an empty batch, 
so the caller can rejoin.
     /// A member that holds no partitions gets an empty batch whose 
`partition_id` is [`NO_ASSIGNED_PARTITION`](crate::NO_ASSIGNED_PARTITION).
     #[allow(clippy::too_many_arguments)]
+    /// With server-side auto-commit enabled, a new consumer offset key can be
+    /// rejected with `TooManyConsumerOffsets` at the partition's configured
+    /// limit. That poll returns no messages. Existing keys remain writable,
+    /// and polling without auto-commit does not allocate a stored offset.
     async fn poll_messages(
         &self,
         stream_id: &Identifier,
diff --git a/core/connectors/sdk/Cargo.toml b/core/connectors/sdk/Cargo.toml
index fae1b4a0a..5d6b98daa 100644
--- a/core/connectors/sdk/Cargo.toml
+++ b/core/connectors/sdk/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_sdk"
-version = "0.4.0-edge.3"
+version = "0.4.0-edge.4"
 description = "Iggy is the persistent message streaming platform written in 
Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing 
millions of messages per second."
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/integration/tests/cluster/consumer_offset_quota.rs 
b/core/integration/tests/cluster/consumer_offset_quota.rs
index c39bab514..3aa9a7d9d 100644
--- a/core/integration/tests/cluster/consumer_offset_quota.rs
+++ b/core/integration/tests/cluster/consumer_offset_quota.rs
@@ -141,6 +141,22 @@ async fn 
given_replicated_partition_when_no_ack_offsets_mutate_should_converge_a
     )
     .await;
 
+    let auto_commit_consumer = 
Consumer::new(Identifier::numeric(1).expect("consumer identifier"));
+    let polled = client
+        .poll_messages(
+            &stream,
+            &topic,
+            Some(PARTITION_ID),
+            &auto_commit_consumer,
+            &PollingStrategy::first(),
+            1,
+            true,
+        )
+        .await
+        .expect("auto-commit poll within limit");
+    assert_eq!(polled.messages.len(), 1);
+    wait_for_file_state(harness, stream_details.id, topic_details.id, 1, 
true).await;
+
     for consumer_id in 1..=4 {
         client
             .store_consumer_offset(
@@ -306,23 +322,15 @@ async fn wait_for_max_file_count(harness: &TestHarness, 
stream_id: u32, topic_id
     loop {
         let counts: Vec<usize> = (0..harness.cluster_size())
             .map(|node| {
-                let dir = offset_file(harness, node, stream_id, topic_id, 0)
-                    .parent()
-                    .expect("offset file has parent")
-                    .to_path_buf();
-                std::fs::read_dir(dir)
-                    .map(|entries| {
-                        entries
-                            .filter_map(Result::ok)
-                            .filter(|entry| {
-                                entry
-                                    .file_name()
-                                    .to_str()
-                                    .is_some_and(|name| 
name.parse::<u32>().is_ok())
-                            })
-                            .count()
-                    })
-                    .unwrap_or_default()
+                disk::consumer_offset_file_ids(
+                    &harness.node(node).data_path(),
+                    stream_id,
+                    topic_id,
+                    PARTITION_ID,
+                    ConsumerKind::Consumer,
+                )
+                .expect("consumer offset directory")
+                .len()
             })
             .collect();
         if counts.iter().all(|count| *count == max) {
diff --git a/core/integration/tests/server/consumer_offset_quota_vsr.rs 
b/core/integration/tests/server/consumer_offset_quota_vsr.rs
index 3849ae096..2ccac51c4 100644
--- a/core/integration/tests/server/consumer_offset_quota_vsr.rs
+++ b/core/integration/tests/server/consumer_offset_quota_vsr.rs
@@ -107,6 +107,49 @@ async fn 
given_full_consumer_offset_table_when_creating_another_should_reject_wi
         .await
         .expect("consumer login");
 
+    let first_consumer = Consumer::new(Identifier::numeric(1).unwrap());
+    let polled = client
+        .poll_messages(
+            &stream,
+            &topic,
+            Some(PARTITION_ID),
+            &first_consumer,
+            &PollingStrategy::first(),
+            1,
+            true,
+        )
+        .await
+        .expect("new auto-commit consumer fits");
+    assert_eq!(polled.messages.len(), 1);
+    let first_file = harness.server().data_path().join(format!(
+        "streams/{}/topics/{}/partitions/{PARTITION_ID}/offsets/consumers/1",
+        stream_details.id, topic_details.id
+    ));
+    let deadline = tokio::time::Instant::now() + 
std::time::Duration::from_secs(10);
+    while !first_file.is_file() {
+        assert!(
+            tokio::time::Instant::now() < deadline,
+            "auto-commit never reached its file"
+        );
+        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
+    }
+    assert!(
+        client
+            .poll_messages(
+                &stream,
+                &topic,
+                Some(PARTITION_ID),
+                &first_consumer,
+                &PollingStrategy::next(),
+                1,
+                true
+            )
+            .await
+            .expect("next poll")
+            .messages
+            .is_empty()
+    );
+
     for consumer_id in 1..=LIMIT {
         client
             .store_consumer_offset(
@@ -205,23 +248,23 @@ async fn 
given_full_consumer_offset_table_when_creating_another_should_reject_wi
     let (reply, _) = raw_tcp::exchange(&mut raw, &header, 
&unresolved_group).await;
     assert_eq!(
         raw_tcp::reply_status(&reply),
-        IggyError::InvalidIdentifier.as_code()
+        IggyError::ConsumerGroupIdNotFound(Identifier::numeric(999).unwrap(), 
topic.clone())
+            .as_code()
     );
 
     let offsets_dir = harness.server().data_path().join(format!(
         "streams/{}/topics/{}/partitions/{PARTITION_ID}/offsets/consumers",
         stream_details.id, topic_details.id
     ));
-    let file_count = fs::read_dir(&offsets_dir)
-        .expect("consumer offsets directory")
-        .filter_map(Result::ok)
-        .filter(|entry| {
-            entry
-                .file_name()
-                .to_str()
-                .is_some_and(|name| name.parse::<u32>().is_ok())
-        })
-        .count();
+    let file_count = integration::harness::disk::consumer_offset_file_ids(
+        &harness.server().data_path(),
+        stream_details.id,
+        topic_details.id,
+        PARTITION_ID,
+        ConsumerKind::Consumer,
+    )
+    .expect("consumer offsets directory")
+    .len();
     assert_eq!(file_count, LIMIT as usize);
     let groups_dir = offsets_dir
         .parent()
@@ -232,6 +275,28 @@ async fn 
given_full_consumer_offset_table_when_creating_another_should_reject_wi
         .unwrap_or_default();
     assert_eq!(group_file_count, 0);
 
+    let named_group = StoreConsumerOffsetRequest {
+        consumer: 
WireConsumer::consumer_group(WireIdentifier::named("unknown-group").unwrap()),
+        stream_id: WireIdentifier::Numeric(stream_details.id),
+        topic_id: WireIdentifier::Numeric(topic_details.id),
+        partition_id: Some(PARTITION_ID),
+        offset: 0,
+        ack: AckLevel::Quorum,
+    }
+    .to_bytes();
+    let header = raw_tcp::request_header(
+        Operation::StoreConsumerOffset,
+        raw_client_id,
+        session,
+        2,
+        named_group.len(),
+    );
+    let (reply, _) = raw_tcp::exchange(&mut raw, &header, &named_group).await;
+    assert_eq!(
+        raw_tcp::reply_status(&reply),
+        IggyError::ConsumerGroupNameNotFound("unknown-group".to_owned(), 
topic.clone()).as_code()
+    );
+
     let http = HttpClient::login_root(harness).await;
     let response = http
         .client
diff --git a/core/message_bus/src/lib.rs b/core/message_bus/src/lib.rs
index 74836bb8e..d8038b612 100644
--- a/core/message_bus/src/lib.rs
+++ b/core/message_bus/src/lib.rs
@@ -797,7 +797,7 @@ impl IggyMessageBus {
     }
 
     /// Construct a bus with explicit runtime tunables and a pre-allocated
-    /// owner table. Server-ng bootstrap uses this so every shard's bus
+    /// owner table. Server bootstrap uses this so every shard's bus
     /// shares the same atomic slots; tests use [`Self::with_tunables`]
     /// which allocates a fresh table per bus.
     ///
diff --git a/core/metadata/src/impls/metadata.rs 
b/core/metadata/src/impls/metadata.rs
index 726a013c1..5358fd0d4 100644
--- a/core/metadata/src/impls/metadata.rs
+++ b/core/metadata/src/impls/metadata.rs
@@ -679,7 +679,7 @@ pub fn apply_committed_prepare<M>(
 pub type CommitNotifier = std::rc::Rc<dyn Fn(Operation)>;
 
 pub struct IggyMetadata<C, J, S, M, SB = PingPongSuperblock> {
-    /// `Some` on shard 0, `None` on other shards. Server-ng bootstrap
+    /// `Some` on shard 0, `None` on other shards. Server bootstrap
     /// holds the invariant: only shard 0 owns the metadata consensus
     /// replica; every other shard reconstructs `mux_stm` from the
     /// `MetadataHandoff::Waiter` factory bundle broadcast by shard 0
@@ -897,7 +897,7 @@ impl<C, J, S, M, SB> IggyMetadata<C, J, S, M, SB> {
     }
 
     /// Install (or replace) the post-commit notifier. Passing `None`
-    /// removes any previous one. Server-ng bootstrap calls this on shard 0
+    /// removes any previous one. Server bootstrap calls this on shard 0
     /// only; peer shards never commit metadata locally.
     pub fn set_commit_notifier(&self, notifier: Option<CommitNotifier>) {
         *self.commit_notifier.borrow_mut() = notifier;
@@ -906,7 +906,7 @@ impl<C, J, S, M, SB> IggyMetadata<C, J, S, M, SB> {
     /// Seed the coordinator's last-checkpoint pairing at boot from the 
recovered
     /// snapshot, so the first post-boot view-change superblock write records 
the real
     /// `(checkpoint_op, checksum)` instead of `(0, 0)`. No-op without a 
coordinator
-    /// (peer shards, the simulator). Server-ng bootstrap calls this on shard 
0 after
+    /// (peer shards, the simulator). Server bootstrap calls this on shard 0 
after
     /// cross-checking the pairing.
     pub fn seed_checkpoint_ref(&self, checkpoint_op: u64, checkpoint_checksum: 
u128) {
         if let Some(coordinator) = &self.coordinator {
diff --git a/core/metadata/src/stm/user.rs b/core/metadata/src/stm/user.rs
index 1e79d7853..c78ea7507 100644
--- a/core/metadata/src/stm/user.rs
+++ b/core/metadata/src/stm/user.rs
@@ -642,14 +642,14 @@ impl StateHandler for UpdatePermissionsRequest {
 /// The success reply here is deliberately empty: the raw token the caller 
needs
 /// is the one thing this apply must never see.
 ///
-/// The primary mints the raw token and its hash at ingress (server-ng
+/// The primary mints the raw token and its hash at ingress (server
 /// `pat::rewrite_pat_request_for_user`) and replicates only the hash. Minting
 /// inside this apply would call `ring::rand` on every replica and diverge the
 /// token index, and replicating the raw token would persist a live credential 
in
 /// every WAL and snapshot. So the raw token leaves the primary by a side 
channel
 /// (`maybe_rewrite_pat_request` returns it alongside the rewritten request) 
and
 /// the home shard splices it into this op's reply as a typed
-/// `RawPersonalAccessTokenResponse` (server-ng 
`responses::build_raw_pat_reply`).
+/// `RawPersonalAccessTokenResponse` (server `responses::build_raw_pat_reply`).
 ///
 /// One consequence rides on that: the secret exists only on the wire of the
 /// original reply, so a replayed request cannot be served from the 
client-table
diff --git a/core/partitions/src/consumer_offset_capacity.rs 
b/core/partitions/src/consumer_offset_capacity.rs
index 6523b2614..27c786ceb 100644
--- a/core/partitions/src/consumer_offset_capacity.rs
+++ b/core/partitions/src/consumer_offset_capacity.rs
@@ -19,6 +19,7 @@ use iggy_common::ConsumerKind;
 use std::cell::{Cell, RefCell};
 use std::collections::{HashMap, HashSet};
 use std::rc::Rc;
+use std::sync::{Arc, Weak};
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub struct DurableOffsetState {
@@ -28,25 +29,30 @@ pub struct DurableOffsetState {
 
 #[derive(Debug, Default)]
 pub struct DurableConsumerOffsets {
-    entries: RefCell<HashMap<(ConsumerKind, u32), DurableOffsetState>>,
-    consumer_count: Cell<usize>,
-    group_count: Cell<usize>,
+    consumers: RefCell<HashMap<u32, DurableOffsetState>>,
+    groups: RefCell<HashMap<u32, DurableOffsetState>>,
 }
 
 impl DurableConsumerOffsets {
     pub(crate) fn get(&self, kind: ConsumerKind, id: u32) -> 
Option<DurableOffsetState> {
-        self.entries.borrow().get(&(kind, id)).copied()
+        self.entries(kind).borrow().get(&id).copied()
     }
 
     pub(crate) fn contains(&self, kind: ConsumerKind, id: u32) -> bool {
-        self.entries.borrow().contains_key(&(kind, id))
+        self.entries(kind).borrow().contains_key(&id)
     }
 
-    pub(crate) const fn count(&self, kind: ConsumerKind) -> usize {
-        match kind {
-            ConsumerKind::Consumer => self.consumer_count.get(),
-            ConsumerKind::ConsumerGroup => self.group_count.get(),
-        }
+    pub(crate) fn count(&self, kind: ConsumerKind) -> usize {
+        self.entries(kind).borrow().len()
+    }
+
+    pub(crate) fn covers(&self, kind: ConsumerKind, id: u32, offset: u64) -> 
bool {
+        self.get(kind, id).is_some_and(|state| {
+            state.committed_offset >= offset
+                && state
+                    .persisted_high_water
+                    .is_some_and(|persisted| persisted >= offset)
+        })
     }
 
     pub(crate) fn record_explicit(
@@ -56,8 +62,7 @@ impl DurableConsumerOffsets {
         committed_offset: u64,
         persisted_high_water: Option<u64>,
     ) {
-        self.insert_or_update(
-            kind,
+        self.entries(kind).borrow_mut().insert(
             id,
             DurableOffsetState {
                 committed_offset,
@@ -71,99 +76,70 @@ impl DurableConsumerOffsets {
         kind: ConsumerKind,
         id: u32,
         committed_offset: u64,
-        persisted_high_water: Option<u64>,
+        persisted_high_water: u64,
     ) {
-        let mut entries = self.entries.borrow_mut();
-        if let Some(state) = entries.get_mut(&(kind, id)) {
-            state.committed_offset = 
state.committed_offset.max(committed_offset);
-            if let Some(high_water) = persisted_high_water {
-                state.persisted_high_water = Some(
-                    state
-                        .persisted_high_water
-                        .map_or(high_water, |current| current.max(high_water)),
-                );
-            }
-            return;
-        }
-        entries.insert(
-            (kind, id),
-            DurableOffsetState {
-                committed_offset,
-                persisted_high_water,
-            },
+        let mut entries = self.entries(kind).borrow_mut();
+        let state = entries.entry(id).or_insert(DurableOffsetState {
+            committed_offset,
+            persisted_high_water: None,
+        });
+        state.committed_offset = state.committed_offset.max(committed_offset);
+        state.persisted_high_water = Some(
+            state
+                .persisted_high_water
+                .unwrap_or(0)
+                .max(persisted_high_water),
         );
-        drop(entries);
-        self.increment(kind);
     }
 
     pub(crate) fn mark_persisted(&self, kind: ConsumerKind, id: u32, 
high_water: u64) {
-        if let Some(state) = self.entries.borrow_mut().get_mut(&(kind, id)) {
+        if let Some(state) = self.entries(kind).borrow_mut().get_mut(&id) {
             state.persisted_high_water = Some(high_water);
         }
     }
 
     pub(crate) fn remove(&self, kind: ConsumerKind, id: u32) -> bool {
-        if self.entries.borrow_mut().remove(&(kind, id)).is_none() {
-            return false;
-        }
-        self.decrement(kind);
-        true
+        self.entries(kind).borrow_mut().remove(&id).is_some()
     }
 
     pub(crate) fn clear(&self) {
-        self.entries.borrow_mut().clear();
-        self.consumer_count.set(0);
-        self.group_count.set(0);
+        self.consumers.borrow_mut().clear();
+        self.groups.borrow_mut().clear();
     }
 
     pub(crate) fn committed_entries(&self, kind: ConsumerKind) -> Vec<(u32, 
u64)> {
-        self.entries
+        self.entries(kind)
             .borrow()
             .iter()
-            .filter_map(|((entry_kind, id), state)| {
-                (*entry_kind == kind).then_some((*id, state.committed_offset))
-            })
+            .map(|(id, state)| (*id, state.committed_offset))
             .collect()
     }
 
-    fn insert_or_update(&self, kind: ConsumerKind, id: u32, state: 
DurableOffsetState) {
-        let inserted = self
-            .entries
-            .borrow_mut()
-            .insert((kind, id), state)
-            .is_none();
-        if inserted {
-            self.increment(kind);
-        }
-    }
-
-    fn increment(&self, kind: ConsumerKind) {
-        match kind {
-            ConsumerKind::Consumer => 
self.consumer_count.set(self.consumer_count.get() + 1),
-            ConsumerKind::ConsumerGroup => 
self.group_count.set(self.group_count.get() + 1),
-        }
-    }
-
-    fn decrement(&self, kind: ConsumerKind) {
+    const fn entries(&self, kind: ConsumerKind) -> &RefCell<HashMap<u32, 
DurableOffsetState>> {
         match kind {
-            ConsumerKind::Consumer => 
self.consumer_count.set(self.consumer_count.get() - 1),
-            ConsumerKind::ConsumerGroup => 
self.group_count.set(self.group_count.get() - 1),
+            ConsumerKind::Consumer => &self.consumers,
+            ConsumerKind::ConsumerGroup => &self.groups,
         }
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CapacityReservation {
-    Existing,
-    Reserved,
-}
-
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub struct ConsumerOffsetCapacityError {
     pub kind: ConsumerKind,
     pub occupied: usize,
     pub limit: usize,
     pub first_in_episode: bool,
+    pub uncertain: bool,
+}
+
+impl From<ConsumerOffsetCapacityError> for iggy_common::IggyError {
+    fn from(error: ConsumerOffsetCapacityError) -> Self {
+        if error.uncertain {
+            Self::TransientNotAccepted
+        } else {
+            Self::TooManyConsumerOffsets
+        }
+    }
 }
 
 #[derive(Debug)]
@@ -171,7 +147,7 @@ pub struct ConsumerOffsetCapacity {
     kind: ConsumerKind,
     limit: Cell<usize>,
     pending: RefCell<HashMap<u32, usize>>,
-    provisional: RefCell<HashMap<u32, usize>>,
+    provisional: RefCell<HashMap<u32, Weak<()>>>,
     stranded: RefCell<HashSet<u32>>,
     uncertain: Cell<bool>,
     durable_warned: Cell<bool>,
@@ -196,43 +172,52 @@ impl ConsumerOffsetCapacity {
         self.limit.set(limit);
     }
 
+    pub(crate) const fn limit(&self) -> usize {
+        self.limit.get()
+    }
+
     pub(crate) fn try_reserve(
         &self,
         id: u32,
         durable: &DurableConsumerOffsets,
-    ) -> Result<CapacityReservation, ConsumerOffsetCapacityError> {
-        let existing = self.check(id, durable)?;
+    ) -> Result<(), ConsumerOffsetCapacityError> {
+        self.check(id, durable)?;
         *self.pending.borrow_mut().entry(id).or_default() += 1;
-        Ok(if existing {
-            CapacityReservation::Existing
-        } else {
-            CapacityReservation::Reserved
-        })
+        Ok(())
     }
 
     pub(crate) fn check(
         &self,
         id: u32,
         durable: &DurableConsumerOffsets,
-    ) -> Result<bool, ConsumerOffsetCapacityError> {
+    ) -> Result<(), ConsumerOffsetCapacityError> {
+        self.rearm_if_below_limit(durable);
         if durable.contains(self.kind, id)
             || self.pending.borrow().contains_key(&id)
-            || self.provisional.borrow().contains_key(&id)
+            || self
+                .provisional
+                .borrow()
+                .get(&id)
+                .is_some_and(|token| token.strong_count() > 0)
             || self.stranded.borrow().contains(&id)
         {
-            return Ok(true);
+            return Ok(());
         }
+        self.provisional
+            .borrow_mut()
+            .retain(|_, token| token.strong_count() > 0);
         let occupied = self.occupied(durable);
         let limit = self.limit.get();
         if self.uncertain.get() || occupied >= limit {
             return Err(ConsumerOffsetCapacityError {
                 kind: self.kind,
-                occupied: occupied.max(limit),
+                occupied,
                 limit,
                 first_in_episode: !self.durable_warned.replace(true),
+                uncertain: self.uncertain.get(),
             });
         }
-        Ok(false)
+        Ok(())
     }
 
     pub(crate) fn reserve_provisional(
@@ -241,14 +226,41 @@ impl ConsumerOffsetCapacity {
         durable: &Rc<DurableConsumerOffsets>,
     ) -> Result<AutoCommitReservation, ConsumerOffsetCapacityError> {
         self.check(id, durable)?;
-        *self.provisional.borrow_mut().entry(id).or_default() += 1;
+        let mut provisional = self.provisional.borrow_mut();
+        let token = provisional
+            .get(&id)
+            .and_then(Weak::upgrade)
+            .unwrap_or_else(|| {
+                let token = Arc::new(());
+                provisional.insert(id, Arc::downgrade(&token));
+                token
+            });
         Ok(AutoCommitReservation {
-            capacity: Rc::clone(self),
-            durable: Rc::clone(durable),
+            token,
+            kind: self.kind,
             consumer_id: id,
         })
     }
 
+    pub(crate) fn owns(&self, reservation: &AutoCommitReservation) -> bool {
+        reservation.kind == self.kind
+            && self
+                .provisional
+                .borrow()
+                .get(&reservation.consumer_id)
+                .is_some_and(|token| std::ptr::eq(token.as_ptr(), 
Arc::as_ptr(&reservation.token)))
+    }
+
+    pub(crate) fn protects(&self, id: u32, durable: &DurableConsumerOffsets) 
-> bool {
+        durable.contains(self.kind, id)
+            || self.pending.borrow().contains_key(&id)
+            || self
+                .provisional
+                .borrow()
+                .get(&id)
+                .is_some_and(|token| token.strong_count() > 0)
+    }
+
     pub(crate) fn set_pending_count(&self, id: u32, count: usize) {
         if count == 0 {
             self.pending.borrow_mut().remove(&id);
@@ -257,11 +269,6 @@ impl ConsumerOffsetCapacity {
         }
     }
 
-    pub(crate) fn promote_to_durable(&self, id: u32) {
-        self.release_reservation(id);
-        self.stranded.borrow_mut().remove(&id);
-    }
-
     pub(crate) fn release_reservation(&self, id: u32) {
         let mut pending = self.pending.borrow_mut();
         let Some(count) = pending.get_mut(&id) else {
@@ -315,6 +322,9 @@ impl ConsumerOffsetCapacity {
     }
 
     pub(crate) fn rearm_if_below_limit(&self, durable: 
&DurableConsumerOffsets) {
+        if !self.durable_warned.get() || self.uncertain.get() {
+            return;
+        }
         if self.occupied(durable) < self.limit.get() {
             self.durable_warned.set(false);
         }
@@ -333,6 +343,7 @@ impl ConsumerOffsetCapacity {
             occupied: map_len,
             limit,
             first_in_episode: !self.map_warned.replace(true),
+            uncertain: false,
         })
     }
 
@@ -342,51 +353,36 @@ impl ConsumerOffsetCapacity {
         }
     }
 
-    pub(crate) fn occupied_count(&self, durable: &DurableConsumerOffsets) -> 
usize {
-        self.occupied(durable)
-    }
-
-    fn occupied(&self, durable: &DurableConsumerOffsets) -> usize {
+    pub(crate) fn occupied(&self, durable: &DurableConsumerOffsets) -> usize {
         let pending = self.pending.borrow();
         let provisional = self.provisional.borrow();
         let stranded = self.stranded.borrow();
         durable.count(self.kind)
             + pending
                 .keys()
-                .chain(provisional.keys().filter(|id| 
!pending.contains_key(id)))
                 .chain(
-                    stranded
+                    provisional
                         .iter()
-                        .filter(|id| !pending.contains_key(id) && 
!provisional.contains_key(id)),
+                        .filter(|(id, token)| !pending.contains_key(id) && 
token.strong_count() > 0)
+                        .map(|(id, _)| id),
                 )
+                .chain(stranded.iter().filter(|id| {
+                    !pending.contains_key(id)
+                        && provisional
+                            .get(id)
+                            .is_none_or(|token| token.strong_count() == 0)
+                }))
                 .filter(|id| !durable.contains(self.kind, **id))
                 .count()
     }
 }
 
-/// Owns a poll's reservation until its submitted operation either commits or
-/// loses its reply channel.
-///
-/// Journal reservations are accounted independently,
-/// so cancellation and view changes cannot release another operation's slot.
+/// Keeps a provisional key occupied until the pump admits or drops its 
request.
+#[derive(Debug)]
 pub struct AutoCommitReservation {
-    capacity: Rc<ConsumerOffsetCapacity>,
-    durable: Rc<DurableConsumerOffsets>,
-    consumer_id: u32,
-}
-
-impl Drop for AutoCommitReservation {
-    fn drop(&mut self) {
-        let mut provisional = self.capacity.provisional.borrow_mut();
-        if let Some(count) = provisional.get_mut(&self.consumer_id) {
-            *count -= 1;
-            if *count == 0 {
-                provisional.remove(&self.consumer_id);
-            }
-        }
-        drop(provisional);
-        self.capacity.rearm_if_below_limit(&self.durable);
-    }
+    token: Arc<()>,
+    pub(crate) kind: ConsumerKind,
+    pub(crate) consumer_id: u32,
 }
 
 #[cfg(test)]
@@ -421,9 +417,9 @@ mod tests {
         assert!(capacity.check(9, &durable).is_err());
         drop(first);
         assert!(capacity.check(9, &durable).is_ok());
-        assert_eq!(capacity.occupied_count(&durable), 1);
+        assert_eq!(capacity.occupied(&durable), 1);
         drop(second);
-        assert_eq!(capacity.occupied_count(&durable), 0);
+        assert_eq!(capacity.occupied(&durable), 0);
     }
 
     #[test]
@@ -443,14 +439,8 @@ mod tests {
     fn given_same_pending_key_when_reserved_twice_should_consume_one_slot() {
         let durable = DurableConsumerOffsets::default();
         let capacity = ConsumerOffsetCapacity::new(ConsumerKind::Consumer, 1);
-        assert_eq!(
-            capacity.try_reserve(7, &durable),
-            Ok(CapacityReservation::Reserved)
-        );
-        assert_eq!(
-            capacity.try_reserve(7, &durable),
-            Ok(CapacityReservation::Existing)
-        );
+        assert_eq!(capacity.try_reserve(7, &durable), Ok(()));
+        assert_eq!(capacity.try_reserve(7, &durable), Ok(()));
         assert!(capacity.try_reserve(8, &durable).is_err());
         capacity.release_reservation(7);
         assert!(
@@ -484,7 +474,15 @@ mod tests {
         let error = capacity
             .try_reserve(7, &durable)
             .expect_err("unknown pending state must block new keys");
-        assert_eq!(error.occupied, 4);
+        assert_eq!(error.occupied, 0);
+        assert!(error.uncertain);
+        assert!(error.first_in_episode);
+        assert!(
+            !capacity
+                .try_reserve(8, &durable)
+                .expect_err("the same uncertain episode stays closed")
+                .first_in_episode
+        );
     }
 
     #[test]
diff --git a/core/partitions/src/iggy_partition.rs 
b/core/partitions/src/iggy_partition.rs
index b42d8cb01..352dc60ea 100644
--- a/core/partitions/src/iggy_partition.rs
+++ b/core/partitions/src/iggy_partition.rs
@@ -16,8 +16,7 @@
 // under the License.
 
 use crate::consumer_offset_capacity::{
-    CapacityReservation, ConsumerOffsetCapacity, ConsumerOffsetCapacityError,
-    DurableConsumerOffsets,
+    ConsumerOffsetCapacity, ConsumerOffsetCapacityError, 
DurableConsumerOffsets,
 };
 use crate::iggy_index_writer::IggyIndexWriter;
 use crate::journal::{MessageLookup, PartitionJournal, 
PartitionJournalMemStorage};
@@ -209,6 +208,8 @@ where
     /// server down without the partition moving again in the meantime.
     fatal: Option<FatalCommit>,
     pub(crate) pending_consumer_offset_commits: HashMap<u64, 
PendingConsumerOffsetCommit>,
+    pub(crate) queued_auto_commit_reservations:
+        RefCell<HashMap<(ConsumerKind, u32), 
Vec<crate::AutoCommitReservation>>>,
     /// Committed consumer-offset membership and values. This is deliberately
     /// separate from the eager poll maps because follower-local and 
uncommitted
     /// auto-commit progress must never consume a durable slot or enter a state
@@ -556,6 +557,7 @@ where
             installed_frontier: None,
             fatal: None,
             pending_consumer_offset_commits: HashMap::new(),
+            queued_auto_commit_reservations: RefCell::new(HashMap::new()),
             durable_consumer_offsets: 
Rc::new(DurableConsumerOffsets::default()),
             consumer_offset_capacity: Rc::new(ConsumerOffsetCapacity::new(
                 ConsumerKind::Consumer,
@@ -2009,11 +2011,13 @@ where
         );
     }
 
-    pub fn seed_stranded_consumer_offset(&self, kind: ConsumerKind, 
consumer_id: u32) {
+    pub fn seed_stranded_consumer_offset(&self, kind: ConsumerKind, 
consumer_id: u32) -> bool {
         if !self.durable_consumer_offsets.contains(kind, consumer_id) {
             self.consumer_offset_capacity_for(kind)
                 .record_stranded(consumer_id);
+            return true;
         }
+        false
     }
 
     #[must_use]
@@ -2024,7 +2028,7 @@ where
     #[must_use]
     pub fn occupied_consumer_offset_count(&self, kind: ConsumerKind) -> usize {
         self.consumer_offset_capacity_for(kind)
-            .occupied_count(&self.durable_consumer_offsets)
+            .occupied(&self.durable_consumer_offsets)
     }
 
     #[must_use]
@@ -2118,6 +2122,10 @@ where
     ) -> Result<(), IggyError> {
         let path = self.persisted_offset_path(pending.kind, 
pending.consumer_id);
         let capacity = self.consumer_offset_capacity_for(pending.kind);
+        let creates_group = pending.kind == ConsumerKind::ConsumerGroup
+            && !self
+                .durable_consumer_offsets
+                .contains(pending.kind, pending.consumer_id);
         match pending.mutation {
             // A server auto-commit persists monotonically: its op offset can
             // trail the durably-recorded value (disk-tier polls replicate in
@@ -2134,35 +2142,37 @@ where
                     .durable_consumer_offsets
                     .get(pending.kind, pending.consumer_id);
                 let persisted_high_water = match (path.as_deref(), tracked) {
-                    (None, _) => Some(offset),
+                    (None, _) => offset,
                     (Some(_), Some(state))
                         if state
                             .persisted_high_water
                             .is_some_and(|high_water| offset <= high_water) =>
                     {
-                        state.persisted_high_water
+                        state.persisted_high_water.expect("covered persisted 
value")
                     }
                     (Some(path), Some(state)) => {
                         let value = state.committed_offset.max(offset);
                         persist_offset(path, value, 
self.consumer_offset_enforce_fsync).await?;
-                        Some(value)
+                        value
+                    }
+                    (Some(path), None) => {
+                        persist_offset_max(path, offset, 
self.consumer_offset_enforce_fsync).await?
                     }
-                    (Some(path), None) => Some(
-                        persist_offset_max(path, offset, 
self.consumer_offset_enforce_fsync)
-                            .await?,
-                    ),
                 };
                 self.durable_consumer_offsets.record_auto_commit(
                     pending.kind,
                     pending.consumer_id,
                     if tracked.is_none() {
-                        persisted_high_water.unwrap_or(offset)
+                        persisted_high_water
                     } else {
                         offset
                     },
                     persisted_high_water,
                 );
-                capacity.promote_to_durable(pending.consumer_id);
+                capacity.clear_stranded(pending.consumer_id);
+                if creates_group {
+                    self.consumer_group_offsets_need_reconcile.set(true);
+                }
                 Ok(())
             }
             PendingConsumerOffsetMutation::Upsert(offset) => {
@@ -2175,7 +2185,10 @@ where
                     offset,
                     Some(offset),
                 );
-                capacity.promote_to_durable(pending.consumer_id);
+                capacity.clear_stranded(pending.consumer_id);
+                if creates_group {
+                    self.consumer_group_offsets_need_reconcile.set(true);
+                }
                 Ok(())
             }
             PendingConsumerOffsetMutation::Delete => {
@@ -2206,13 +2219,7 @@ where
         offset: u64,
     ) -> bool {
         self.durable_consumer_offsets
-            .get(kind, consumer_id)
-            .is_some_and(|state| {
-                state.committed_offset >= offset
-                    && state
-                        .persisted_high_water
-                        .is_some_and(|high_water| offset <= high_water)
-            })
+            .covers(kind, consumer_id, offset)
     }
 
     /// Reject completion from a removed partition or a view whose retained
@@ -2222,13 +2229,22 @@ where
         applied.belongs_to(&self.durable_consumer_offsets)
             && self.observed_view == self.consensus.view()
             && !self.offset_reservations_need_resync.get()
-            && !self
+            && (!self
                 .consumer_offset_capacity_for(applied.kind)
                 .is_uncertain()
+                || self
+                    .durable_consumer_offsets
+                    .contains(applied.kind, applied.consumer_id))
     }
 
     fn apply_consumer_offset_commit(&self, pending: 
PendingConsumerOffsetCommit) {
-        if pending.kind == ConsumerKind::ConsumerGroup {
+        if pending.kind == ConsumerKind::ConsumerGroup
+            && (matches!(pending.mutation, 
PendingConsumerOffsetMutation::Delete)
+                || !self
+                    .consumer_group_offsets
+                    .pin()
+                    .contains_key(&ConsumerGroupId(pending.consumer_id as 
usize)))
+        {
             self.consumer_group_offsets_need_reconcile.set(true);
         }
         match pending.mutation {
@@ -2434,6 +2450,9 @@ where
             &request_header,
             committed_reply_body(request_header.operation),
         );
+        if offset.is_some() {
+            self.release_consumer_offset_reservation(kind, consumer_id);
+        }
         // Same rule as the committed path: a submit's waiter takes the reply,
         // because `header.client` is then the VSR consensus id.
         if let Some(waiter) = waiter {
@@ -2487,7 +2506,7 @@ where
         &self,
         kind: ConsumerKind,
         consumer_id: u32,
-    ) -> Result<CapacityReservation, ConsumerOffsetCapacityError> {
+    ) -> Result<(), ConsumerOffsetCapacityError> {
         self.consumer_offset_capacity_for(kind)
             .try_reserve(consumer_id, &self.durable_consumer_offsets)
     }
@@ -2519,7 +2538,9 @@ where
         consumer_id: u32,
         waiter: &mut Option<consensus::Sender<Message<ReplyHeader>>>,
     ) -> bool {
-        if self.consumer_offset_capacity_for(kind).is_uncertain() {
+        if self.consumer_offset_capacity_for(kind).is_uncertain()
+            && !self.durable_consumer_offsets.contains(kind, consumer_id)
+        {
             Self::send_partition_deny_or_log(
                 &self.consensus,
                 header,
@@ -2578,7 +2599,6 @@ where
             || self
                 .consumer_offset_capacity_for(kind)
                 .is_stranded(consumer_id)
-            || self.durable_consumer_offsets.contains(kind, consumer_id)
         {
             Ok(())
         } else {
@@ -2593,12 +2613,22 @@ where
         ReplicaLogContext::from_consensus(self.consensus(), 
PlaneKind::Partitions)
     }
 
+    fn store_offset_range_error(&self, offset: u64) -> Option<IggyError> {
+        let current = self.stats.current_offset();
+        (offset > current || (current == 0 && 
self.stats.messages_count_inconsistent() == 0))
+            .then_some(IggyError::InvalidOffset(offset))
+    }
+
     fn resynchronize_consumer_offset_reservations(&mut self) {
         let current_view = self.consensus.view();
         if current_view == self.observed_view && 
!self.offset_reservations_need_resync.get() {
             return;
         }
 
+        if current_view != self.observed_view {
+            self.queued_auto_commit_reservations.borrow_mut().clear();
+        }
+
         let from_op = self
             .consensus
             .commit_min()
@@ -2665,7 +2695,29 @@ where
         }
         self.observed_view = current_view;
         self.consumer_group_offsets_need_reconcile.set(true);
-        self.offset_reservations_need_resync.set(decode_failed);
+        // Repair and truncation rearm this flag when journal contents change.
+        // A failed decode alone must not cause a full scan on every tick.
+        self.offset_reservations_need_resync.set(false);
+        if !decode_failed && self.consensus.is_primary() {
+            self.reclaim_phantom_offsets(ConsumerKind::Consumer);
+            self.reclaim_phantom_offsets(ConsumerKind::ConsumerGroup);
+        }
+    }
+
+    fn reclaim_phantom_offsets(&self, kind: ConsumerKind) {
+        let capacity = self.consumer_offset_capacity_for(kind);
+        let keep = |id| capacity.protects(id, &self.durable_consumer_offsets);
+        match kind {
+            ConsumerKind::Consumer => self
+                .consumer_offsets
+                .pin()
+                .retain(|id, _| u32::try_from(*id).ok().is_none_or(keep)),
+            ConsumerKind::ConsumerGroup => self
+                .consumer_group_offsets
+                .pin()
+                .retain(|id, _| u32::try_from(id.0).ok().is_none_or(keep)),
+        }
+        
capacity.rearm_map_if_below_limit(self.consumer_offset_map_count(kind));
     }
 
     /// Build an owned [`PollPlan`] synchronously (no `.await`), so the caller
@@ -2724,6 +2776,30 @@ where
             }
         };
 
+        if args.auto_commit
+            && self.consensus.is_primary()
+            && let Ok(pending) = 
PendingConsumerOffsetCommit::try_from_polling_consumer(consumer, 0)
+        {
+            let capacity = self.consumer_offset_capacity_for(pending.kind);
+            if !capacity.is_uncertain()
+                && self.consumer_offset_map_count(pending.kind) >= 
capacity.limit()
+            {
+                let exists = match pending.kind {
+                    ConsumerKind::Consumer => self
+                        .consumer_offsets
+                        .pin()
+                        .contains_key(&(pending.consumer_id as usize)),
+                    ConsumerKind::ConsumerGroup => self
+                        .consumer_group_offsets
+                        .pin()
+                        .contains_key(&ConsumerGroupId(pending.consumer_id as 
usize)),
+                };
+                if !exists {
+                    self.reclaim_phantom_offsets(pending.kind);
+                }
+            }
+        }
+
         // Past the empty-return guards: only now build the auto-commit 
context,
         // whose offset-path `format!()` is wasted on the early returns above.
         let auto_commit = self.auto_commit_ctx(consumer, args.auto_commit);
@@ -3109,6 +3185,23 @@ where
         message: Message<RoutedRequestHeader>,
         reply: Option<consensus::Sender<Message<ReplyHeader>>>,
     ) {
+        self.on_request_with_reservation(message, reply, None).await;
+    }
+
+    #[allow(clippy::too_many_lines)]
+    pub(crate) async fn on_request_with_reservation(
+        &mut self,
+        message: Message<RoutedRequestHeader>,
+        reply: Option<consensus::Sender<Message<ReplyHeader>>>,
+        mut reservation: Option<crate::AutoCommitReservation>,
+    ) {
+        if reservation.as_ref().is_some_and(|reservation| {
+            !self
+                .consumer_offset_capacity_for(reservation.kind)
+                .owns(reservation)
+        }) {
+            return;
+        }
         // Taken by whichever arm answers: the deny paths, the NoAck fast path,
         // or the pipeline entry that fires it at commit. Exactly one runs.
         let mut reply = reply;
@@ -3303,30 +3396,26 @@ where
             // code on this committed-shaped frame (op=commit_max) as success.
             if matches!(message.header().operation, 
Operation::StoreConsumerOffset)
                 && let Some((_, _, Some(requested_offset), _)) = 
consumer_offset
+                && let Some(error) = 
self.store_offset_range_error(requested_offset)
             {
-                let current_offset = self.stats.current_offset();
-                let partition_empty =
-                    self.stats.messages_count_inconsistent() == 0 && 
current_offset == 0;
-                if partition_empty || requested_offset > current_offset {
-                    emit_partition_diag(
-                        tracing::Level::WARN,
-                        &PartitionDiagEvent::new(
-                            ReplicaLogContext::from_consensus(consensus, 
PlaneKind::Partitions),
-                            "rejecting store_consumer_offset for out-of-range 
offset",
-                        )
-                        .with_operation(message.header().operation)
-                        
.with_error(IggyError::InvalidOffset(requested_offset).to_string()),
-                    );
-                    Self::send_partition_deny_or_log(
-                        consensus,
-                        message.header(),
-                        IggyError::InvalidOffset(requested_offset).as_code(),
-                        "store_consumer_offset deny reply send failed",
-                        reply.take(),
+                emit_partition_diag(
+                    tracing::Level::WARN,
+                    &PartitionDiagEvent::new(
+                        ReplicaLogContext::from_consensus(consensus, 
PlaneKind::Partitions),
+                        "rejecting store_consumer_offset for out-of-range 
offset",
                     )
-                    .await;
-                    return;
-                }
+                    .with_operation(message.header().operation)
+                    .with_error(error.to_string()),
+                );
+                Self::send_partition_deny_or_log(
+                    consensus,
+                    message.header(),
+                    error.as_code(),
+                    "store_consumer_offset deny reply send failed",
+                    reply.take(),
+                )
+                .await;
+                return;
             }
 
             // The node-local fast path is safe only for a single-replica
@@ -3392,6 +3481,12 @@ where
                             waiter,
                         )
                         .await;
+                    } else if let Some(reservation) = reservation.take() {
+                        self.queued_auto_commit_reservations
+                            .borrow_mut()
+                            .entry((reservation.kind, reservation.consumer_id))
+                            .or_default()
+                            .push(reservation);
                     }
                     return;
                 }
@@ -3471,6 +3566,23 @@ where
         while promoted < slots_freed {
             let req = self.consensus().pop_queued_request();
             let Some(mut req) = req else { break };
+            let _reservation =
+                
Self::parse_consumer_offset_request(req.message.header().operation, 
&req.message)
+                    .ok()
+                    .and_then(|(kind, id, _, _)| {
+                        if req.message.header().operation != 
Operation::StoreConsumerOffset
+                            || 
!is_auto_commit_client(req.message.header().client)
+                        {
+                            return None;
+                        }
+                        let mut queued = 
self.queued_auto_commit_reservations.borrow_mut();
+                        let reservations = queued.get_mut(&(kind, id))?;
+                        let reservation = reservations.pop();
+                        if reservations.is_empty() {
+                            queued.remove(&(kind, id));
+                        }
+                        reservation
+                    });
 
             // Taken before the preflight so a refusal answers the parked 
waiter
             // instead of waking it with `Canceled`.
@@ -3498,14 +3610,11 @@ where
                     .await;
                     continue;
                 };
-                let current_offset = self.stats.current_offset();
-                if offset > current_offset
-                    || (current_offset == 0 && 
self.stats.messages_count_inconsistent() == 0)
-                {
+                if let Some(error) = self.store_offset_range_error(offset) {
                     Self::send_partition_deny_or_log(
                         self.consensus(),
                         req.message.header(),
-                        IggyError::InvalidOffset(offset).as_code(),
+                        error.as_code(),
                         "queued offset range deny reply send failed",
                         reply_sender.take(),
                     )
@@ -5056,6 +5165,9 @@ where
         send_fail_label: &'static str,
         waiter: Option<consensus::Sender<Message<ReplyHeader>>>,
     ) {
+        if waiter.is_none() && is_auto_commit_client(header.client) {
+            return;
+        }
         let reply = build_deny_reply_from_request(consensus, header, status);
         Self::deliver_reply_or_log(consensus, header, reply, waiter, 
send_fail_label).await;
     }
@@ -6105,10 +6217,7 @@ where
             
crate::state_transfer::strayed_offset_files(self.consumer_offsets_path.as_deref(),
 &[])
                 .into_iter()
                 .filter_map(|path| {
-                    std::path::Path::new(&path)
-                        .file_name()
-                        .and_then(|name| name.to_str())
-                        .and_then(|name| name.parse().ok())
+                    crate::state_transfer::numeric_offset_id(&path)
                         .map(|id| (ConsumerKind::Consumer, id, path))
                 });
         let strayed_groups = crate::state_transfer::strayed_offset_files(
@@ -6117,10 +6226,7 @@ where
         )
         .into_iter()
         .filter_map(|path| {
-            std::path::Path::new(&path)
-                .file_name()
-                .and_then(|name| name.to_str())
-                .and_then(|name| name.parse().ok())
+            crate::state_transfer::numeric_offset_id(&path)
                 .map(|id| (ConsumerKind::ConsumerGroup, id, path))
         });
         for (kind, consumer_id, path) in consumer_paths
@@ -6175,6 +6281,7 @@ where
         }
         self.durable_consumer_offsets.clear();
         self.pending_consumer_offset_commits.clear();
+        self.queued_auto_commit_reservations.borrow_mut().clear();
         self.consumer_offset_capacity
             .rebuild(&self.durable_consumer_offsets, std::iter::empty());
         self.consumer_group_offset_capacity
@@ -8396,6 +8503,220 @@ mod tests {
         );
     }
 
+    #[compio::test]
+    async fn 
given_queued_auto_commit_when_view_changes_should_release_its_provisional_slot()
 {
+        let namespace = IggyNamespace::new(1, 1, 0);
+        let consensus = VsrConsensus::new(
+            TEST_CLUSTER,
+            0,
+            3,
+            namespace.inner(),
+            RecordingBus::default(),
+            LocalPipeline::with_capacities(1, 2),
+        );
+        consensus.init();
+        let mut partition: IggyPartition<RecordingBus> = 
IggyPartition::with_in_memory_storage(
+            Arc::new(PartitionStats::default()),
+            consensus,
+            IggyByteSize::from(1024 * 1024),
+            false,
+        );
+        partition.stats.increment_messages_count(1);
+        partition.set_consumer_offsets_max(2);
+        partition
+            .on_request(
+                store_offset_request(42, 1, ConsumerKind::Consumer, 7, 0, 
AckLevel::Quorum),
+                None,
+            )
+            .await;
+        let reservation = partition
+            .consumer_offset_capacity
+            .reserve_provisional(8, &partition.durable_consumer_offsets)
+            .unwrap();
+        partition
+            .on_request_with_reservation(
+                store_offset_request(
+                    message_bus::AUTO_COMMIT_CLIENT_ID,
+                    1,
+                    ConsumerKind::Consumer,
+                    8,
+                    0,
+                    AckLevel::Quorum,
+                ),
+                None,
+                Some(reservation),
+            )
+            .await;
+        assert_eq!(
+            partition.occupied_consumer_offset_count(ConsumerKind::Consumer),
+            2
+        );
+        assert_eq!(partition.queued_auto_commit_reservations.borrow().len(), 
1);
+        partition.consensus.set_view(3);
+        partition.resynchronize_consumer_offset_reservations();
+        assert!(
+            partition
+                .queued_auto_commit_reservations
+                .borrow()
+                .is_empty()
+        );
+        assert_eq!(
+            partition.occupied_consumer_offset_count(ConsumerKind::Consumer),
+            1
+        );
+    }
+
+    #[compio::test]
+    async fn 
given_auto_commit_guard_when_pump_admits_should_transfer_to_journal_without_reply()
 {
+        let (mut partition, sent) = recording_partition_at(0, 3);
+        partition.stats.increment_messages_count(1);
+        partition.set_consumer_offsets_max(1);
+        let reservation = partition
+            .consumer_offset_capacity
+            .reserve_provisional(7, &partition.durable_consumer_offsets)
+            .unwrap();
+        partition
+            .on_request_with_reservation(
+                store_offset_request(
+                    message_bus::AUTO_COMMIT_CLIENT_ID,
+                    1,
+                    ConsumerKind::Consumer,
+                    7,
+                    0,
+                    AckLevel::Quorum,
+                ),
+                None,
+                Some(reservation),
+            )
+            .await;
+        assert_eq!(partition.pending_consumer_offset_commits.len(), 1);
+        assert_eq!(
+            partition.occupied_consumer_offset_count(ConsumerKind::Consumer),
+            1
+        );
+        assert!(sent.borrow().is_empty());
+        partition
+            .apply_staged_consumer_offset_commit(1)
+            .await
+            .unwrap();
+        assert_eq!(
+            partition.durable_consumer_offset_count(ConsumerKind::Consumer),
+            1
+        );
+        assert!(sent.borrow().is_empty());
+    }
+
+    #[compio::test]
+    async fn 
given_group_offset_updates_when_key_already_exists_should_keep_reconciliation_idle()
 {
+        let (mut partition, _) = recording_partition();
+        partition.stage_consumer_offset_upsert(1, ConsumerKind::ConsumerGroup, 
7, 1, true);
+        partition
+            .apply_staged_consumer_offset_commit(1)
+            .await
+            .unwrap();
+        assert!(partition.consumer_group_offsets_reconcile_needed());
+        assert!(
+            partition
+                .dead_consumer_group_offset_ids(|_| true)
+                .is_empty()
+        );
+        assert!(!partition.consumer_group_offsets_reconcile_needed());
+        partition.stage_consumer_offset_upsert(2, ConsumerKind::ConsumerGroup, 
7, 2, true);
+        partition
+            .apply_staged_consumer_offset_commit(2)
+            .await
+            .unwrap();
+        assert!(!partition.consumer_group_offsets_reconcile_needed());
+        partition.consumer_group_offsets.pin().insert(
+            ConsumerGroupId(8),
+            ConsumerOffset::new(ConsumerKind::ConsumerGroup, 8, 3, 
String::new()),
+        );
+        partition.stage_consumer_offset_upsert(3, ConsumerKind::ConsumerGroup, 
8, 3, true);
+        partition
+            .apply_staged_consumer_offset_commit(3)
+            .await
+            .unwrap();
+        assert!(
+            partition.consumer_group_offsets_reconcile_needed(),
+            "first durable commit must discover an eager map key"
+        );
+    }
+
+    #[compio::test]
+    async fn 
given_primary_view_change_when_phantoms_exist_should_keep_only_protected_keys() 
{
+        let (mut partition, _) = recording_partition_at(0, 3);
+        partition.set_consumer_offsets_max(4);
+        partition.stats.increment_messages_count(1);
+        partition.seed_recovered_consumer_offset(ConsumerKind::Consumer, 7, 0, 
0);
+        partition
+            .on_request(
+                store_offset_request(42, 1, ConsumerKind::Consumer, 8, 0, 
AckLevel::Quorum),
+                None,
+            )
+            .await;
+        let held = partition
+            .consumer_offset_capacity
+            .reserve_provisional(9, &partition.durable_consumer_offsets)
+            .unwrap();
+        for id in 7..=10 {
+            partition.consumer_offsets.pin().insert(
+                id as usize,
+                ConsumerOffset::new(ConsumerKind::Consumer, id, 0, 
String::new()),
+            );
+        }
+        partition.consensus.set_view(3);
+        partition.resynchronize_consumer_offset_reservations();
+        assert_eq!(partition.consumer_offsets.len(), 3);
+        assert!(!partition.consumer_offsets.pin().contains_key(&10));
+        for id in 7..=9 {
+            assert!(partition.consumer_offsets.pin().contains_key(&id));
+        }
+        drop(held);
+    }
+
+    #[test]
+    fn 
given_full_live_map_when_polling_existing_key_should_reclaim_only_for_new_keys()
 {
+        let (mut partition, _) = recording_partition();
+        partition.set_consumer_offsets_max(2);
+        partition.offset_space.committed_seeded = true;
+        partition.seed_recovered_consumer_offset(ConsumerKind::Consumer, 7, 0, 
0);
+        for id in 7..=8 {
+            partition.consumer_offsets.pin().insert(
+                id as usize,
+                ConsumerOffset::new(ConsumerKind::Consumer, id, 0, 
String::new()),
+            );
+        }
+        let args = PollingArgs::new(iggy_common::PollingStrategy::first(), 1, 
true);
+        let _ = partition.build_poll_plan(PollingConsumer::Consumer(7, 0), 
&args, false);
+        assert_eq!(partition.consumer_offsets.len(), 2);
+        let _ = partition.build_poll_plan(PollingConsumer::Consumer(9, 0), 
&args, false);
+        assert_eq!(partition.consumer_offsets.len(), 1);
+        assert!(partition.consumer_offsets.pin().contains_key(&7));
+    }
+
+    #[test]
+    fn 
given_missing_retained_header_when_resync_fails_should_wait_for_new_journal_state()
 {
+        let (mut partition, _) = recording_partition();
+        partition.consensus.sequencer().set_sequence(1);
+        partition.consensus.set_view(1);
+        partition.resynchronize_consumer_offset_reservations();
+        assert!(partition.consumer_offset_capacity.is_uncertain());
+        assert!(!partition.offset_reservations_need_resync.get());
+        partition.seed_recovered_consumer_offset(ConsumerKind::Consumer, 7, 0, 
0);
+        let existing = partition
+            .auto_commit_ctx(PollingConsumer::Consumer(7, 0), true)
+            .unwrap()
+            .apply(0)
+            .unwrap();
+        assert!(partition.auto_commit_admission_ready(&existing));
+        let new = partition
+            .auto_commit_ctx(PollingConsumer::Consumer(8, 0), true)
+            .unwrap()
+            .apply(0)
+            .unwrap();
+        assert!(!partition.auto_commit_admission_ready(&new));
+    }
+
     #[compio::test]
     async fn 
given_full_offset_table_when_storing_new_id_should_reject_before_replication() {
         let (mut partition, sent_to_clients) = recording_partition();
diff --git a/core/partitions/src/iggy_partitions.rs 
b/core/partitions/src/iggy_partitions.rs
index 2a2ee6e1e..d66b96dc7 100644
--- a/core/partitions/src/iggy_partitions.rs
+++ b/core/partitions/src/iggy_partitions.rs
@@ -628,6 +628,22 @@ where
             let _ = waiter.send(build_deny_reply_from_request_header(header, 
status));
         }
     }
+
+    pub async fn on_auto_commit_request(
+        &self,
+        request: Message<RoutedRequestHeader>,
+        reservation: crate::AutoCommitReservation,
+    ) {
+        let namespace = IggyNamespace::from_raw(request.header().group);
+        if self.is_tombstoned(&namespace) {
+            return;
+        }
+        if let Some(partition) = self.get_mut_by_ns(&namespace) {
+            partition
+                .on_request_with_reservation(request, None, Some(reservation))
+                .await;
+        }
+    }
 }
 
 impl<B, SB> Plane<VsrConsensus<B>> for IggyPartitions<B, SB>
diff --git a/core/partitions/src/poll_plan.rs b/core/partitions/src/poll_plan.rs
index 176fc8245..51ae28a28 100644
--- a/core/partitions/src/poll_plan.rs
+++ b/core/partitions/src/poll_plan.rs
@@ -311,6 +311,9 @@ impl PollPlan {
     /// # Errors
     /// Returns a capacity error when auto-commit would create a new live-map
     /// entry after the configured per-kind bound has been reached.
+    /// The serving shard also rejects completion with `TransientNotAccepted`
+    /// if the partition disappeared or changed incarnation during disk I/O.
+    /// No fragments are returned for that rejected completion.
     pub async fn execute(
         self,
     ) -> Result<(PollFragments<4096>, u64, Option<AutoCommitApplied>), 
ConsumerOffsetCapacityError>
@@ -890,64 +893,37 @@ impl AutoCommitCtx {
         offset: u64,
     ) -> Result<AutoCommitApplied, ConsumerOffsetCapacityError> {
         let (kind, consumer_id) = self.kind_and_id();
+        let create = |path: Option<&str>| {
+            ConsumerOffset::new(
+                kind,
+                consumer_id,
+                offset,
+                path.map_or_else(String::new, |path| 
format!("{path}/{consumer_id}")),
+            )
+        };
         let previous_offset = match &self.target {
             AutoCommitTarget::Consumer {
                 offsets,
                 consumer_id,
                 create_path,
-            } => {
-                let consumer_id = *consumer_id;
-                let map: &ConsumerOffsets = offsets;
-                let guard = map.pin();
-                if let Some(existing) = guard.get(&(consumer_id as usize)) {
-                    Some(existing.offset.fetch_max(offset, Ordering::Relaxed))
-                } else {
-                    self.capacity.admit_local_map_key(guard.len())?;
-                    let created = create_path.as_deref().map_or_else(
-                        || {
-                            ConsumerOffset::new(
-                                ConsumerKind::Consumer,
-                                consumer_id,
-                                offset,
-                                String::new(),
-                            )
-                        },
-                        |path| 
ConsumerOffset::default_for_consumer(consumer_id, path),
-                    );
-                    created.offset.store(offset, Ordering::Relaxed);
-                    guard.insert(consumer_id as usize, created);
-                    None
-                }
-            }
+            } => apply_local_offset(
+                offsets,
+                *consumer_id as usize,
+                offset,
+                &self.capacity,
+                || create(create_path.as_deref()),
+            )?,
             AutoCommitTarget::ConsumerGroup {
                 offsets,
                 group_id,
                 create_path,
-            } => {
-                let group_id = *group_id;
-                let key = ConsumerGroupId(group_id as usize);
-                let map: &ConsumerGroupOffsets = offsets;
-                let guard = map.pin();
-                if let Some(existing) = guard.get(&key) {
-                    Some(existing.offset.fetch_max(offset, Ordering::Relaxed))
-                } else {
-                    self.capacity.admit_local_map_key(guard.len())?;
-                    let created = create_path.as_deref().map_or_else(
-                        || {
-                            ConsumerOffset::new(
-                                ConsumerKind::ConsumerGroup,
-                                group_id,
-                                offset,
-                                String::new(),
-                            )
-                        },
-                        |path| ConsumerOffset::default_for_consumer_group(key, 
path),
-                    );
-                    created.offset.store(offset, Ordering::Relaxed);
-                    guard.insert(key, created);
-                    None
-                }
-            }
+            } => apply_local_offset(
+                offsets,
+                ConsumerGroupId(*group_id as usize),
+                offset,
+                &self.capacity,
+                || create(create_path.as_deref()),
+            )?,
         };
         Ok(AutoCommitApplied {
             kind,
@@ -980,13 +956,7 @@ impl AutoCommitApplied {
     ) -> Result<Option<AutoCommitReservation>, ConsumerOffsetCapacityError> {
         if self
             .durable
-            .get(self.kind, self.consumer_id)
-            .is_some_and(|state| {
-                state.committed_offset >= self.offset
-                    && state
-                        .persisted_high_water
-                        .is_some_and(|high_water| self.offset <= high_water)
-            })
+            .covers(self.kind, self.consumer_id, self.offset)
         {
             return Ok(None);
         }
@@ -1007,35 +977,51 @@ impl AutoCommitApplied {
                 offsets,
                 consumer_id,
                 ..
-            } => {
-                let guard = offsets.pin();
-                if let Some(previous) = self.previous_offset {
-                    if let Some(entry) = guard.get(&(*consumer_id as usize)) {
-                        entry.offset.store(previous, Ordering::Relaxed);
-                    }
-                } else {
-                    guard.remove(&(*consumer_id as usize));
-                }
-                guard.len()
-            }
+            } => rollback_local_offset(offsets, *consumer_id as usize, 
self.previous_offset),
             AutoCommitTarget::ConsumerGroup {
                 offsets, group_id, ..
-            } => {
-                let guard = offsets.pin();
-                if let Some(previous) = self.previous_offset {
-                    if let Some(entry) = guard.get(&ConsumerGroupId(*group_id 
as usize)) {
-                        entry.offset.store(previous, Ordering::Relaxed);
-                    }
-                } else {
-                    guard.remove(&ConsumerGroupId(*group_id as usize));
-                }
-                guard.len()
-            }
+            } => rollback_local_offset(
+                offsets,
+                ConsumerGroupId(*group_id as usize),
+                self.previous_offset,
+            ),
         };
         self.capacity.rearm_map_if_below_limit(map_len);
     }
 }
 
+fn rollback_local_offset<K: Hash + Eq + Send + Sync + Copy>(
+    map: &papaya::HashMap<K, ConsumerOffset>,
+    key: K,
+    previous: Option<u64>,
+) -> usize {
+    let guard = map.pin();
+    if let Some(previous) = previous {
+        if let Some(entry) = guard.get(&key) {
+            entry.offset.store(previous, Ordering::Relaxed);
+        }
+    } else {
+        guard.remove(&key);
+    }
+    guard.len()
+}
+
+fn apply_local_offset<K: Hash + Eq + Clone + Send + Sync>(
+    map: &papaya::HashMap<K, ConsumerOffset>,
+    key: K,
+    offset: u64,
+    capacity: &ConsumerOffsetCapacity,
+    create: impl FnOnce() -> ConsumerOffset,
+) -> Result<Option<u64>, ConsumerOffsetCapacityError> {
+    let guard = map.pin();
+    if let Some(existing) = guard.get(&key) {
+        return Ok(Some(existing.offset.fetch_max(offset, Ordering::Relaxed)));
+    }
+    capacity.admit_local_map_key(guard.len())?;
+    guard.insert(key, create());
+    Ok(None)
+}
+
 /// Upsert a committed offset into a lock-free `papaya` offset map: bump an
 /// existing entry in place, or build one via `create_on_miss` on first commit
 /// for a consumer/group that has none yet. Shared by the pump's
diff --git a/core/partitions/src/state_transfer.rs 
b/core/partitions/src/state_transfer.rs
index ff5efe345..41d7f6f09 100644
--- a/core/partitions/src/state_transfer.rs
+++ b/core/partitions/src/state_transfer.rs
@@ -2000,12 +2000,9 @@ where
         let mut consumers = self
             .durable_consumer_offsets
             .committed_entries(ConsumerKind::Consumer);
+        let consumer_map = self.consumer_offsets.pin();
         for (consumer_id, _) in &consumers {
-            if !self
-                .consumer_offsets
-                .pin()
-                .contains_key(&(*consumer_id as usize))
-            {
+            if !consumer_map.contains_key(&(*consumer_id as usize)) {
                 return Err(
                     
PartitionTransferUnavailable::ConsumerOffsetStateInconsistent {
                         kind: ConsumerKind::Consumer,
@@ -2018,12 +2015,9 @@ where
         let mut groups = self
             .durable_consumer_offsets
             .committed_entries(ConsumerKind::ConsumerGroup);
+        let group_map = self.consumer_group_offsets.pin();
         for (consumer_id, _) in &groups {
-            if !self
-                .consumer_group_offsets
-                .pin()
-                .contains_key(&ConsumerGroupId(*consumer_id as usize))
-            {
+            if !group_map.contains_key(&ConsumerGroupId(*consumer_id as 
usize)) {
                 return Err(
                     
PartitionTransferUnavailable::ConsumerOffsetStateInconsistent {
                         kind: ConsumerKind::ConsumerGroup,
@@ -2825,6 +2819,7 @@ where
         }
         self.durable_consumer_offsets.clear();
         self.pending_consumer_offset_commits.clear();
+        self.queued_auto_commit_reservations.borrow_mut().clear();
         self.consumer_offset_capacity
             .rebuild(&self.durable_consumer_offsets, std::iter::empty());
         self.consumer_group_offset_capacity
@@ -3450,7 +3445,7 @@ pub(crate) fn strayed_offset_files(dir: Option<&str>, 
incoming: &[(u32, u64)]) -
         .collect()
 }
 
-fn numeric_offset_id(path: &str) -> Option<u32> {
+pub(crate) fn numeric_offset_id(path: &str) -> Option<u32> {
     Path::new(path).file_name()?.to_str()?.parse().ok()
 }
 
diff --git a/core/sdk/Cargo.toml b/core/sdk/Cargo.toml
index 09ef84d03..93993589c 100644
--- a/core/sdk/Cargo.toml
+++ b/core/sdk/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy"
-version = "0.11.0-edge.6"
+version = "0.11.0-edge.7"
 description = "Iggy is the persistent message streaming platform written in 
Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing 
millions of messages per second."
 edition = "2024"
 rust-version.workspace = true
diff --git a/core/sdk/src/clients/consumer.rs b/core/sdk/src/clients/consumer.rs
index 1f38f2c76..e957cce7a 100644
--- a/core/sdk/src/clients/consumer.rs
+++ b/core/sdk/src/clients/consumer.rs
@@ -618,6 +618,11 @@ unsafe impl Sync for IggyConsumer {}
 /// [`topic()`]: crate::prelude::IggyConsumerBuilder::topic
 /// [`without_encryptor()`]: 
crate::prelude::IggyConsumerBuilder::without_encryptor
 /// [`without_poll_interval()`]: 
crate::prelude::IggyConsumerBuilder::without_poll_interval
+///
+/// A server-side auto-commit poll can fail with `TooManyConsumerOffsets` when
+/// its consumer needs a new offset key at the partition's configured limit.
+/// The rejected poll returns no messages. Existing offset keys remain usable,
+/// and polling with server-side auto-commit disabled allocates no offset key.
 pub struct IggyConsumer {
     initialized: bool,
     shutdown: Arc<AtomicBool>,
diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml
index 1d5911488..553d8d9ff 100644
--- a/core/server/Cargo.toml
+++ b/core/server/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "server"
-version = "0.9.0-edge.6"
+version = "0.9.0-edge.7"
 edition = "2024"
 license = "Apache-2.0"
 publish = false
diff --git a/core/server/config.toml b/core/server/config.toml
index 285e9c807..909b791c5 100644
--- a/core/server/config.toml
+++ b/core/server/config.toml
@@ -1027,6 +1027,9 @@ dedup_clients_max = 4096
 # Distinct durable consumer-offset keys a partition primary admits per kind.
 # Standalone consumers and consumer groups are counted separately. Existing
 # keys remain writable at the limit. A new key is rejected before consensus.
+# A non-empty auto_commit poll that needs a new offset key is also rejected
+# with TooManyConsumerOffsets and returns no messages. Polls with auto_commit
+# disabled do not allocate offset keys and remain available.
 # Must be > 0 and <= 262144.
 consumer_offsets_max = 4096
 
diff --git a/core/server/src/boot/recovery.rs b/core/server/src/boot/recovery.rs
index 0610e826a..917306bdc 100644
--- a/core/server/src/boot/recovery.rs
+++ b/core/server/src/boot/recovery.rs
@@ -336,7 +336,7 @@ const _: () = assert!(
         == partitions::DEFAULT_CONSUMER_OFFSETS_MAX
 );
 const _: () = assert!(
-    2 * configs::partition::PARTITION_CONSUMER_OFFSETS_CEILING
+    4 * configs::partition::PARTITION_CONSUMER_OFFSETS_CEILING
         <= partitions::CONSUMER_OFFSETS_ENTRIES_MAX as usize
 );
 const _: () =
diff --git a/core/server/src/consumer_group.rs 
b/core/server/src/consumer_group.rs
index 2f347782f..1a6beed9b 100644
--- a/core/server/src/consumer_group.rs
+++ b/core/server/src/consumer_group.rs
@@ -25,7 +25,7 @@
 //! primary enriches the op here before replication, mirroring the PAT mint
 //! in [`crate::pat`] and the password hash in [`crate::users`].
 
-use crate::responses::resolve_partition_namespace;
+use crate::responses::{missing_consumer_group_error, 
resolve_partition_namespace};
 use crate::shell::{ShellBus, ShellShard};
 use crate::wire::{request_body, rewrite_request_body};
 use consensus::MetadataHandle;
@@ -235,9 +235,12 @@ where
             if wire.consumer.kind != KIND_CONSUMER_GROUP {
                 return Ok(request);
             }
-            let group_id =
-                resolve_group_offset_id(shard, &wire.consumer, 
(&wire.stream_id, &wire.topic_id))
-                    .ok_or(IggyError::InvalidIdentifier)?;
+            let group_id = resolve_group_offset_id(
+                shard,
+                &wire.consumer,
+                (&wire.stream_id, &wire.topic_id),
+            )
+            .ok_or_else(|| missing_consumer_group_error(&wire.consumer.id, 
&wire.topic_id))?;
             // The partition-plane group-offset key is u32 (see the documented
             // ceiling on `Topic::next_consumer_group_id`). Clamp on the
             // ~4-billion-creates overflow rather than panic this live
@@ -270,9 +273,6 @@ where
     S: 'static,
     SB: SuperblockStore + 'static,
 {
-    if consumer.kind != KIND_CONSUMER_GROUP {
-        return None;
-    }
     shard
         .plane
         .metadata()
diff --git a/core/server/src/dispatch/partition.rs 
b/core/server/src/dispatch/partition.rs
index 1c8e21711..48bbe0b5b 100644
--- a/core/server/src/dispatch/partition.rs
+++ b/core/server/src/dispatch/partition.rs
@@ -51,7 +51,7 @@ use iggy_binary_protocol::PrepareHeader;
 use iggy_binary_protocol::primitives::consumer::WireConsumer;
 use iggy_binary_protocol::primitives::polling_strategy::WirePollingStrategy;
 use iggy_binary_protocol::requests::consumer_offsets::{
-    DeleteConsumerOffsetRequest, GetConsumerOffsetRequest, 
StoreConsumerOffsetRequest,
+    GetConsumerOffsetRequest, StoreConsumerOffsetRequest,
 };
 use iggy_binary_protocol::requests::messages::PollMessagesRequest;
 use iggy_binary_protocol::requests::segments::DeleteSegmentsRequest;
@@ -128,7 +128,7 @@ where
                             Err(error) => {
                                 
shard.metrics().record_consumer_offset_denied(error.kind);
                                 warn_auto_commit_capacity(namespace, error);
-                                
PartitionReadReply::Rejected(IggyError::TooManyConsumerOffsets)
+                                PartitionReadReply::Rejected(error.into())
                             }
                         };
                         let _ = reply.try_send(result);
@@ -228,7 +228,7 @@ fn spawn_poll_io<B, MJ, S, SB>(
             Err(error) => {
                 shard.metrics().record_consumer_offset_denied(error.kind);
                 warn_auto_commit_capacity(namespace, error);
-                PartitionReadReply::Rejected(IggyError::TooManyConsumerOffsets)
+                PartitionReadReply::Rejected(error.into())
             }
         };
         let _ = reply.try_send(result);
@@ -296,7 +296,7 @@ where
             shard.metrics().record_consumer_offset_denied(applied.kind);
             warn_auto_commit_capacity(namespace, error);
             applied.rollback_created();
-            return Err(IggyError::TooManyConsumerOffsets);
+            return Err(error.into());
         }
     };
     let message = match build_auto_commit_request(namespace, applied) {
@@ -313,15 +313,13 @@ where
     };
     // Routes by namespace to this same owning primary shard's inbox. The pump
     // admits it next turn exactly like a client store. `dispatch` never 
blocks.
-    let Ok(ticket) = shard.partition_submit(namespace, message) else {
+    if shard
+        .submit_auto_commit_offset(message, reservation)
+        .is_err()
+    {
         applied.rollback_created();
         return Err(IggyError::TransientNotAccepted);
-    };
-    let shard = Rc::clone(shard);
-    shard.bus.clone().spawn(async move {
-        let _ = shard.await_partition_submit(ticket).await;
-        drop(reservation);
-    });
+    }
     applied.mark_served();
     Ok(())
 }
@@ -449,7 +447,10 @@ pub async fn dispatch_partition_request<B, MJ, S, SB>(
                 operation = ?header.operation,
                 "partition request with unresolved namespace; replying denied"
             );
-            let status = if error == IggyError::InvalidIdentifier {
+            let status = if matches!(
+                error,
+                IggyError::ConsumerGroupIdNotFound(..) | 
IggyError::ConsumerGroupNameNotFound(..)
+            ) {
                 error.as_code()
             } else {
                 IggyError::ResourceNotFound(String::new()).as_code()
@@ -650,22 +651,12 @@ async fn relay_partition_reply<B, MJ, S, SB>(
 }
 
 fn consumer_offset_kind(request: &Message<RoutedRequestHeader>) -> 
Option<ConsumerKind> {
-    let kind = match request.header().operation {
-        Operation::StoreConsumerOffset => {
-            StoreConsumerOffsetRequest::decode_from(request_body(request))
-                .ok()?
-                .consumer
-                .kind
-        }
-        Operation::DeleteConsumerOffset => {
-            DeleteConsumerOffsetRequest::decode_from(request_body(request))
-                .ok()?
-                .consumer
-                .kind
-        }
-        _ => return None,
-    };
-    ConsumerKind::from_code(kind).ok()
+    if request.header().operation != Operation::StoreConsumerOffset {
+        return None;
+    }
+    // WireConsumer starts with its kind byte. The dispatch path has already
+    // decoded and validated the complete request.
+    ConsumerKind::from_code(*request_body(request).first()?).ok()
 }
 
 /// Serve `poll_messages`: resolve the partition namespace, run the read on
diff --git a/core/server/src/partition_helpers.rs 
b/core/server/src/partition_helpers.rs
index b8fc36c3d..6c3f888c7 100644
--- a/core/server/src/partition_helpers.rs
+++ b/core/server/src/partition_helpers.rs
@@ -276,10 +276,16 @@ pub fn configure_consumer_offsets(
         enforce_fsync,
     );
     for consumer_id in numeric_offset_file_ids(&consumer_offsets_path) {
-        partition.seed_stranded_consumer_offset(ConsumerKind::Consumer, 
consumer_id);
+        if partition.seed_stranded_consumer_offset(ConsumerKind::Consumer, 
consumer_id) {
+            warn!(stream_id, topic_id, partition_id, consumer_id, path = 
%consumer_offsets_path,
+                "unloaded consumer offset file retains its capacity slot until 
updated or deleted");
+        }
     }
     for group_id in numeric_offset_file_ids(&consumer_group_offsets_path) {
-        partition.seed_stranded_consumer_offset(ConsumerKind::ConsumerGroup, 
group_id);
+        if 
partition.seed_stranded_consumer_offset(ConsumerKind::ConsumerGroup, group_id) {
+            warn!(stream_id, topic_id, partition_id, group_id, path = 
%consumer_group_offsets_path,
+                "unloaded group offset file retains its capacity slot until 
repaired or reclaimed");
+        }
     }
     for kind in [ConsumerKind::Consumer, ConsumerKind::ConsumerGroup] {
         let count = partition.occupied_consumer_offset_count(kind);
diff --git a/core/server/src/partition_reconciler.rs 
b/core/server/src/partition_reconciler.rs
index 21f018085..8fedc7204 100644
--- a/core/server/src/partition_reconciler.rs
+++ b/core/server/src/partition_reconciler.rs
@@ -194,6 +194,7 @@ use tracing::{debug, error, trace};
 
 const BACKOFF_BASE: Duration = Duration::from_secs(1);
 const BACKOFF_MAX: Duration = Duration::from_mins(1);
+const GROUP_OFFSET_DELETES_PER_PASS: usize = 32;
 
 /// Consecutive same-cause failures before [`ReconcilerCtx::record_failure`]
 /// escalates to an operator-visible error (the backoff is capped, so
@@ -235,6 +236,8 @@ pub struct ReconcilerCtx {
     /// `true` when the previous pass made no changes. Only then is a
     /// same-`revision` pass safe to skip.
     last_pass_noop: Cell<bool>,
+    group_offset_cleanup_inflight: Rc<RefCell<AHashSet<IggyNamespace>>>,
+    group_offset_cleanup_completed: Rc<Cell<usize>>,
 }
 
 impl ReconcilerCtx {
@@ -257,6 +260,8 @@ impl ReconcilerCtx {
             failure_state: RefCell::new(AHashMap::new()),
             last_revision: Cell::new(None),
             last_pass_noop: Cell::new(false),
+            group_offset_cleanup_inflight: 
Rc::new(RefCell::new(AHashSet::new())),
+            group_offset_cleanup_completed: Rc::new(Cell::new(0)),
         }
     }
 
@@ -389,11 +394,11 @@ struct PassCounters {
     backoff_skipped: usize,
     /// Stale incarnations (slab-key reuse) torn down for rebuild.
     stale: usize,
-    /// Consumer-group offsets reclaimed for groups deleted while their topic
-    /// survived (a bare `DeleteConsumerGroup`, not a topic/stream delete).
-    cg_offsets_purged: usize,
-    /// Consumer-group offset files whose unlink failed and remain queued for
-    /// the next pass. Counted so the revision fast-skip cannot strand them.
+    /// Group-offset deletes successfully handed to the pump this pass.
+    cg_offsets_submitted: usize,
+    /// Successful replicated deletes reported by detached completion tasks.
+    cg_offsets_completed: usize,
+    /// Group-offset deletes refused by the inbox and needing another pass.
     cg_offsets_pending: usize,
     /// Committed delete watermarks not yet fully enforced on local segments.
     /// Counted so the pass does not arm the fast-skip: the pump can be
@@ -432,7 +437,8 @@ impl PassCounters {
             + self.removed_routed
             + self.backoff_skipped
             + self.stale
-            + self.cg_offsets_purged
+            + self.cg_offsets_submitted
+            + self.cg_offsets_completed
             + self.cg_offsets_pending
             + self.trims_pending
             + self.purges_staged
@@ -474,6 +480,7 @@ async fn reconcile_once(ctx: &ReconcilerCtx) -> bool {
     if ctx.last_revision.get() == Some(revision)
         && ctx.last_pass_noop.get()
         && ctx.failure_state.borrow().is_empty()
+        && ctx.group_offset_cleanup_completed.get() == 0
         && !ctx.shard.has_parked_partition_frames()
         && !ctx.shard.plane.partitions().namespaces().any(|namespace| {
             ctx.shard
@@ -481,6 +488,10 @@ async fn reconcile_once(ctx: &ReconcilerCtx) -> bool {
                 .partitions()
                 .with_partition(namespace, |partition| {
                     partition.consumer_group_offsets_reconcile_needed()
+                        && !ctx
+                            .group_offset_cleanup_inflight
+                            .borrow()
+                            .contains(namespace)
                 })
                 .unwrap_or(false)
         })
@@ -494,12 +505,15 @@ async fn reconcile_once(ctx: &ReconcilerCtx) -> bool {
 
     let target = snapshot_target_namespaces(ctx);
     let target_set: AHashSet<IggyNamespace> = target.iter().map(|partition| 
partition.ns).collect();
-    let mut counters = PassCounters::default();
+    let mut counters = PassCounters {
+        cg_offsets_completed: ctx.group_offset_cleanup_completed.replace(0),
+        ..PassCounters::default()
+    };
 
     reconcile_additions(ctx, target, &mut counters).await;
     reconcile_removals(ctx, &target_set, &mut counters).await;
     reconcile_parked_frames(ctx, &mut counters);
-    reconcile_consumer_group_offsets(ctx, &mut counters).await;
+    reconcile_consumer_group_offsets(ctx, &mut counters);
     reconcile_segment_truncations(ctx, &mut counters);
     reconcile_partition_purges(ctx, &mut counters);
 
@@ -982,11 +996,18 @@ async fn tear_down_owned_partition(
 
 /// Reclaim deleted groups through ordered offset deletes. Replicas must see
 /// each delete before a replacement store can reuse its durable slot.
-async fn reconcile_consumer_group_offsets(ctx: &ReconcilerCtx, counters: &mut 
PassCounters) {
+fn reconcile_consumer_group_offsets(ctx: &ReconcilerCtx, counters: &mut 
PassCounters) {
     let live_groups = snapshot_topic_live_groups(ctx);
     let partitions = ctx.shard.plane.partitions();
     let owned: Vec<IggyNamespace> = partitions.namespaces().copied().collect();
     for namespace in owned {
+        if ctx
+            .group_offset_cleanup_inflight
+            .borrow()
+            .contains(&namespace)
+        {
+            continue;
+        }
         let live = live_groups.get(&(namespace.stream_id(), 
namespace.topic_id()));
         let dead = partitions
             .with_partition(&namespace, |partition| {
@@ -997,32 +1018,48 @@ async fn reconcile_consumer_group_offsets(ctx: 
&ReconcilerCtx, counters: &mut Pa
             .unwrap_or_default();
         // Bound work per pass so a historical directory cannot monopolize the
         // reconciler. Unprocessed keys keep the partition's dirty flag armed.
-        let mut tickets = Vec::with_capacity(dead.len().min(32));
-        for consumer_id in dead.into_iter().take(32) {
-            counters.cg_offsets_pending += 1;
+        let mut tickets = 
Vec::with_capacity(dead.len().min(GROUP_OFFSET_DELETES_PER_PASS));
+        for consumer_id in 
dead.into_iter().take(GROUP_OFFSET_DELETES_PER_PASS) {
             let request = group_offset_delete_request(namespace, consumer_id);
             if let Ok(ticket) = ctx.shard.partition_submit(namespace, request) 
{
+                counters.cg_offsets_submitted += 1;
                 tickets.push(ticket);
+            } else {
+                counters.cg_offsets_pending += 1;
             }
         }
-        let replies = futures::future::join_all(
-            tickets
-                .into_iter()
-                .map(|ticket| ctx.shard.await_partition_submit(ticket)),
-        )
-        .await;
-        for reply in replies {
-            let Some(reply) = reply else {
-                continue;
-            };
-            let header = reply
-                .as_slice()
-                .get(..size_of::<ReplyHeader>())
-                .and_then(|bytes| 
bytemuck::checked::try_from_bytes::<ReplyHeader>(bytes).ok());
-            if header.is_some_and(|header| header.status == 0) {
-                counters.cg_offsets_purged += 1;
-            }
+        if tickets.is_empty() {
+            continue;
         }
+        ctx.group_offset_cleanup_inflight
+            .borrow_mut()
+            .insert(namespace);
+        let inflight = Rc::clone(&ctx.group_offset_cleanup_inflight);
+        let completed = Rc::clone(&ctx.group_offset_cleanup_completed);
+        let shard = Rc::clone(&ctx.shard);
+        shard.bus.clone().spawn(async move {
+            let replies = futures::future::join_all(
+                tickets
+                    .into_iter()
+                    .map(|ticket| shard.await_partition_submit(ticket)),
+            )
+            .await;
+            let count = replies
+                .into_iter()
+                .flatten()
+                .filter(|reply| {
+                    reply
+                        .as_slice()
+                        .get(..size_of::<ReplyHeader>())
+                        .and_then(|bytes| {
+                            
bytemuck::checked::try_from_bytes::<ReplyHeader>(bytes).ok()
+                        })
+                        .is_some_and(|header| header.status == 0)
+                })
+                .count();
+            completed.set(completed.get() + count);
+            inflight.borrow_mut().remove(&namespace);
+        });
     }
 }
 
@@ -1331,8 +1368,9 @@ pub fn install_tick_handler(shard: &Rc<ServerShard>, 
wake_tx: WakeTx) {
 #[cfg(test)]
 mod tests {
     use super::{
-        FailureCause, FailureRecord, ReconcilerCtx, build_partition_fresh,
-        delete_partitions_from_disk, fetch_partition_stats, reconcile_once,
+        FailureCause, FailureRecord, PassCounters, ReconcilerCtx, 
build_partition_fresh,
+        delete_partitions_from_disk, fetch_partition_stats, 
reconcile_consumer_group_offsets,
+        reconcile_once,
     };
     use configs::server::{ServerConfig, ServerSystemConfig};
     use consensus::{MetadataHandle, PartitionsHandle};
@@ -3241,6 +3279,60 @@ mod tests {
 
     /// Failed delivery must leave the offset and its quota slot intact for a
     /// later replicated delete. This fixture deliberately has no running pump.
+    #[compio::test]
+    async fn 
given_pending_group_cleanup_when_another_topic_arrives_should_reconcile_without_waiting()
+     {
+        let tmp = TempDir::new().unwrap();
+        let config = test_config(&tmp);
+        let mux = TestMux::default();
+        seed_stream(&mux, 1, "cleanup-stream");
+        seed_topic(&mux, 2, 0, "cleanup-topic", vec![assignment(0, 1)]);
+        let (shard, _inbox) = build_test_shard_with_inbox(0, &config, mux, 32);
+        let ctx = make_ctx(Rc::clone(&shard), 1, Rc::new(config));
+        reconcile_pass(&ctx).await;
+        let namespace = IggyNamespace::new(0, 0, 0);
+        shard
+            .plane
+            .partitions()
+            .with_partition(&namespace, |partition| {
+                partition.consumer_group_offsets.pin().insert(
+                    iggy_common::ConsumerGroupId(7),
+                    iggy_common::ConsumerOffset::new(
+                        iggy_common::ConsumerKind::ConsumerGroup,
+                        7,
+                        0,
+                        String::new(),
+                    ),
+                );
+            });
+        reconcile_consumer_group_offsets(&ctx, &mut PassCounters::default());
+        assert!(
+            ctx.group_offset_cleanup_inflight
+                .borrow()
+                .contains(&namespace)
+        );
+        seed_topic(
+            &shard.plane.metadata().mux_stm,
+            3,
+            0,
+            "unrelated-topic",
+            vec![assignment(0, 1)],
+        );
+        reconcile_pass(&ctx).await;
+        assert!(
+            shard
+                .plane
+                .partitions()
+                .contains(&IggyNamespace::new(0, 1, 0)),
+            "an unanswered cleanup must not block partition creation"
+        );
+        assert!(
+            ctx.group_offset_cleanup_inflight
+                .borrow()
+                .contains(&namespace)
+        );
+    }
+
     #[compio::test]
     async fn 
given_deleted_group_when_cleanup_submit_fails_should_preserve_state_for_retry() 
{
         use iggy_common::{ConsumerGroupId, ConsumerKind, ConsumerOffset};
diff --git a/core/server/src/responses.rs b/core/server/src/responses.rs
index cc0654af1..2b9b524a5 100644
--- a/core/server/src/responses.rs
+++ b/core/server/src/responses.rs
@@ -279,12 +279,6 @@ where
     }
     let partition_id = partition_id.ok_or(IggyError::InvalidIdentifier)?;
     let streams = shard.plane.metadata().mux_stm.streams();
-    if streams
-        .resolve_consumer_group_id(stream_id, topic_id, &consumer.id)
-        .is_none()
-    {
-        return Err(IggyError::InvalidIdentifier);
-    }
     #[allow(clippy::cast_possible_truncation)]
     streams
         // Commit fence: allow a pending-revoked partition (the source commits 
it
@@ -298,10 +292,28 @@ where
             false,
         )
         .map(|_| ())
-        .ok_or(IggyError::ConsumerGroupPartitionNotOwned(
-            client_id as u32,
-            partition_id,
-        ))
+        .ok_or_else(|| {
+            if streams
+                .resolve_consumer_group_id(stream_id, topic_id, &consumer.id)
+                .is_none()
+            {
+                missing_consumer_group_error(&consumer.id, topic_id)
+            } else {
+                IggyError::ConsumerGroupPartitionNotOwned(client_id as u32, 
partition_id)
+            }
+        })
+}
+
+pub fn missing_consumer_group_error(group: &WireIdentifier, topic: 
&WireIdentifier) -> IggyError {
+    let topic = wire_identifier_for_display(topic);
+    match group {
+        WireIdentifier::Numeric(_) => {
+            
IggyError::ConsumerGroupIdNotFound(wire_identifier_for_display(group), topic)
+        }
+        WireIdentifier::String(name) => {
+            IggyError::ConsumerGroupNameNotFound(name.as_str().to_owned(), 
topic)
+        }
+    }
 }
 
 /// Fence a consumer-group offset op then resolve its target partition
diff --git a/core/shard/src/lib.rs b/core/shard/src/lib.rs
index 4dbcdf428..2922b8150 100644
--- a/core/shard/src/lib.rs
+++ b/core/shard/src/lib.rs
@@ -114,6 +114,7 @@ where
 pub struct PartitionMaterialisation {
     epoch: u64,
     created_view: u32,
+    consumer_offsets_max: usize,
 }
 
 #[cfg(feature = "simulator")]
@@ -123,8 +124,15 @@ impl PartitionMaterialisation {
         Self {
             epoch,
             created_view,
+            consumer_offsets_max: partitions::DEFAULT_CONSUMER_OFFSETS_MAX,
         }
     }
+
+    #[must_use]
+    pub const fn with_consumer_offsets_max(mut self, consumer_offsets_max: 
usize) -> Self {
+        self.consumer_offsets_max = consumer_offsets_max;
+        self
+    }
 }
 
 /// Replica id + count bundle.
@@ -736,6 +744,12 @@ pub enum LifecycleFrame {
         request: Message<RoutedRequestHeader>,
         reply: Sender<Option<Message<GenericHeader>>>,
     },
+    /// Local auto-commit submission. The guard travels with the frame so an
+    /// inbox drop or admission refusal releases its provisional key directly.
+    AutoCommitSubmit {
+        request: Message<RoutedRequestHeader>,
+        reservation: partitions::AutoCommitReservation,
+    },
     /// Shard 0 broadcasts after a partition-shaped metadata commit; wakes
     /// the per-shard reconciler. No payload: reconciler re-reads target
     /// state. Drops covered by the periodic safety tick.
@@ -2010,6 +2024,32 @@ where
         })
     }
 
+    /// Submit an auto-commit back to the partition-owning shard's pump.
+    ///
+    /// # Errors
+    /// Returns a refusal if the local inbox cannot accept the frame.
+    pub fn submit_auto_commit_offset(
+        &self,
+        request: Message<RoutedRequestHeader>,
+        reservation: partitions::AutoCommitReservation,
+    ) -> Result<(), PartitionSubmitRefused> {
+        let frame = ShardFrame::lifecycle(LifecycleFrame::AutoCommitSubmit {
+            request,
+            reservation,
+        });
+        let sender = self
+            .senders
+            .get(usize::from(self.id))
+            .ok_or(PartitionSubmitRefused)?;
+        sender.try_send(frame).map_err(|error| {
+            self.metrics.record_frame_drop(
+                crate::metrics::frame_drop_variant::PARTITION,
+                crate::coordinator::classify_try_send_err(&error),
+            );
+            PartitionSubmitRefused
+        })
+    }
+
     /// Wait out a submitted write's committed reply.
     ///
     /// `None` = reply channel dropped before a reply (view-change reset, park
@@ -3966,7 +4006,6 @@ where
     // and `adopt_retained_log` are configured out and the crate does not 
compile.
     // The feature forwards to `partitions/simulator` instead.
     #[cfg(feature = "simulator")]
-    #[allow(clippy::too_many_arguments)]
     pub fn init_partition(
         &self,
         namespace: IggyNamespace,
@@ -3975,7 +4014,6 @@ where
         retained: Option<partitions::RetainedPartitionState>,
         restore_frontier: bool,
         materialisation: PartitionMaterialisation,
-        consumer_offsets_max: usize,
     ) where
         B: MessageBus + Clone + 'static,
         T: ShardsTable,
@@ -3983,6 +4021,7 @@ where
         let PartitionMaterialisation {
             epoch,
             created_view,
+            consumer_offsets_max,
         } = materialisation;
         let partitions = self.plane.partitions();
         if partitions.contains(&namespace) {
diff --git a/core/shard/src/metrics.rs b/core/shard/src/metrics.rs
index d355b0381..91b6a2001 100644
--- a/core/shard/src/metrics.rs
+++ b/core/shard/src/metrics.rs
@@ -189,6 +189,13 @@ fn reason_index(s: &str) -> Option<usize> {
     REASONS.iter().position(|r| *r == s)
 }
 
+const fn consumer_kind_index(kind: ConsumerKind) -> usize {
+    match kind {
+        ConsumerKind::Consumer => 0,
+        ConsumerKind::ConsumerGroup => 1,
+    }
+}
+
 /// Per-shard metric handles.
 ///
 /// Cheap to clone (`Arc` of a `Family` under the hood). Each shard owns
@@ -271,21 +278,13 @@ impl ShardMetrics {
     }
 
     pub fn record_consumer_offset_denied(&self, kind: ConsumerKind) {
-        let index = match kind {
-            ConsumerKind::Consumer => 0,
-            ConsumerKind::ConsumerGroup => 1,
-        };
-        self.consumer_offset_denied_counters[index].inc();
+        self.consumer_offset_denied_counters[consumer_kind_index(kind)].inc();
     }
 
-    #[cfg(any(test, feature = "simulator"))]
+    #[cfg(test)]
     #[must_use]
     pub fn consumer_offset_denied_value(&self, kind: ConsumerKind) -> u64 {
-        let index = match kind {
-            ConsumerKind::Consumer => 0,
-            ConsumerKind::ConsumerGroup => 1,
-        };
-        self.consumer_offset_denied_counters[index].get()
+        self.consumer_offset_denied_counters[consumer_kind_index(kind)].get()
     }
 
     /// Bumped every time a client request is answered with a retryable denial
diff --git a/core/shard/src/router.rs b/core/shard/src/router.rs
index 4a3a0d214..b7465d2b7 100644
--- a/core/shard/src/router.rs
+++ b/core/shard/src/router.rs
@@ -728,6 +728,15 @@ where
                 // already made.
                 self.on_partition_submit(request, reply).await;
             }
+            LifecycleFrame::AutoCommitSubmit {
+                request,
+                reservation,
+            } => {
+                self.plane
+                    .partitions()
+                    .on_auto_commit_request(request, reservation)
+                    .await;
+            }
             LifecycleFrame::MetadataCommitTick => {
                 // Reconciler may not yet be wired (e.g. mid-bootstrap, or
                 // single-shard tests that never enable the reconciler loop).
diff --git a/core/simulator/src/lib.rs b/core/simulator/src/lib.rs
index a4e055c82..242d4004f 100644
--- a/core/simulator/src/lib.rs
+++ b/core/simulator/src/lib.rs
@@ -505,11 +505,6 @@ impl Simulator {
         self.consumer_offsets_max = consumer_offsets_max;
     }
 
-    #[must_use]
-    pub const fn consumer_offsets_max(&self) -> usize {
-        self.consumer_offsets_max
-    }
-
     #[must_use]
     /// # Panics
     /// Panics when `replica_idx` is outside the simulated roster.
@@ -1350,8 +1345,10 @@ impl Simulator {
         };
         // Partitions are driven directly, so a poll's auto-commit is never
         // replicated (the serving shard's job in the real server). Offset 
discarded.
-        let (fragments, _commit_offset, _auto_commit) = 
futures::executor::block_on(plan.execute())
-            .map_err(|_| IggyError::TooManyConsumerOffsets)?;
+        let (fragments, _commit_offset, auto_commit) = 
futures::executor::block_on(plan.execute())?;
+        if let Some(applied) = auto_commit {
+            applied.mark_served();
+        }
         Ok(fragments)
     }
 
@@ -1527,8 +1524,8 @@ fn materialise_partition(
         recovered_state,
         retained,
         restore_frontier,
-        PartitionMaterialisation::new(epoch, created_view),
-        consumer_offsets_max,
+        PartitionMaterialisation::new(epoch, created_view)
+            .with_consumer_offsets_max(consumer_offsets_max),
     );
     for shard in &replica.shards {
         shard.shards_table().insert(
@@ -3059,7 +3056,6 @@ mod tests {
                     None,
                     false,
                     PartitionMaterialisation::new(0, 0),
-                    partitions::DEFAULT_CONSUMER_OFFSETS_MAX,
                 );
             });
             executor.run_until_stalled(POLL_BUDGET); // grow while the borrow 
is live
@@ -3102,7 +3098,6 @@ mod tests {
                 None,
                 false,
                 PartitionMaterialisation::new(0, 0),
-                partitions::DEFAULT_CONSUMER_OFFSETS_MAX,
             );
         });
         executor.run_until_stalled(POLL_BUDGET);
diff --git a/core/simulator/src/workload/effect.rs 
b/core/simulator/src/workload/effect.rs
index a9d8c868c..e8df466e9 100644
--- a/core/simulator/src/workload/effect.rs
+++ b/core/simulator/src/workload/effect.rs
@@ -17,7 +17,7 @@
 
 //! Predicted server-state mutations emitted by op modules on commit.
 //!
-//! Name-keyed throughout. Server-ng emits empty reply bodies, so the
+//! Name-keyed throughout. Server emits empty reply bodies, so the
 //! workload cannot recover server-assigned numeric ids; shadow lookups
 //! address entities by name (`WireIdentifier::named`). Id-keyed effects
 //! return once reply-body parsing lands.
diff --git a/core/simulator/src/workload/shadow.rs 
b/core/simulator/src/workload/shadow.rs
index 2692ff168..b0e0ea8cb 100644
--- a/core/simulator/src/workload/shadow.rs
+++ b/core/simulator/src/workload/shadow.rs
@@ -17,7 +17,7 @@
 
 //! Shadow state: the workload's prediction of server-side entity state.
 //!
-//! Name-keyed throughout. Server-ng does not yet ship reply bodies, so
+//! Name-keyed throughout. Server does not yet ship reply bodies, so
 //! the workload cannot observe server-assigned numeric ids. Lookups are
 //! by name; requests route via `WireIdentifier::named(...)`. When typed
 //! response bodies land, id-keyed maps return as a parallel index;
diff --git a/examples/node/package-lock.json b/examples/node/package-lock.json
index fdf8f7883..2847ff873 100644
--- a/examples/node/package-lock.json
+++ b/examples/node/package-lock.json
@@ -23,7 +23,7 @@
     },
     "../../foreign/node": {
       "name": "apache-iggy",
-      "version": "0.10.0-edge.5",
+      "version": "0.10.0-edge.6",
       "license": "Apache-2.0",
       "dependencies": {
         "@node-rs/xxhash": "1.7.7",
diff --git a/examples/python/uv.lock b/examples/python/uv.lock
index bde592d6b..469565c7e 100644
--- a/examples/python/uv.lock
+++ b/examples/python/uv.lock
@@ -8,7 +8,7 @@ exclude-newer-span = "P7D"
 
 [[package]]
 name = "apache-iggy"
-version = "0.9.0.dev6"
+version = "0.9.0.dev7"
 source = { directory = "../../foreign/python" }
 
 [package.metadata]
diff --git a/foreign/go/contracts/version.go b/foreign/go/contracts/version.go
index 32ff7a282..f3799086d 100644
--- a/foreign/go/contracts/version.go
+++ b/foreign/go/contracts/version.go
@@ -17,4 +17,4 @@
 
 package iggcon
 
-const Version = "0.9.0-edge.5"
+const Version = "0.9.0-edge.6"
diff --git a/foreign/go/errors/errors.yaml b/foreign/go/errors/errors.yaml
index 7219af03e..e9f8b7c96 100644
--- a/foreign/go/errors/errors.yaml
+++ b/foreign/go/errors/errors.yaml
@@ -777,7 +777,7 @@
       type: string
 - name: TooManyConsumerOffsets
   code: 3024
-  format: "too many consumer offsets"
+  format: "too many consumer offsets for partition"
   fields: []
 - name: PartitionIdSpaceExhausted
   code: 3013
diff --git a/foreign/go/errors/errors_gen.go b/foreign/go/errors/errors_gen.go
index 67641e984..06031cd66 100644
--- a/foreign/go/errors/errors_gen.go
+++ b/foreign/go/errors/errors_gen.go
@@ -1586,7 +1586,7 @@ func (e CannotOpenConsumerOffsetsFile) Is(target error) 
bool {
 
 type TooManyConsumerOffsets struct{}
 
-func (e TooManyConsumerOffsets) Error() string { return "too many consumer 
offsets" }
+func (e TooManyConsumerOffsets) Error() string { return "too many consumer 
offsets for partition" }
 func (e TooManyConsumerOffsets) Code() Code    { return 3024 }
 func (e TooManyConsumerOffsets) Is(target error) bool {
        _, ok := target.(TooManyConsumerOffsets)
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java
 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java
index 992f3b094..5d776f827 100644
--- 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java
+++ 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java
@@ -93,6 +93,8 @@ public enum IggyErrorCode {
     // Partition errors
     PARTITION_NOT_FOUND(3007),
     PARTITION_ID_SPACE_EXHAUSTED(3013),
+
+    // Consumer offset errors
     TOO_MANY_CONSUMER_OFFSETS(3024),
 
     // Segment errors
diff --git a/foreign/node/package-lock.json b/foreign/node/package-lock.json
index 69b394e60..10bbf5baf 100644
--- a/foreign/node/package-lock.json
+++ b/foreign/node/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "apache-iggy",
-  "version": "0.10.0-edge.5",
+  "version": "0.10.0-edge.6",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "apache-iggy",
-      "version": "0.10.0-edge.5",
+      "version": "0.10.0-edge.6",
       "license": "Apache-2.0",
       "dependencies": {
         "@node-rs/xxhash": "1.7.7",
diff --git a/foreign/node/package.json b/foreign/node/package.json
index 6722c7161..ea4ec4dd0 100644
--- a/foreign/node/package.json
+++ b/foreign/node/package.json
@@ -1,7 +1,7 @@
 {
   "name": "apache-iggy",
   "type": "module",
-  "version": "0.10.0-edge.5",
+  "version": "0.10.0-edge.6",
   "description": "Official Apache Iggy NodeJS SDK",
   "keywords": [
     "iggy",
diff --git a/foreign/node/src/wire/error.code.test.ts 
b/foreign/node/src/wire/error.code.test.ts
index fc4cb34cb..edbaf7367 100644
--- a/foreign/node/src/wire/error.code.test.ts
+++ b/foreign/node/src/wire/error.code.test.ts
@@ -20,7 +20,7 @@ import { it } from 'node:test';
 import { translateErrorCode } from './error.code.js';
 
 it('translates the consumer-offset capacity error', () => {
-  assert.equal(translateErrorCode(3024), 'Too many consumer offsets');
+  assert.equal(translateErrorCode(3024), 'Too many consumer offsets for 
partition');
 });
 
 it('translates the consumer-group error range', () => {
diff --git a/foreign/node/src/wire/error.code.ts 
b/foreign/node/src/wire/error.code.ts
index d0a1f3d78..2cdb31e2f 100644
--- a/foreign/node/src/wire/error.code.ts
+++ b/foreign/node/src/wire/error.code.ts
@@ -165,7 +165,7 @@ export const translateErrorCode = (code: number): string => 
{
     case '3021': return "Consumer offset for consumer with ID: {0} was not 
found.";
     case '3022': return "Failed to resolve consumer with ID: {0}";
     case '3023': return "Cannot open consumer offsets file for path: {0}";
-    case '3024': return "Too many consumer offsets";
+    case '3024': return "Too many consumer offsets for partition";
     case '3013': return "Partition id space exhausted for this topic";
 
     // MESSAGE
diff --git a/foreign/python/Cargo.toml b/foreign/python/Cargo.toml
index f3965a16e..1430a98fc 100644
--- a/foreign/python/Cargo.toml
+++ b/foreign/python/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "apache-iggy"
-version = "0.9.0-dev6"
+version = "0.9.0-dev7"
 edition = "2024"
 authors = ["Iggy Committers <[email protected]>"]
 license = "Apache-2.0"
@@ -37,7 +37,7 @@ doc = false
 [dependencies]
 bytes = "1.12.1"
 futures = "0.3.34"
-iggy = { path = "../../core/sdk", version = "0.11.0-edge.6" }
+iggy = { path = "../../core/sdk", version = "0.11.0-edge.7" }
 paste = "1"
 pyo3 = "0.29.2"
 pyo3-async-runtimes = { version = "0.29.0", features = [
diff --git a/foreign/python/pyproject.toml b/foreign/python/pyproject.toml
index 54d53ec90..1434010dc 100644
--- a/foreign/python/pyproject.toml
+++ b/foreign/python/pyproject.toml
@@ -22,7 +22,7 @@ build-backend = "maturin"
 [project]
 name = "apache-iggy"
 requires-python = ">=3.10"
-version = "0.9.0.dev6"
+version = "0.9.0.dev7"
 description = "Apache Iggy is the persistent message streaming platform 
written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of 
processing millions of messages per second."
 readme = "README.md"
 license = { file = "LICENSE" }
diff --git a/foreign/python/uv.lock b/foreign/python/uv.lock
index d6f3eb506..f4fce79c9 100644
--- a/foreign/python/uv.lock
+++ b/foreign/python/uv.lock
@@ -8,7 +8,7 @@ exclude-newer-span = "P7D"
 
 [[package]]
 name = "apache-iggy"
-version = "0.9.0.dev6"
+version = "0.9.0.dev7"
 source = { editable = "." }
 
 [package.optional-dependencies]

Reply via email to