If Apache Iggy is useful to you, give it a star on GitHubStar apache/iggy

Apache Iggy 0.9.0 Release

Piotr Gankiewicz
Piotr Gankiewicz

Apache Iggy Founder & PMC Member

Hubert Gruszecki
Hubert Gruszecki

Apache Iggy PMC Member

Release 0.9.0

We are proud to announce the release of Apache Iggy 0.9.0. This release marks a historical milestone for the project: it is our very first as an official Apache Top-Level Project following our graduation from the Incubator on August 19, 2026. This isn't just another version increment; it is the flagship release we have been relentlessly building toward over the past year. And we can’t wait for you to try it out.

The official release is the source archive distributed by the Apache Software Foundation, with its PGP signature, SHA-512 checksum and the project KEYS. The downloads page explains how to verify it. The GitHub release carries the full list of merged pull requests.

Iggy now runs as a cluster. 0.9.0 ships Viewstamped Replication Revisited (VSR) consensus in the standard iggy-server binary. Start three nodes, lose one, and the cluster keeps serving reads and writes. There is no separate build, feature flag, or clustered edition. The same server runs single-node or replicated, and cluster.enabled in the configuration decides which one you get.

This is the biggest release in the project's history: over 600 merged pull requests from 60 contributors, 34 of them contributing to Iggy for the first time. Alongside the server, we're releasing the Rust SDK 0.11.0, the CLI 0.14.0, the connectors runtime 0.5.0, the MCP server 0.5.0, the Web UI 0.4.0, the bench dashboard 0.8.0, and the Python 0.9.0, Node.js 0.10.0, Java 0.9.0, C# 0.9.0 and Go 0.9.0 SDKs, all from the same repository. Beyond clustering, 0.9.0 brings per-topic durability policies, an extensible topic options catalog, a new PHP SDK, eight new connectors and an HTTP webhook source, the foundation of a Kafka protocol gateway for existing Kafka clients, a redesigned benchmarks dashboard, cluster-aware Helm charts, and hundreds of fixes and performance improvements across the server, SDKs and connectors.

Before the changelog, two topics deserve their own space: clustering and performance.


Clustering with VSR consensus

For three releases the roadmap section of these posts has ended the same way: complete the clustering implementation with VSR consensus. 0.7.0 laid the consensus foundations. 0.8.0 added the shard crate, the plane abstractions, the persistent WAL journal, and the experimental iggy-server-ng binary. In 0.9.0 that work became the server. server-ng was promoted to the Apache Iggy server, the vsr feature flag is gone, the legacy server and its wire format were deleted, and every SDK and CI lane runs against the replicated server. Single-node deployment remains the default, and clustering is opt-in through the [cluster] section of the configuration.

What a cluster gives you

Apache Iggy replicates with Viewstamped Replication Revisited. VSR keeps an ordered state machine consistent across replicas and elects a new primary when the current one fails:

  • Normal operation: the primary assigns an operation number, sends Prepare, and commits after a quorum answers PrepareOk.
  • View change: when replicas stop hearing from the primary, they exchange StartViewChange and DoViewChange, and the new primary sends StartView. Failover needs no operator.
  • Recovery: a restarted or lagging replica probes the current view and repairs the missing WAL ranges from its peers before it serves current state.

Three replicas tolerate one unavailable replica, five tolerate two. Three nodes is the recommended minimum for a highly available deployment. A two-node cluster is useful for development, but it cannot make progress after either node fails.

Replication is split by namespace rather than run as one global log:

PlaneConsensus groupReplicated state
MetadataOne group on shard 0Streams, topics, users, permissions, consumer groups, access tokens
PartitionOne group per partitionMessages and consumer offsets

Each group runs its own consensus and elects its own primary. Metadata writes route through the metadata primary, and partition writes route through the primary of that partition. Reads are served from the local replicated state on any node, followers included.

How it was built

The consensus work spans well over a hundred pull requests. The milestones, in roughly the order they landed:

Then came the correctness fixes found by the simulator, the crash tests and real cluster runs:

Durability you choose per topic

Replication forced a precise answer to a simple question: what does an acknowledgement mean? 0.9.0 answers it with two create-only topic options, introduced in let topics require durable acks:

PolicyRequired before success
replicated (default)VSR quorum commit and local application, without an additional stable-storage barrier
persistedVSR quorum commit backed by recoverable stable-storage copies at the required quorum

durability governs message production and consumer_offset_durability governs explicit offset stores and deletes. They default independently and neither inherits the other. Both policies write data to disk. With persisted, each multi-replica partition keeps a bounded on-disk prepare WAL, sized by partition.wal_bytes_max (256 MiB by default), and a replica cannot release its PrepareOk until its message bodies, WAL history and durable frontier are recoverable. The old enforce_fsync topic option and the server-wide consumer_offset_enforce_fsync setting are gone. Every SDK, the CLI, the HTTP API and iggy-bench expose both options.

