Contents
On September 10, ClickHouse open-sourced WalShadow, a Postgres-to-ClickHouse replication engine that reads Postgres’s physical WAL instead of using logical replication. In ClickHouse’s own benchmark, a transaction committed in Postgres became visible in ClickHouse in about 200ms and the pipeline sustained 289,000 rows per second, against roughly 10 seconds and 120,000 rows per second for PeerDB, the logical-replication-based CDC tool ClickHouse acquired and already runs in production as ClickPipes.[1] The pitch is that going around logical replication removes its operational overhead: no replication slot, no output plugin, less load on the source.
Four days after launch, a Hacker News commenter pointed out something the blog post doesn’t mention: WalShadow requires wal_level = logical, the exact setting that makes logical replication possible in the first place, not the more permissive wal_level = replica that plain physical replication needs.[2] Sai Srirampur, the ClickHouse engineer who wrote the announcement, confirmed it in the same thread.[2] The project’s own limitations document says so too: “wal_level = logical required” and “every replicated table needs usable replica identity” are both listed as hard preflight requirements, not optional tuning.[3] That matters because wal_level = logical is also the reason logical replication has a WAL-volume reputation problem: it’s what forces Postgres to log a full “before” image for updates and deletes on any table with REPLICA IDENTITY FULL, and to carry more per-row identity information generally. WalShadow reads WAL differently, but it hasn’t escaped that cost. It inherits it.
What physical WAL actually contains
A physical WAL record is a byte-level description of a change to a specific page in a specific relation file: block number, offset, the new bytes. That’s sufficient for a physical replica, because a physical replica’s whole job is to reproduce the same pages, and it already has an identical copy of the catalog to interpret them with. It’s not sufficient for building a row in ClickHouse, because a raw heap tuple has no column names and no types attached to it at the WAL level. You need to know, as of the moment that WAL record was written, what table that relfilenode belonged to, what its columns were, and what their types were. Logical decoding solves this by running inside the source Postgres process, using an output plugin that queries the live system catalog and emits a structured stream of inserts, updates, and deletes. WalShadow can’t do that without becoming logical decoding, so it built something else: a second, real Postgres instance (“shadow”) that receives a filtered copy of the same WAL stream, but with all the user-table records replaced by no-ops. The shadow only replays catalog changes, so it ends up with an accurate, versioned picture of the schema at any WAL position, without ever holding actual row data.[4] A pool of Rust decoder workers then interprets the real heap records against that shadow catalog, in parallel, entirely outside the source database, and a separate pool of inserters writes the results into ClickHouse as native blocks.[1][4]
That architecture is genuinely different from logical replication’s single serial decoding process running inline in the source’s WAL sender. It’s also why the four-stage pipeline in the announcement (track the schema, decode in parallel, batch into ClickHouse blocks, insert in parallel) needs a second Postgres process as a load-bearing component, not an optimization. Rows can also arrive out of order because decoding is parallelized, so every row carries the source LSN as _lsn and ClickHouse keeps the newest version per key on merge; schema changes and truncations get an explicit barrier that waits for all preceding data to land before they apply.[1]
What wal_level = logical is actually paying for
Once you need REPLICA IDENTITY on every table, whether default (primary key), USING INDEX, or FULL, you’re paying the same per-row WAL cost a logical replication subscriber would force on that table, because that identity information exists to let a downstream consumer find the “old” row for an UPDATE or DELETE without depending on where it physically sits.[3] WalShadow needs that for the same reason any CDC consumer does: it’s reconstructing rows keyed by primary key for ClickHouse, not raw page positions. On Hacker News, ClickHouse engineer __s put it plainly: “we require wal_level=logical, would be nice to support looser in future… agreed that there’s a huge complexity cost to going this route."[2] So the “no logical replication overhead” claim in the launch post is true of two specific things and not a third: it’s true that there’s no replication slot, and true that decoding no longer runs as a single-threaded process inside the source. It is not true that the WAL-volume cost of tracking row identity goes away, because that cost is a property of wal_level and REPLICA IDENTITY, not of who does the decoding or where.
The part that is real, and worth the added complexity for the right workload, is what a replication slot costs when a consumer falls behind. A logical slot pins restart_lsn and catalog_xmin on the primary, so a stalled subscriber makes WAL (and vacuum progress on system catalogs) accumulate without bound until the slot is dropped or catches up. WalShadow’s design sidesteps that specific failure mode: the shadow and decoders can fall behind and resume from archived WAL segments as a fallback rather than forcing the primary to retain anything past normal physical-standby retention.[4] That’s the actual overhead being removed, and it’s a real operational win if slot-growth incidents are the thing you’ve been burned by. It’s just a narrower claim than “physical WAL means less WAL work,” and the benchmark numbers, real as they look, come from ClickHouse comparing its new tool against its own older tool, on a single table, on matched c8i.2xlarge instances in one region.[1] That’s a legitimate before/after measurement of ClickHouse’s own architecture change. It is not an independent, adversarial benchmark, and nothing in the post claims it is.
The constraints that come with the design
The project’s limitations document, current as of this week, lists several hard boundaries worth knowing before evaluating it against a real workload: the shadow Postgres instance’s major version must exactly match the source’s; one source database per WalShadow process; sequence values aren’t replicated (only whatever’s already landed in table rows); prepared transactions aren’t validated for production restart and bootstrap paths yet; and unplanned primary promotion isn’t supported at all, because “walshadow consumes PostgreSQL failover decisions, it does not make them."[3] TOAST handling, separately, is still in flux: on Hacker News __s described three competing storage modes, disabled, ClickHouse-backed chunk storage, and shadow-catalog-backed storage, and called TOAST “a pita."[2] The shadow-backed mode landed as a pull request opened September 17 and was still being updated as of hours before this piece went up, which is a reasonable sign of how young this specific subsystem is.[5]
It also only works where you can read physical WAL directly, which most managed Postgres providers don’t expose to customers. Asked on Hacker News whether it works against a Supabase-hosted database, Srirampur said no: WalShadow needs direct physical WAL access, which restricts it to self-hosted Postgres or ClickHouse’s own Managed Postgres offering, where ClickHouse controls both sides of the stack.[2] If your Postgres lives on RDS, Cloud SQL, or Supabase today, this isn’t a tool you can point at it regardless of how the latency numbers look.
The decision this actually changes
If you’re running self-hosted Postgres and need ClickHouse to see changes in under a second, WalShadow’s trade is coherent: you take on a second Postgres process, a project still labeled “Development Preview” with the caveat that “interfaces and behavior may evolve,” and a narrower set of supported failure modes, in exchange for parallel decode and insert throughput and freedom from slot-retention risk.[3] What you should not budget for is a free lunch on WAL volume. If your tables need REPLICA IDENTITY FULL today because your rows are wide and updates are frequent, they’ll need it under WalShadow too, and your source will log the same “before” images either way. The header numbers are about where the decoding work happens and what backpressure looks like when a consumer stalls, not about making logical replication’s underlying WAL cost disappear.
Sources
[1] https://clickhouse.com/blog/introducing-walshadow: Sai Srirampur, “Introducing WalShadow: Sub-second Postgres replication to ClickHouse from physical WAL,” ClickHouse Blog, September 10, 2026
[2] https://news.ycombinator.com/item?id=49660791: Hacker News discussion, “WalShadow: Sub-second Postgres replication to ClickHouse from physical WAL,” comments from saisrirampur and __s (ClickHouse engineers), September 17-18, 2026
[3] https://github.com/ClickHouse/walshadow/blob/main/docs/limitations.md: ClickHouse/walshadow, “Current limits” documentation
[4] https://github.com/ClickHouse/walshadow/blob/main/architecture/README.md: ClickHouse/walshadow, architecture documentation, “Why a shadow” and streaming topology
[5] https://github.com/ClickHouse/walshadow/pull/163: “Add a mode to let toast stay in shadow pg,” pull request opened September 17, 2026