+Full benchmark results: 0.9.0 → next version
+
+
+#### Single node: 0.9.0 → next version
+
+Each cell shows **0.9.0 → next version with optimizations**. The next-version results are from a development snapshot.
+
+| Workload (configured rate limit) | Achieved MB/s | p50 ms | p99 ms | p999 ms |
+| --- | ---: | ---: | ---: | ---: |
+| pinned producer, 20 actors, 800 MB/s | 800.1 → 800.1 | 0.650 → 0.588 | 1.313 → 1.145 | 11.087 → 2.956 |
+| pinned producer, 20 actors, persisted, 800 MB/s | 800.0 → 800.1 | 1.232 → 1.098 | 1.601 → 1.424 | 2.249 → 1.967 |
+| pinned consumer, 20 actors, cold, 800 MB/s | 800.1 → 800.1 | 0.498 → 0.447 | 4.698 → 3.313 | 5.156 → 3.777 |
+| pinned consumer, 20 actors, warm, 800 MB/s | 800.1 → 800.1 | 0.521 → 0.544 | 1.022 → 0.923 | 1.573 → 3.920 |
+| pinned producer, 1 actor, 500 MB/s | 500.0 → 500.0 | 0.347 → 0.352 | 0.679 → 0.683 | 1.356 → 1.046 |
+| pinned consumer, 1 actor, warm, 500 MB/s | 500.0 → 500.0 | 0.272 → 0.296 | 0.468 → 0.478 | 0.491 → 0.502 |
+| pinned producer, 1 actor, persisted, 500 MB/s | 372.1 → 335.8 | 0.665 → 0.733 | 0.825 → 0.901 | 0.995 → 1.151 |
+
+#### 3-node cluster: 0.9.0 → next version
+
+Each cell shows **0.9.0 → next version with optimizations**. The next-version results are from a development snapshot.
+
+| Workload (configured rate limit) | Achieved MB/s | p50 ms | p99 ms | p999 ms |
+| --- | ---: | ---: | ---: | ---: |
+| pinned producer, 20 actors, persisted, 800 MB/s | 800.0 → 800.0 | 2.905 → 2.243 | 7.073 → 2.938 | 12.403 → 3.889 |
+| pinned producer, 1 actor, persisted, 500 MB/s | 234.4 → 233.6 | 1.047 → 1.062 | 1.200 → 1.223 | 10.857 → 1.759 |
+| pinned consumer, 20 actors, cold, 800 MB/s | 800.1 → 800.0 | 0.477 → 0.436 | 3.831 → 2.346 | 4.241 → 2.675 |
+| pinned consumer, 20 actors, warm, 800 MB/s | 800.1 → 797.9 | 0.484 → 0.451 | 0.933 → 0.812 | 1.233 → 1.216 |
+| pinned producer, 20 actors, 800 MB/s | 800.0 → 800.0 | 1.782 → 1.679 | 3.186 → 3.250 | 3.683 → 3.660 |
+| pinned producer, 1 actor, 500 MB/s | 337.2 → 325.4 | 0.716 → 0.735 | 0.973 → 1.016 | 1.057 → 1.130 |
+| pinned consumer, 1 actor, warm, 500 MB/s | 500.0 → 500.3 | 0.297 → 0.326 | 0.493 → 0.536 | 0.533 → 0.574 |
+
+The single-node warm consumer with 20 actors saw p999 rise from 1.6 ms to 3.9 ms. Some single-actor rows also lost throughput or added latency. **These numbers are a snapshot of work in progress, not a final result.** The branch is being profiled and tuned as we write this, with further changes aimed at throughput and tail latency.
+
+
+
+
+The tails are where the work shows. On a three-node cluster with `persisted` topics, the **p99 of a 20-producer workload dropped from 7.07 ms to 2.94 ms and the p999 from 12.4 ms to 3.9 ms**. The single-producer p999 on the same cluster went from 10.9 ms to 1.8 ms. On a single node, the 20-producer **p999 fell from 11.1 ms to 3.0 ms**. Cold consumer reads improved on both setups. In this development snapshot, 20 persisted producers on a three-node cluster achieved 800 MB/s with a median latency of 2.243 ms.
+
+### Three changes behind the numbers
+
+Three changes are in flight, and the tables above measure them together against 0.9.0. These runs do not isolate how much each change contributes. A fourth is on the drawing board.
+
+#### Consumers stop asking for nothing
+
+A polling consumer pays a round trip for nothing on an idle topic, and on a busy one its interval trades log latency against empty replies.
+
+`pollMessagesDeferred` (commands 105 and 106, tracked in [#3470](https://github.com/apache/iggy/issues/3470)) appends a 24-byte trailer to the poll request: a maximum wait, a minimum message count, a byte cap for the reply, and a total request budget. The server parks the request and answers when the minimum is readable, the cap is reached, or the wait expires. The Rust client speaks it on every transport, the Java client over TCP and HTTP, and the Rust consumer now defaults to bounded long polling with manual commits. It answers a [community request for deferred responses](https://github.com/apache/iggy/discussions/2854).
+
+#### File I/O leaves the shard loop
+
+A shard owns many partitions and used to wait for each one's file writes and durability barriers before serving the next, so one slow disk delayed every partition on the shard.
+
+Now each file job runs as its own task, up to 16 in flight by default, and its result returns to the shard for identity matching and ordered publication. Storage work now overlaps with network work.
+
+#### Replica links read ahead
+
+Every pair of nodes shares one TCP connection for consensus traffic, owned by a single link shard. Reading one frame took two `io_uring` reads, one for the 256-byte header and one for the body, and frames already in the kernel receive queue waited for the next call. Under load that shard bottlenecked the cluster write path.
+
+A 256 KiB read-ahead buffer ([#4224](https://github.com/apache/iggy/pull/4224)) now serves a whole burst with one socket read. A frame at least as large as the buffer skips it and lands in its own allocation, so large messages pay no extra copy.
+
+#### The next one: more replica links
+
+The read-ahead makes each read cheaper, but every partition group a pair of nodes replicates still goes through one link shard.
+
+The next change gives a peer several links, spread over shards. An operator picks the link count per peer, each link carries a contiguous range of shards, and the default stays at today's one. The upgrade rolls, but changing the link count takes a coordinated restart, and nodes that disagree on it refuse each other.
+
+This one is a design, not code, and the read-ahead has to justify it first. More links only make sense when the link shard stays saturated behind the buffer. Otherwise the cheaper fix has already won.
+
+All three measured changes are still in development, and every number above will move before they ship. A driver for the OpenMessaging Benchmark is in progress as well, and its results will follow.
+
+Beyond these, in the order we expect them to land:
+
+- **Explicit group commit**: several acknowledgements share one disk barrier by design, rather than by whatever happened to queue up during the previous one
+- **Direct I/O** for the storage engine, writing past the page cache instead of paying for `fsync` barriers
+- **Multi-leader clusters**, further out: a leader per partition, with leaders spread across all nodes of the cluster, so write traffic and replication work are shared by every machine instead of concentrating on one
+
+And a long tail of smaller changes in the same direction. Efficiency is the reason Iggy exists, and it stays the priority now that clustering is in.
+
+A like-for-like comparison of 0.8.0 against 0.9.0 with this parameter set is planned for the [benchmarking platform](https://benchmarks.iggy.apache.org), together with the final numbers for the next release. The [benchmarking guide](/docs/server/benchmarking) has the commands to reproduce these workloads on your own hardware, and the [Linux tuning guide](/docs/server/linux-tuning) covers the host preparation.
+
+---
+
+## Iggy Server
+
+### One server, one wire protocol
+
+- **[server-ng promoted to the Apache Iggy server](https://github.com/apache/iggy/pull/3856)**: the thread-per-core, `io_uring` server is now the only server. The `iggy-server` binary, the `server` package, the configs tree and the systemd integration are the former server-ng, and all transports are kept: TCP with TLS, QUIC, WebSocket and HTTP. The legacy server, its compat lane and the classic TCP, QUIC and WebSocket framing were deleted (**BREAKING**).
+- **[Legacy message wire format and v2 bridge APIs removed](https://github.com/apache/iggy/pull/3904)** from the SDK. The new message wire format is encoded on the client side (**BREAKING**).
+- **[Namespace removed from client headers](https://github.com/apache/iggy/pull/3836)**, a smaller header on every request
+- **[Server-only types extracted into `server_common`](https://github.com/apache/iggy/pull/3298)**, **[shard allocator extracted into `cpu_allocation`](https://github.com/apache/iggy/pull/3578)**, **[compio executor and io_uring diagnostics extracted into a shared crate](https://github.com/apache/iggy/pull/3331)**, the server's **[module graph turned into an enforced DAG](https://github.com/apache/iggy/pull/4023)**, and the dispatch spine **[split into plane-named leaves](https://github.com/apache/iggy/pull/4026)**
+
+### Topic options
+
+- **[Extensible key-value options for topics, streams and users](https://github.com/apache/iggy/pull/3880)** (**BREAKING**): retention, durability and segment layout are per-topic decisions made at creation. `segment_size`, `messages_required_to_save`, `size_of_messages_required_to_save` and the new `preallocate_segments` moved from server-wide `[system.*]` keys to the topic. Unknown keys are rejected at the edge, never silently ignored, and `iggy options topic` (or `GET /options/topic`) lists what the server accepts. The server refuses to boot on the relocated configuration keys. See [Topic Options](/docs/server/topic-options).
+- **[Durability policies](https://github.com/apache/iggy/pull/4092)** as described above (**BREAKING**: `enforce_fsync` is rejected, `[system]` is flattened into the root, and `IGGY_SYSTEM_*` variables lose the `SYSTEM_` segment)
+
+### Storage and recovery
+
+- **[Log and index persisted concurrently, recovery hardened](https://github.com/apache/iggy/pull/3970)**: an index is derived from the log and is never evidence about the log. Recovery drops a divergent index whole and rebuilds it from a byte-0 walk, checksumming every batch it accepts. A local persist failure now stops the whole server instead of silently killing one shard.
+- **[Torn segment tails truncated instead of resurrected](https://github.com/apache/iggy/pull/3946)**, **[WAL suffix truncated above the state-transfer floor](https://github.com/apache/iggy/pull/3918)**, **[recovery slot collisions rejected](https://github.com/apache/iggy/pull/4009)**
+- **[Offsets reserved before they are confirmed](https://github.com/apache/iggy/pull/3975)**, **[polls completed on the owning shard](https://github.com/apache/iggy/pull/4119)**, **[producer batches preserved across replication](https://github.com/apache/iggy/pull/3859)**
+- **[Data persisted on VSR backups, with a data integrity test](https://github.com/apache/iggy/pull/3512)**
+- **[Deleted partitions and topics rolled out of parent stats](https://github.com/apache/iggy/pull/4046)**, **[namespace caps enforced at admission](https://github.com/apache/iggy/pull/3907)**
+- **[Expired personal access token cleaner](https://github.com/apache/iggy/pull/3448)**
+
+### HTTP, security and operations
+
+- **[Cluster-correct HTTP listener with per-operation RBAC](https://github.com/apache/iggy/pull/3622)**, **[HTTPS on the REST listener](https://github.com/apache/iggy/pull/3672)**, **[CORS honored](https://github.com/apache/iggy/pull/3646)**, **[Web UI served by the server](https://github.com/apache/iggy/pull/3676)**
+- **[PAT limits enforced and Prometheus metrics exposed](https://github.com/apache/iggy/pull/3773)**
+- **[Wildcard bind without an advertised address rejected](https://github.com/apache/iggy/pull/3923)** (**BREAKING**): a `0.0.0.0` bind says where a node listens, not where a client can reach it. Deployments that bind a wildcard must set `node.advertised_address`. The Helm chart derives it from the Service DNS name.
+- **[Trusted-issuer A2A JWT verification with SDK token refresh](https://github.com/apache/iggy/pull/3626)**, **[message encryption in the replicated server](https://github.com/apache/iggy/pull/3644)**
+- **[`read_servers` permission required for the system snapshot](https://github.com/apache/iggy/pull/3579)**, **[`get_user` self-read restored without `read_users`](https://github.com/apache/iggy/pull/3897)**
+- **[systemd watchdog integration](https://github.com/apache/iggy/pull/3233)** for the server and the MCP server
+- **[Listener, auth, shutdown and disk-poll paths hardened](https://github.com/apache/iggy/pull/3774)**, **[configuration surface hardened](https://github.com/apache/iggy/pull/3756)**, and the **[server config cleaned up after a full field audit](https://github.com/apache/iggy/pull/3895)**
+- **[Sibling `IGGY` environment variables allowed](https://github.com/apache/iggy/pull/4149)**, **[deterministic environment variable name suggestions](https://github.com/apache/iggy/pull/3142)**
+- **[io_uring panic from an unsupported opcode diagnosed](https://github.com/apache/iggy/pull/3589)**, **[shard startup kept off blocking fallbacks](https://github.com/apache/iggy/commit/8c4986ac545c1b75afd40b11c425d35b7fbbe058)**
+- **[Partitions distributed evenly in cooperative rebalancing](https://github.com/apache/iggy/pull/3374)**
+- **[Non-numeric filenames in consumer offset directories skipped instead of panicking](https://github.com/apache/iggy/pull/3135)**
+- **[Iggy banner on server startup](https://github.com/apache/iggy/pull/4085)**
+- Rust toolchain moved to **[1.98](https://github.com/apache/iggy/pull/3962)**, with an **[MSRV of 1.95 declared and enforced in CI](https://github.com/apache/iggy/pull/3558)**
+
+### Breaking changes
+
+This is a major upgrade. Read this list before moving an existing deployment:
+
+- **Data directory**: the on-disk format changed, and there is no migration of 0.8.x data directories. Until 1.0.0, an upgrade means starting from a fresh data directory.
+- **[Wire protocol](https://github.com/apache/iggy/pull/3856)**: VSR framing is the only binary wire protocol. 0.8.x clients cannot talk to a 0.9.0 server and vice versa. Upgrade the server and the SDKs together.
+- **[Legacy message wire format and v2 bridge APIs removed](https://github.com/apache/iggy/pull/3904)** from the Rust SDK
+- **[Server configuration](https://github.com/apache/iggy/pull/3856)**: the `[cluster]` and sharding sections were reworked. Review your `config.toml`.
+- **[Topic options](https://github.com/apache/iggy/pull/3880)**: `segment_size`, `messages_required_to_save`, `size_of_messages_required_to_save` and related keys are per-topic. The server refuses to boot on the old `[system.*]` keys.
+- **[Durability](https://github.com/apache/iggy/pull/4092)**: `enforce_fsync` and `consumer_offset_enforce_fsync` are rejected. `[system]` is flattened into the root of the config and `IGGY_SYSTEM_*` variables drop `SYSTEM_`. The HTTP `Iggy-Durability` header reports `replicated` or `persisted`.
+- **[Cluster metadata requires authentication](https://github.com/apache/iggy/pull/3872)**: `PING` is the only pre-auth command
+- **[Wildcard bind requires an advertised address](https://github.com/apache/iggy/pull/3923)**, and `cluster.nodes[*].ip` must be a literal IP
+- **[Connector SDK `ConnectivityConfig` replaced by `RetryPolicy`](https://github.com/apache/iggy/pull/4104)** (Rust API only, built plugins are unaffected)
+
+The release is available as a [signed source archive](/downloads/) from the Apache Software Foundation. Docker images are available on **[Docker Hub](https://hub.docker.com/u/apache?page=1&search=iggy)**: `apache/iggy:0.9.0` for the server and `apache/iggy-connect:0.5.0` for the connectors runtime.
+
+---
+
+## SDKs
+
+Every SDK migrated to the VSR wire protocol and runs its test suite against the replicated server only. All of them expose the new durability options and a raw command API for custom commands.
+
+### Rust
+
+- **Rust client bumped to 0.11.0**
+- **[VSR header framing](https://github.com/apache/iggy/pull/3254)**, the client side of the new protocol
+- **[Fail over to a surviving node when the current one dies](https://github.com/apache/iggy/pull/3944)**, **[rejoin consumer groups after membership loss](https://github.com/apache/iggy/pull/3703)**, **[late offset stores no longer hit a left group](https://github.com/apache/iggy/pull/3668)**
+- **[Raw requests for custom commands](https://github.com/apache/iggy/pull/3373)** and **[unknown command codes forwarded instead of rejected](https://github.com/apache/iggy/pull/3766)**, so the protocol can be extended without forking the client
+- **[Consumer group polling and commits corrected in `IggyConsumer`](https://github.com/apache/iggy/pull/4034)**, **[consumer progress and error handling preserved](https://github.com/apache/iggy/pull/4151)**, **[stream terminates after consumer shutdown](https://github.com/apache/iggy/pull/3964)**
+- **[`max_buffer_size` respected when merging batches](https://github.com/apache/iggy/pull/3933)**, **[error callback awaits shard flush on producer shutdown](https://github.com/apache/iggy/pull/3953)**
+- **[`NonZeroIggyDuration` for retries and heartbeats](https://github.com/apache/iggy/pull/3891)**
+- Documentation for **[`IggyClient`](https://github.com/apache/iggy/pull/3809)**, **[`IggyProducer`](https://github.com/apache/iggy/pull/3989)** and **[`IggyConsumer`](https://github.com/apache/iggy/pull/3913)**
+
+### PHP (new)
+
+A brand new SDK. **[The PHP SDK](https://github.com/apache/iggy/pull/3235)** is a native PHP extension built in Rust with `ext-php-rs`. It wraps the Rust SDK and exposes a synchronous `Iggy\Client` covering streams, topics, messages and consumer groups. It **[maps Iggy errors to typed exceptions](https://github.com/apache/iggy/pull/3382)**, ships a **[consumer message iterator](https://github.com/apache/iggy/pull/3463)** and **[examples](https://github.com/apache/iggy/pull/3368)**, and runs a **[BDD suite](https://github.com/apache/iggy/pull/3410)** with **[TLS tests in CI](https://github.com/apache/iggy/pull/3381)**. It is defined as the `apache/iggy-php` composer package, is not yet published to a package manager, and is built from source today. See the [PHP SDK documentation](/docs/sdk/php/intro).
+
+### Python
+
+Python received more pull requests than any other SDK in this release and closed most of its API gap with the Rust client:
+
+- **[High-level producer API](https://github.com/apache/iggy/pull/4156)** and **[message partitioning strategies](https://github.com/apache/iggy/pull/3927)**
+- **[TCP](https://github.com/apache/iggy/pull/3776)**, **[QUIC](https://github.com/apache/iggy/pull/3991)**, **[HTTP](https://github.com/apache/iggy/pull/3992)** and **[WebSocket](https://github.com/apache/iggy/pull/4000)** transport configuration
+- Stream and topic management: **[stream listing, update, delete, purge](https://github.com/apache/iggy/pull/3701)**, **[topic listing, update, delete, purge](https://github.com/apache/iggy/pull/3572)**, **[remaining Topic fields and partitions](https://github.com/apache/iggy/pull/3623)**, **[partition management](https://github.com/apache/iggy/pull/4017)**
+- Consumer groups: **[create and get](https://github.com/apache/iggy/pull/3581)**, **[delete, join and leave](https://github.com/apache/iggy/pull/3607)**, **[`poll_messages` takes a consumer](https://github.com/apache/iggy/pull/3877)**
+- Users: **[user management](https://github.com/apache/iggy/pull/3695)**, **[permissions and remaining auth methods](https://github.com/apache/iggy/pull/3727)**, **[`update_user` options](https://github.com/apache/iggy/pull/4173)**
+- **[User headers and origin timestamp](https://github.com/apache/iggy/pull/3613)**, **[`get_stats`](https://github.com/apache/iggy/pull/4018)**
+- **[ruff configured, modern coding standards](https://github.com/apache/iggy/pull/3318)**, **[pyrefly type checking](https://github.com/apache/iggy/pull/3323)**, **[Windows wheels restored](https://github.com/apache/iggy/pull/4049)**
+
+### Java
+
+- **[TCP client migrated to the VSR wire protocol](https://github.com/apache/iggy/pull/3841)**
+- **[Cluster metadata and leader redirection](https://github.com/apache/iggy/pull/3745)**
+- **[TCP clients run one I/O thread or share a group](https://github.com/apache/iggy/pull/4096)**
+- **[Wire strings sized by UTF-8 byte length](https://github.com/apache/iggy/pull/4068)**
+- Java benchmark: **[CLI and resource provisioner](https://github.com/apache/iggy/pull/3159)**, **[actor and data batch generator](https://github.com/apache/iggy/pull/3218)**, **[reporting suite](https://github.com/apache/iggy/pull/3420)**
+- **[Pinot connector E2E coverage](https://github.com/apache/iggy/pull/3922)**
+
+### C\#
+
+- **[Migrated to the VSR wire protocol](https://github.com/apache/iggy/pull/3800)**, with the **[TCP connection cleaned up afterwards](https://github.com/apache/iggy/pull/3858)**
+- **[Heartbeat mechanism](https://github.com/apache/iggy/pull/3894)**
+- **[Rented message polling](https://github.com/apache/iggy/pull/3250)** and **[rented payloads](https://github.com/apache/iggy/pull/3471)** for fewer allocations
+- **[Encryption moved to the raw client](https://github.com/apache/iggy/pull/3639)**, **[missing metrics in `StatsResponse`](https://github.com/apache/iggy/pull/3221)**, **[integer properties made unsigned](https://github.com/apache/iggy/pull/4027)**
+- **[UTF-8 byte count for string validation](https://github.com/apache/iggy/pull/4072)**, **[OS default socket buffer size](https://github.com/apache/iggy/pull/4013)**
+
+### Go
+
+- **[Migrated to the VSR wire protocol](https://github.com/apache/iggy/pull/3834)**
+- **[`context.Context` on client methods](https://github.com/apache/iggy/pull/2964)**
+- **[Transport state separated from session state](https://github.com/apache/iggy/pull/3652)**, **[structured logger](https://github.com/apache/iggy/pull/3379)** aligned with the Rust SDK
+- **[Request-path wire payload pooled](https://github.com/apache/iggy/pull/3442)**, **[delete segments](https://github.com/apache/iggy/pull/3191)**
+- Fixed **[bounds checking for payloads over 64 KB](https://github.com/apache/iggy/pull/3165)**, **[S2 decode returning an error instead of panicking](https://github.com/apache/iggy/pull/3166)**, **[teardown of only the failed connection](https://github.com/apache/iggy/pull/3986)**, **[`MaxPayloadSize` raised to 64 MB](https://github.com/apache/iggy/pull/4080)**
+
+### Node.js (TypeScript)
+
+- **[VSR framing](https://github.com/apache/iggy/pull/3763)** and **[classic framing dropped](https://github.com/apache/iggy/pull/3843)**
+- **[Connection string syntax](https://github.com/apache/iggy/pull/3917)**
+- **[TLS with VSR](https://github.com/apache/iggy/pull/3813)**, **[scoped permissions wire codec](https://github.com/apache/iggy/pull/3642)**, **[tokens no longer dropped in list responses](https://github.com/apache/iggy/pull/4171)**
+- **[npm prereleases published under a channel tag, not `latest`](https://github.com/apache/iggy/pull/3370)**
+- Node.js client bumped to 0.10.0
+
+### C++
+
+The low-level bindings from 0.8.0 grew into a client:
+
+- **[Messaging FFI functions](https://github.com/apache/iggy/pull/3046)**, **[system functions](https://github.com/apache/iggy/pull/3102)**, **[consumer groups and topics](https://github.com/apache/iggy/pull/3506)**, **[offsets](https://github.com/apache/iggy/pull/3559)**, **[client lifecycle and message headers](https://github.com/apache/iggy/pull/3610)**
+- **[User management and an initial high-level client](https://github.com/apache/iggy/pull/3733)**, extended with **[stream, topic and partition functions](https://github.com/apache/iggy/pull/4022)**
+- **[BDD tests for basic messaging](https://github.com/apache/iggy/pull/3554)**, **[Bazel upgraded to 9.1.1](https://github.com/apache/iggy/pull/3511)**
+
+### CLI
+
+- **CLI bumped to 0.14.0**, **[with VSR support](https://github.com/apache/iggy/pull/3679)**
+- **[`context show` and `session status` commands](https://github.com/apache/iggy/pull/3096)**
+- **[Pure-Rust zbus keyring instead of libdbus](https://github.com/apache/iggy/pull/3326)**, one system dependency fewer
+- **[Bounded ping retries](https://github.com/apache/iggy/pull/3903)**, **[CLI input validated](https://github.com/apache/iggy/pull/4150)**
+
+---
+
+## Connectors
+
+The connectors runtime, now 0.5.0, gained eight new sinks, a new source, a new payload format, and a long list of reliability fixes.
+
+### New connectors
+
+- **[S3 sink](https://github.com/apache/iggy/pull/3103)**
+- **[ClickHouse sink](https://github.com/apache/iggy/pull/2886)**
+- **[Delta Lake sink](https://github.com/apache/iggy/pull/2889)**
+- **[Apache Doris sink](https://github.com/apache/iggy/pull/3215)**, with **[in-request retries](https://github.com/apache/iggy/pull/3574)** and an **[opt-in CSV output format](https://github.com/apache/iggy/pull/3575)**
+- **[Meilisearch sink](https://github.com/apache/iggy/pull/3497)**
+- **[SurrealDB sink](https://github.com/apache/iggy/pull/3453)**
+- **[Redshift sink](https://github.com/apache/iggy/pull/3654)**
+- **[RabbitMQ sink](https://github.com/apache/iggy/pull/3973)**
+- **[HTTP source webhook gateway](https://github.com/apache/iggy/pull/3798)**: Iggy can now receive webhooks directly. One listener serves many providers, each routed to its own topic, with bearer or HMAC authentication, secret URL endpoints, and a token-guarded admin API to register, re-key and revoke endpoints at runtime. Delivery is best-effort, and the connector's README says so up front.
+- **[InfluxDB v2 and v3 connectors](https://github.com/apache/iggy/pull/3140)** rebuilt as separate source and sink crates
+
+### Runtime and SDK
+
+- **[Avro payload support](https://github.com/apache/iggy/pull/3141)** with separate encoder, decoder and transform crates
+- **[`unwrap_envelope` transform and envelope detection](https://github.com/apache/iggy/pull/3197)**, fixing source-to-sink format mismatches
+- **[Source batch acknowledgments](https://github.com/apache/iggy/pull/3855)**: a source's checkpoint is saved only after Iggy accepted the batch. **[Source checkpoints are deferred and sink failures surfaced](https://github.com/apache/iggy/pull/4153)** instead of swallowed, and the **[PostgreSQL source defers progress until ack](https://github.com/apache/iggy/pull/3957)**.
+- **[Source state stored on an HTTP state server](https://github.com/apache/iggy/pull/3940)** as an alternative to a local file
+- **[Agent docs, per-batch observability and atomic state](https://github.com/apache/iggy/pull/3321)**
+- **[Shared `retry_async` helper](https://github.com/apache/iggy/pull/4104)**, replacing seven hand-rolled retry loops that all waited twice the configured delay before the first retry (**BREAKING** for the Rust connector SDK: `ConnectivityConfig` became `RetryPolicy`)
+- **[Per-connector init failures isolated](https://github.com/apache/iggy/pull/3244)**, **[duplicate `iggy_sink_open` and `iggy_source_open` rejected](https://github.com/apache/iggy/pull/3179)**, **[connector key validated before it becomes a path](https://github.com/apache/iggy/pull/4083)**, **[warning when the runtime API is exposed without a key](https://github.com/apache/iggy/pull/3804)**
+- **[Sink writes and startup readiness validated](https://github.com/apache/iggy/pull/4154)**, **[failures reported and format conversion corrected](https://github.com/apache/iggy/pull/4152)**
+- **[Distinct error variants instead of an overloaded `InvalidRecord`](https://github.com/apache/iggy/pull/3194)**, **[FlatBuffer conversions no longer fail silently](https://github.com/apache/iggy/pull/3431)**, **[protobuf field lengths validated consistently](https://github.com/apache/iggy/pull/4069)**
+- Fixed **[PostgreSQL CDC producing no messages](https://github.com/apache/iggy/pull/3640)**, **[Iceberg sink partitions on iceberg 0.10](https://github.com/apache/iggy/pull/3969)**, **[InfluxDB v3 cursor rollback](https://github.com/apache/iggy/pull/3434)**, **[doubled `_total` on counter metrics](https://github.com/apache/iggy/pull/3921)**
+- **[Flink sink uses the TCP client](https://github.com/apache/iggy/pull/3657)**
+- **[CPU and cgroup limits respected in connector stats](https://github.com/apache/iggy/pull/3647)**
+
+Learn more in the [connectors documentation](/docs/connectors/introduction).
+
+---
+
+## Kafka gateway
+
+One of the questions we hear most often is some form of "can I point my Kafka clients at Iggy?". Our answer has two parts. Iggy is not built on Kafka and will not reimplement the Kafka protocol inside the server: the Iggy wire protocol, storage engine and replication are their own design, and that is where the performance comes from. Interoperability instead lives in a **separate gateway process** that speaks the Kafka wire protocol on one side and Iggy's protocol on the other, so existing Kafka producers and consumers can reach Iggy streams without code changes and without touching the server's own wire surface.
+
+0.9.0 ships the first layer of that gateway. **[The Kafka wire protocol gateway](https://github.com/apache/iggy/pull/3519)** is a new `gateways/kafka` workspace crate: a TCP listener on the Kafka port that decodes requests, enforces a version firewall, and answers `ApiVersions`, `Metadata`, `Produce`, `Fetch`, `ListOffsets` and `CreateTopics`, backed by 184 regression tests over golden wire fixtures. The community-driven rollout plan is public in the **[Kafka to Iggy bridge discussion](https://github.com/apache/iggy/discussions/3253)** and tracked in the **[protocol parity issue](https://github.com/apache/iggy/issues/3560)**: the bridge from `Produce` and `Fetch` into Iggy streams comes first, then consumer groups and offset management, then the admin and authentication APIs. Transactions will answer with an unsupported error until Iggy itself has transactional writes.
+
+To be clear about the current state: **in 0.9.0 this is the foundation layer, not a working bridge yet.** No API persists or reads Iggy data. `Produce`, `Fetch` and `ListOffsets` return a retriable error so Kafka clients keep their data and retry, and `CreateTopics` does not create topics. The work has not stopped at the tag, though. The **[bridge core](https://github.com/apache/iggy/pull/4043)**, an Iggy SDK client with stream and topic mapping, provisioning, high watermarks and an Iggy-to-Kafka error map, merged the day after the 0.9.0 code was cut, and pull requests for **[mapping Kafka records to Iggy messages](https://github.com/apache/iggy/pull/4229)** and **[SASL/PLAIN authentication](https://github.com/apache/iggy/pull/4222)** are in review. Wiring `Produce` and `Fetch` through the bridge is next, the work is happening in the open, and contributions are very welcome.
+
+---
+
+## Benchmark Dashboard
+
+- **[Redesigned dashboard with a landing page and compare mode](https://github.com/apache/iggy/pull/3145)**: the [benchmarking platform](https://benchmarks.iggy.apache.org) now opens on a showcase pick and compares any two runs side by side
+- **[Unified benchmarks list and best-pick selection](https://github.com/apache/iggy/pull/3146)**, **[mobile layout and compare URLs](https://github.com/apache/iggy/pull/3148)** fixed
+- **[Cluster topology recorded in reports and surfaced in the UI](https://github.com/apache/iggy/pull/3737)**, so a result says whether it came from one node or three
+- `iggy-bench` **[exposes both durability policies](https://github.com/apache/iggy/pull/4092)** as `--durability` and `--consumer-offset-durability` flags
+- Bench dashboard bumped to 0.8.0
+
+---
+
+## Web UI and MCP
+
+- Web UI bumped to **0.4.0** and **[served directly by the server](https://github.com/apache/iggy/pull/3676)**
+- **[Explicit CSS import for the latest Vite build](https://github.com/apache/iggy/pull/3502)**, frontend dependencies updated
+- MCP server bumped to **0.5.0**, **[runs against the replicated server](https://github.com/apache/iggy/pull/3617)** and supports the **[systemd watchdog](https://github.com/apache/iggy/pull/3233)**
+
+---
+
+## Helm Charts
+
+- **[Cluster deployment with one release per node](https://github.com/apache/iggy/pull/4035)**: `server.cluster` mirrors the server's `[[cluster.nodes]]` roster one for one and renders into `IGGY_CLUSTER_*` variables plus `--replica-id`. Each node is its own release overriding only `selfReplicaId`, with a three-node example to start from. Roster mistakes fail at render time. `server.replicaCount > 1` and autoscaling are now refused, because three independent servers behind one Service sharing a PVC was never a cluster.
+- Fixed QUIC published as TCP, a missing WebSocket port, Kubernetes service links leaking `IGGY_*` variables into the server config, and the default image moved off 0.7.0
+- **[Gateway API and Envoy Gateway replace ingress-nginx](https://github.com/apache/iggy/pull/3386)** in the smoke cluster
+
+---
+
+## CI/CD & Infrastructure
+
+- **[Incubating references removed after TLP graduation](https://github.com/apache/iggy/pull/3928)**
+- **[SECURITY.md with the ASF reporting process](https://github.com/apache/iggy/pull/3787)**, **[package tokens replaced with OIDC](https://github.com/apache/iggy/pull/3892)**, **[read-only permissions declared on reusable workflows](https://github.com/apache/iggy/pull/3243)**
+- **[Miri undefined behavior detector](https://github.com/apache/iggy/pull/3284)** for `binary_protocol` and `consensus`, **[build fails on rustdoc and rustc warnings](https://github.com/apache/iggy/pull/3925)**
+- **[Integration tests run when a binary they launch changes](https://github.com/apache/iggy/pull/4131)**, **[a plugin's integration suite runs when it changes](https://github.com/apache/iggy/pull/4077)**, **[Docker `:edge` refresh gated by the cargo-rail DAG](https://github.com/apache/iggy/pull/3369)**
+- **[HawkEye replaces addlicense](https://github.com/apache/iggy/pull/3325)** for license header checks, later **[migrated to v7](https://github.com/apache/iggy/pull/3952)**
+- PR triage automation: **[review state labels via slash commands](https://github.com/apache/iggy/pull/3231)**, **[`/pin` and `/unpin`](https://github.com/apache/iggy/pull/4060)**, **[welcome comment](https://github.com/apache/iggy/pull/3261)**, **[auto-assign volunteering issue authors](https://github.com/apache/iggy/pull/3400)**, **[fork-PR commands via `workflow_run` handoff](https://github.com/apache/iggy/pull/3268)**, and a stale bot that **[leaves PRs waiting on review alone](https://github.com/apache/iggy/pull/4172)**
+- **[AI assistance expectations added to CONTRIBUTING](https://github.com/apache/iggy/pull/4081)** and a **[repo-wide team-review agent skill](https://github.com/apache/iggy/pull/4004)**
+- **[Devcontainer for reproducible development](https://github.com/apache/iggy/pull/3656)**
+- Most triage and status jobs moved to **[ARM runners](https://github.com/apache/iggy/pull/3270)**, runner disk cleanup **[shared across PHP, Rust, BDD and coverage](https://github.com/apache/iggy/pull/3330)**, **[apt-get bounded so a dead mirror cannot wedge a job](https://github.com/apache/iggy/pull/3924)**
+- BDD runners **[fail on undefined steps](https://github.com/apache/iggy/pull/3680)**, **[stream CRUD coverage](https://github.com/apache/iggy/pull/3875)** added, **[integration suites duplicating existing coverage removed](https://github.com/apache/iggy/pull/3823)**
+- **[Source release tarball includes gateways](https://github.com/apache/iggy/pull/4126)**, **[DEPENDENCIES.md dropped per ASF policy](https://github.com/apache/iggy/pull/3222)**
+
+---
+
+## What's Next
+
+Clustering is in. The next releases are about making it faster and complete:
+
+- **Performance**: the effort behind the benchmark tables above continues. Deferred polling so consumers stop paying for empty round trips, a task per read and write for more I/O concurrency, more replica links per node pair, explicit group commit, Direct I/O instead of `fsync`, and more copy-free paths of the kind 0.9.0 started. Further out, multi-leader clusters with a leader per partition spread across all nodes. A full 0.8.0 vs 0.9.0 vs next comparison is planned for the benchmarking platform.
+- **Kafka gateway**: the bridge from the protocol listener into Iggy streams, so Kafka producers and consumers can talk to Iggy through the gateway. Consumer groups follow, as laid out in the [rollout discussion](https://github.com/apache/iggy/discussions/3253).
+- **SDK parity**: leader redirection coverage for Python, Node.js and C++, and the PHP SDK on its way to a published package
+- **Connectors**: more sources, bounded backpressure end to end, and runtime hardening
+- **Agentic AI**: building on the A2A support and the MCP server
+
+---
+
+Thanks to our amazing community and contributors for making Apache Iggy better with every release. Sixty people contributed to 0.9.0, and 34 of them landed their first Iggy pull request in this cycle. Welcome aboard.
+
+**Join us on [Discord](https://discord.gg/apache-iggy) and help shape the future of the project!**
diff --git a/scripts/diagram-text.mjs b/scripts/diagram-text.mjs
index 1c45bb8e6c..e0069ea647 100644
--- a/scripts/diagram-text.mjs
+++ b/scripts/diagram-text.mjs
@@ -30,6 +30,9 @@
const SITE = "https://iggy.apache.org";
export const DIAGRAM_TEXT = {
+ Bench090DurabilityChart: `**Chart: Replicated vs. Persisted.** Side-by-side panels compare the 0.9.0 release and the next-version development snapshot. Each panel shows Replicated and Persisted producer latency on the same linear scale starting at zero. Controls select single node or 3-node cluster and p50, p99 or p999. All four measurements use 20 pinned producers with an aggregate rate limit of 800 MB/s. Replicated waits for quorum commit and local application. Persisted also requires recoverable stable-storage copies at the quorum. The full measurements appear in the expandable tables below.`,
+ Bench090LatencyChart: `**Chart: latency, 0.9.0 versus a development snapshot, per iggy-bench workload.** Three headline tiles (3-node cluster, persisted, 20 producers at p999 and p99, and single node, 20 producers at p999), then an interactive chart limited to workloads with lower p50, p99 and p999 latency, with two dots per row on a log scale, one for the 0.9.0 release and one for the development branch that follows it. It switches between single node and 3-node cluster, between p50, p99 and p999, and between table order and biggest percentage reduction. Each version uses its own iggy-bench client. The expandable “Full benchmark results” section below contains all measurements, including regressions and achieved throughput.`,
+
AppendOnlyLogViz: `**Diagram: Append-only log.** Messages are appended in order, each with the next offset (0, 1, 2 and so on). New messages are always written at the end. A consumer tracks its own position in the log with an offset, independently of other consumers.`,
BenchmarkChart: `**Diagram: Latency improvements, Tokio vs thread-per-core.** Selected historical results comparing v0.5.0 (Tokio) with v0.7.0 (thread-per-core) at about 1,000 MB/s per node. Lower latency is better.
diff --git a/scripts/generate-docs-markdown.mjs b/scripts/generate-docs-markdown.mjs
index e7d0d04278..dbd836188a 100644
--- a/scripts/generate-docs-markdown.mjs
+++ b/scripts/generate-docs-markdown.mjs
@@ -86,7 +86,7 @@ function transformBody(source, body) {
return line;
}
if (inFence) return line;
- const component = /^\s*<([A-Z][A-Za-z]*)\s*\/>\s*$/.exec(line);
+ const component = /^\s*<([A-Z][A-Za-z0-9]*)\s*\/>\s*$/.exec(line);
if (component) {
const text = DIAGRAM_TEXT[component[1]];
if (!text) {
diff --git a/src/app/global.css b/src/app/global.css
index 76a9c6db90..e0d961348c 100644
--- a/src/app/global.css
+++ b/src/app/global.css
@@ -129,6 +129,7 @@ figure.shiki {
border-radius: 0.375rem;
padding: 0.15em 0.4em;
font-size: 0.875em;
+ overflow-wrap: anywhere;
}
.fd-prose a {
diff --git a/src/components/bench-0-9-0.tsx b/src/components/bench-0-9-0.tsx
new file mode 100644
index 0000000000..344bc94d3c
--- /dev/null
+++ b/src/components/bench-0-9-0.tsx
@@ -0,0 +1,461 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+"use client";
+
+import { useId, useState } from "react";
+
+/**
+ * Latency comparison for the 0.9.0 release post: the 0.9.0 release against
+ * the development branch that follows it, per iggy-bench workload. Three
+ * headline tiles on top, then selected improving workloads with two dots on a log
+ * axis joined by a line. The tables in the post carry the same numbers, so
+ * the chart never gates a value.
+ */
+
+type Pair = [before: number, after: number];
+
+type Row = {
+ workload: string;
+ mbps: Pair;
+ p50: Pair;
+ p99: Pair;
+ p999: Pair;
+};
+
+type Percentile = "p50" | "p99" | "p999";
+type SetupKey = "single" | "cluster";
+type Order = "table" | "change";
+
+type Setup = {
+ key: SetupKey;
+ label: string;
+ rows: Row[];
+};
+
+const SERIES = {
+ before: { label: "0.9.0", color: "#8b93a1" },
+ after: { label: "Next version", color: "#fb4800" },
+} as const;
+
+const PERCENTILES: { key: Percentile; label: string }[] = [
+ { key: "p50", label: "p50" },
+ { key: "p99", label: "p99" },
+ { key: "p999", label: "p999" },
+];
+
+const ORDERS: { key: Order; label: string }[] = [
+ { key: "table", label: "Table order" },
+ { key: "change", label: "Biggest change" },
+];
+
+const SETUPS: Setup[] = [
+ {
+ key: "single",
+ label: "Single node",
+ rows: [
+ { workload: "pinned producer, 20 actors, 800 MB/s", mbps: [800.1, 800.1], p50: [0.65, 0.588], p99: [1.313, 1.145], p999: [11.087, 2.956] },
+ { workload: "pinned producer, 20 actors, persisted, 800 MB/s", mbps: [800.0, 800.1], p50: [1.232, 1.098], p99: [1.601, 1.424], p999: [2.249, 1.967] },
+ { workload: "pinned consumer, 20 actors, cold, 800 MB/s", mbps: [800.1, 800.1], p50: [0.498, 0.447], p99: [4.698, 3.313], p999: [5.156, 3.777] },
+ { workload: "pinned consumer, 20 actors, warm, 800 MB/s", mbps: [800.1, 800.1], p50: [0.521, 0.544], p99: [1.022, 0.923], p999: [1.573, 3.92] },
+ { workload: "pinned producer, 1 actor, 500 MB/s", mbps: [500, 500], p50: [0.347, 0.352], p99: [0.679, 0.683], p999: [1.356, 1.046] },
+ { workload: "pinned consumer, 1 actor, warm, 500 MB/s", mbps: [500, 500], p50: [0.272, 0.296], p99: [0.468, 0.478], p999: [0.491, 0.502] },
+ { workload: "pinned producer, 1 actor, persisted, 500 MB/s", mbps: [372.1, 335.8], p50: [0.665, 0.733], p99: [0.825, 0.901], p999: [0.995, 1.151] },
+ ],
+ },
+ {
+ key: "cluster",
+ label: "3-node cluster",
+ rows: [
+ { workload: "pinned producer, 20 actors, persisted, 800 MB/s", mbps: [800.0, 800.0], p50: [2.905, 2.243], p99: [7.073, 2.938], p999: [12.403, 3.889] },
+ { workload: "pinned producer, 1 actor, persisted, 500 MB/s", mbps: [234.4, 233.6], p50: [1.047, 1.062], p99: [1.2, 1.223], p999: [10.857, 1.759] },
+ { workload: "pinned consumer, 20 actors, cold, 800 MB/s", mbps: [800.1, 800.0], p50: [0.477, 0.436], p99: [3.831, 2.346], p999: [4.241, 2.675] },
+ { workload: "pinned consumer, 20 actors, warm, 800 MB/s", mbps: [800.1, 797.9], p50: [0.484, 0.451], p99: [0.933, 0.812], p999: [1.233, 1.216] },
+ { workload: "pinned producer, 20 actors, 800 MB/s", mbps: [800.0, 800.0], p50: [1.782, 1.679], p99: [3.186, 3.25], p999: [3.683, 3.66] },
+ { workload: "pinned producer, 1 actor, 500 MB/s", mbps: [337.2, 325.4], p50: [0.716, 0.735], p99: [0.973, 1.016], p999: [1.057, 1.13] },
+ { workload: "pinned consumer, 1 actor, warm, 500 MB/s", mbps: [500.0, 500.3], p50: [0.297, 0.326], p99: [0.493, 0.536], p999: [0.533, 0.574] },
+ ],
+ },
+];
+
+/** The three results the post leads with. Clicking a tile opens that row in the chart. */
+const HIGHLIGHTS: { setup: SetupKey; workload: string; percentile: Percentile; label: string }[] = [
+ { setup: "cluster", workload: "pinned producer, 20 actors, persisted, 800 MB/s", percentile: "p999", label: "3-node cluster, persisted, 20 producers, p999" },
+ { setup: "cluster", workload: "pinned producer, 20 actors, persisted, 800 MB/s", percentile: "p99", label: "3-node cluster, persisted, 20 producers, p99" },
+ { setup: "single", workload: "pinned producer, 20 actors, 800 MB/s", percentile: "p999", label: "Single node, 20 producers, p999" },
+];
+
+/** Candidate tick positions for the log axis, in milliseconds. */
+const TICKS = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100];
+
+function axisTicks(values: number[]): number[] {
+ const min = Math.min(...values);
+ const max = Math.max(...values);
+ let lo = 0;
+ let hi = TICKS.length - 1;
+ for (let i = 0; i < TICKS.length; i++) {
+ if (TICKS[i] <= min) lo = i;
+ }
+ for (let i = TICKS.length - 1; i >= 0; i--) {
+ if (TICKS[i] >= max) hi = i;
+ }
+ return TICKS.slice(lo, hi + 1);
+}
+
+function logPosition(value: number, lo: number, hi: number): number {
+ const a = Math.log10(lo);
+ const b = Math.log10(hi);
+ return ((Math.log10(value) - a) / (b - a)) * 100;
+}
+
+// Keep CSS coordinates stable across server and browser floating-point math.
+function positionPercent(value: number): string {
+ return `${Number(value.toFixed(3))}%`;
+}
+
+function formatMs(value: number): string {
+ return value.toLocaleString("en-US", { maximumFractionDigits: 3 });
+}
+
+function formatMbps(value: number): string {
+ return value.toLocaleString("en-US", { maximumFractionDigits: 1 });
+}
+
+function deltaPercent([before, after]: Pair): number {
+ return ((after - before) / before) * 100;
+}
+
+function formatDelta(pct: number): string {
+ const rounded = Math.round(pct);
+ if (rounded === 0) return "0%";
+ return `${rounded > 0 ? "+" : "-"}${Math.abs(rounded)}%`;
+}
+
+function deltaClass(pct: number): string {
+ const rounded = Math.round(pct);
+ if (rounded < 0) return "text-emerald-700 dark:text-emerald-400";
+ if (rounded > 0) return "text-red-700 dark:text-red-400";
+ return "text-fd-muted-foreground";
+}
+
+function findSetup(key: SetupKey): Setup {
+ return SETUPS.find((s) => s.key === key) ?? SETUPS[0];
+}
+
+function Segmented