The details are in Durability and Cluster Durability.

Secure replica traffic

The replica-to-replica port is authenticated and, optionally, encrypted:

See Cluster Security.

Client failover

Every SDK speaks the VSR wire protocol and can connect to any node. Leader-aware clients fetch cluster metadata after login, reconnect to the advertised leader when they landed on a follower, poll for a new leader through an election, and cap the number of redirects so a flapping roster cannot bounce them forever:

Writes return after the required VSR commit, or return a retryable error while a replica is changing view or catching up. TransientNotAccepted proves the request was never admitted and permits a retry on another node. TransientNotCommitted has an uncertain outcome and is replayed with the same session and request identity, and the server-side dedup takes care of the rest. Details in Client Failover.

Per-node advertised addresses and per-client-network address selectors let a node publish different endpoints to clients on different networks, which matters for containers and cloud deployments.

Running a cluster

Every node loads the same configuration and is told apart only by --replica-id. Save this configuration as iggy-vsr.toml:

[cluster]
enabled = true
name = "iggy-vsr-dev"

[cluster.auth]
enabled = true
shared_secret = ""

[[cluster.nodes]]
name = "iggy-node-1"
ip = "127.0.0.1"
replica_id = 0
ports = { tcp = 8090, quic = 8080, http = 3000, websocket = 8093, tcp_replica = 9090 }

[[cluster.nodes]]
name = "iggy-node-2"
ip = "127.0.0.1"
replica_id = 1
ports = { tcp = 8091, quic = 8081, http = 3001, websocket = 8094, tcp_replica = 9091 }

[[cluster.nodes]]
name = "iggy-node-3"
ip = "127.0.0.1"
replica_id = 2
ports = { tcp = 8092, quic = 8082, http = 3002, websocket = 8095, tcp_replica = 9092 }

Set the shared configuration path, root credentials and replica authentication secret in each terminal. Use the same credentials and secret for all three nodes. Root credentials are required on the first boot, and the secret must contain at least 32 bytes:

export IGGY_CONFIG_PATH="$PWD/iggy-vsr.toml"
export IGGY_ROOT_USERNAME=iggy
export IGGY_ROOT_PASSWORD="replace-with-your-root-password"
export IGGY_CLUSTER_AUTH_SHARED_SECRET="replace-with-at-least-32-random-bytes"

Start each node in a separate terminal:

IGGY_PATH=local_data/node-0 iggy-server --replica-id 0
IGGY_PATH=local_data/node-1 iggy-server --replica-id 1
IGGY_PATH=local_data/node-2 iggy-server --replica-id 2

Every roster field can also be set through typed IGGY_CLUSTER_* environment variables, which is what the new Helm chart cluster mode renders. The Deploy a Cluster guide walks through the whole setup, and the Configuration reference covers every [cluster] key, including consensus timing (heartbeat_timeout, view change and repair intervals, all on a fixed 10ms tick).

Testing a consensus protocol

We trust this implementation because of how much effort went into breaking it. Most of that effort is deterministic simulation:

The Viewstamped Replication page is the entry point to all of this.


Performance

Efficiency has always been the point of Iggy: thread-per-core, shared-nothing, io_uring, and no copies where the format allows it. 0.9.0 carried that into the replicated server and shipped a series of hot-path improvements:

Benchmarks: 0.9.0 and what comes next

We're not done. A dedicated performance effort started right after the 0.9.0 code freeze, and the first results are in. The tables below compare the 0.9.0 release against a development snapshot, both measured with iggy-bench on the same host with identical workload parameters. Each version ran its own build of iggy-bench, because the development client sends the new deferred poll. These results therefore compare the server and client together. Each cell reads 0.9.0 → next, where next is the development snapshot. Latencies are in milliseconds. p50 is the median, p99 the 99th percentile, and p999 the 99.9th percentile.

Durability legend

  • Replicated (durability=replicated, the default for producer rows without a persisted label): a successful write waits for quorum commit and local application, but does not wait for stable storage. Acknowledged data can still be in memory.
  • Persisted (durability=persisted): a successful write also requires recoverable copies on stable storage at the replication quorum. On a three-node cluster, that means at least two replicas.

Both policies write data to disk. On a single node, the quorum is one: replicated provides no extra copy, while persisted waits for local stable storage. These labels describe producer acknowledgements. Consumer rows measure reads. See Durability for the full guarantees.

Workload legend

The comparisons below cover rate-limited workloads.

  • Pinned producer / consumer: each actor owns its own stream and partition. Actors counts concurrent producers or consumers.
  • 800 MB/s / 500 MB/s: the configured aggregate rate limit. Achieved MB/s is the measured throughput, which can be lower. Compare latency alongside achieved throughput.
  • Cold / warm: consumers read uncached data or re-read cached data, respectively.

Replicated vs. Persisted

20 pinned producers · 800 MB/s aggregate rate limit

Compare both durability policies within each version. Next version is a development snapshot using its own iggy-bench client.

0.9.0

Replicated1.782 ms

Achieved: 800 MB/s

Persisted2.905 ms

Achieved: 800 MB/s

Next version

Replicated1.679 ms

Achieved: 800 MB/s

Persisted2.243 ms

Achieved: 800 MB/s

3-node cluster, p50 latency. Both panels use the same linear scale from zero. Lower is better.

Replicated: quorum commit and local application. Persisted: also requires recoverable copies on stable storage at the quorum. On a single node, the quorum is one.

Latency improvements: 0.9.0 → next version

All shown workloads use an aggregate rate limit of 800 MB/s.

Selected workloads with lower p50, p99 and p999 latency. Next version is a development snapshot, using its own iggy-bench client. Milliseconds on a log scale, lower is better. Expand “Full benchmark results” below for all measurements, including regressions and achieved throughput.

  • 0.9.0
  • Next version
3-node cluster, p999
1251020
change

Change is the p999 latency on the development branch relative to 0.9.0. Biggest change sorts by the largest percentage reduction. Click a row to keep its values open. Snapshot of work in progress, not a final result.

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/sp50 msp99 msp999 ms
pinned producer, 20 actors, 800 MB/s800.1 → 800.10.650 → 0.5881.313 → 1.14511.087 → 2.956
pinned producer, 20 actors, persisted, 800 MB/s800.0 → 800.11.232 → 1.0981.601 → 1.4242.249 → 1.967
pinned consumer, 20 actors, cold, 800 MB/s800.1 → 800.10.498 → 0.4474.698 → 3.3135.156 → 3.777
pinned consumer, 20 actors, warm, 800 MB/s800.1 → 800.10.521 → 0.5441.022 → 0.9231.573 → 3.920
pinned producer, 1 actor, 500 MB/s500.0 → 500.00.347 → 0.3520.679 → 0.6831.356 → 1.046
pinned consumer, 1 actor, warm, 500 MB/s500.0 → 500.00.272 → 0.2960.468 → 0.4780.491 → 0.502
pinned producer, 1 actor, persisted, 500 MB/s372.1 → 335.80.665 → 0.7330.825 → 0.9010.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/sp50 msp99 msp999 ms
pinned producer, 20 actors, persisted, 800 MB/s800.0 → 800.02.905 → 2.2437.073 → 2.93812.403 → 3.889
pinned producer, 1 actor, persisted, 500 MB/s234.4 → 233.61.047 → 1.0621.200 → 1.22310.857 → 1.759
pinned consumer, 20 actors, cold, 800 MB/s800.1 → 800.00.477 → 0.4363.831 → 2.3464.241 → 2.675
pinned consumer, 20 actors, warm, 800 MB/s800.1 → 797.90.484 → 0.4510.933 → 0.8121.233 → 1.216
pinned producer, 20 actors, 800 MB/s800.0 → 800.01.782 → 1.6793.186 → 3.2503.683 → 3.660
pinned producer, 1 actor, 500 MB/s337.2 → 325.40.716 → 0.7350.973 → 1.0161.057 → 1.130
pinned consumer, 1 actor, warm, 500 MB/s500.0 → 500.30.297 → 0.3260.493 → 0.5360.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) 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.

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.

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) 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 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, together with the final numbers for the next release. The benchmarking guide has the commands to reproduce these workloads on your own hardware, and the Linux tuning guide covers the host preparation.


Iggy Server

One server, one wire protocol

Topic options

  • Extensible key-value options for topics, streams and users (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.
  • Durability policies 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

HTTP, security and operations

Breaking changes

This is a major upgrade. Read this list before moving an existing deployment:

The release is available as a signed source archive from the Apache Software Foundation. Docker images are available on Docker Hub: 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

PHP (new)

A brand new SDK. The PHP SDK 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, ships a consumer message iterator and examples, and runs a BDD suite with TLS tests in CI. 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.

Python

Python received more pull requests than any other SDK in this release and closed most of its API gap with the Rust client:

Java

C#

Go

Node.js (TypeScript)

C++

The low-level bindings from 0.8.0 grew into a client:

CLI


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

Runtime and SDK

Learn more in the connectors documentation.


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 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 and tracked in the protocol parity issue: 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, 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 and SASL/PLAIN authentication 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


Web UI and MCP


Helm Charts

  • Cluster deployment with one release per node: 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 in the smoke cluster

CI/CD & Infrastructure


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.
  • 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 and help shape the future of the project!