Spock 6: The Only Logical Choice
Spock 6 has landed in Beta. pgEdge's multi-master replication extension now runs on PostgreSQL 16, 17, 18, and 19, tracks replication progress in shared memory instead of a catalog table, spills oversized replay queues to disk, and reports conflict statistics per subscription. Spock 5 was already doing the hard work of multi-master replication in production. Spock 6 makes the same engine faster, easier to watch, and considerably harder to kill.
We are currently finalizing internal testing, as well as ensuring Spock 6 support is fully available in the Control Plane, HELM, and Cloud, so any feedback as we march towards GA is greatly appreciated.
Rearchitected Internals
Spock 6 ships a custom WAL resource manager (RMGR), and moves progress tracking from a catalog table into shared memory.
In a multi-master cluster, every subscriber tracks which transactions it has received and applied from each peer. Previous versions stored this progress in the spock.progress catalog table, so every replicated transaction generated an additional catalog write. Under light workloads that's fine. Under heavy replication traffic across a busy multi-node cluster, those writes create contention and the progress table accumulates bloat that needs periodic maintenance.
Spock 6 tears the catalog write out of the hot path. Progress state now lives in shared memory during normal operation and is snapshotted to $PGDATA/spock/resource.dat on clean shutdown — nothing extra is written per transaction. Durability comes from PostgreSQL's replication origin tracking, which already records the last commit applied from each peer: after a crash, Spock reconciles its state against the origins, the same machinery PostgreSQL's own logical replication relies on. The custom resource manager complements this by writing a snapshot of the progress state into the WAL at shutdown and during node operations, so replication state is inspectable with standard tools like pg_waldump, in the same durable log PostgreSQL trusts for its own recovery. The old spock.progress table is gone, replaced by a view over a C set-returning function that reads from shared memory, so existing queries and tooling that reference it keep working without changes. Subscribers see less I/O contention, progress bloat is gone along with the table that held it, and crash recovery rebuilds accurate replication state from data PostgreSQL already keeps durable.

Exception handling is Spock's mechanism for dealing with transactions that can't be applied cleanly (constraint violations, unresolvable conflicts, missing tables, schema mismatches). While Spock decides what to do with one of these transactions, the replay queue holds its operations in memory. In Spock 5.x, the spock.exception_replay_queue_size GUC capped how much memory the queue could use, and a transaction that blew past the limit would re-fetch from the remote.
Spock 6 keeps the same GUC (now specified in MB, default 4) but adds spill-to-disk support: once the queue hits its memory limit, subsequent entries spill to a temporary file and get read back on demand. The handover from memory to disk happens mid-transaction, without operator involvement. Multi-gigabyte transactions in exception handling mode now replay fully without memory pressure or needing to re-fetch.
Surviving Catastrophic Node Loss
Multi-master replication has an ugly failure mode that most people don't think about until it hits them. Consider a three-node cluster where node A is replicating to nodes B and C. Node A goes down hard, mid-stream, while it's sending a batch of transactions. Node B might have received all of them. Node C might have received only half. Neither B nor C knows the other has different data, and there's no obvious error. That's silent drift, and it's one of the hardest problems in distributed systems because by the time you notice it, the divergence has been compounded by subsequent writes.

Spock 6 addresses this through the combination of WAL-logged progress tracking and ACE (Active Consistency Engine). Every subscriber now maintains precise, crash-safe records of three values per peer: what the peer committed (remote_commit_lsn), what the peer reported sending (remote_insert_lsn), and what the subscriber actually received (received_lsn). Because these values are logged to WAL through the custom resource manager, they survive crashes and restarts without losing accuracy. That's the detection half of the problem: after a failure, you can determine exactly where each surviving node was in the replication stream.
The repair half uses ACE's table-diff and table-repair commands. After a node fails, you run table-diff across the surviving nodes to identify exactly which rows diverged. Then table-repair copies the missing rows from the node that has them to the node that doesn't, using the --preserve-origin flag to maintain the original replication origin ID and commit timestamps. Without that flag, the repaired rows would look like new local writes and trigger false conflicts when the cluster resumes normal operation.
Conflict Intelligence
Spock has always handled conflicts well across multi-master topologies. Spock 6 adds sharper tools for seeing what those conflicts are and where they come from.
Conflict classification has been overhauled to align with PostgreSQL 18's native conflict type naming. PG18 introduced built-in conflict detection for logical replication and defined a standard set of conflict types using an <operation>_<data_state> naming convention. Spock 6 adopts that same convention:
INSERT_EXISTS (an INSERT hits a row that already exists),
UPDATE_ORIGIN_DIFFERS (an UPDATE targets a row last modified by a different node)
UPDATE_MISSING (an UPDATE can't find its target row)
DELETE_ORIGIN_DIFFERS, and DELETE_MISSING. The old Spock-specific names like CONFLICT_UPDATE_DELETE are gone, replaced by names that match what PostgreSQL itself uses.
Spock also defines DELETE_EXISTS (a remote DELETE that's older than the local row, so the local row wins), which is unique to multi-master topologies and doesn't exist in PG18's enum.
Origin change tracking has been refined so that same-origin updates aren't incorrectly flagged as conflicts, and origin changes on deletes are now logged but not written to spock.resolutions since they're informational rather than genuine data conflicts.
Per-subscription conflict statistics are new in Spock 6 for clusters running PostgreSQL 18 or later. In previous versions, understanding conflict patterns meant parsing log files or querying the spock.resolutions table after the fact. Spock 6 registers a custom statistics kind through PG18's pgstat infrastructure and tracks conflict counts per subscription, broken down by type: INSERT_EXISTS, UPDATE_MISSING, DELETE_EXISTS, DELETE_MISSING, and others. Two new SQL functions, spock.get_subscription_stats() and spock.reset_subscription_stats(), give DBAs direct access to these counters. You can see which subscriptions generate conflicts, what types they are, and whether a configuration change reduced them.
Resolutions retention is a new operational convenience for keeping the spock.resolutions table manageable over time. In busy clusters, this table grows indefinitely and can become a maintenance burden. Spock 6 adds the spock.resolutions_retention_days GUC (default 100 days) for automatic cleanup, and a spock.cleanup_resolutions(days) function for manual cleanup when you want more control. A new index on the log_time column makes the cleanup efficient even on large tables.
Observability
Apply change logging is a new JSON-based audit trail for every change the apply worker processes. Controlled by the spock.apply_change_logging GUC, it has three modes:
none (default, no overhead),
key_only (logs the action, table, primary key, origin, and commit timestamp for each DML change),
verbose (adds old and new row data).
DDL statements are logged in both modes. It covers compliance auditing, replication debugging, and checking what changes flowed through the cluster, without touching your application.
Lag tracking has been rebuilt around three distinct LSN values per subscription. The spock.lag_tracker view now exposes
commit_lsn (what the peer committed),
remote_insert_lsn (what the peer reported sending),
received_lsn (what the subscriber received). T
The old last_received_lsn column has been replaced with these more granular values, making it easier to distinguish between "the peer hasn't sent anything" and "we haven't received what was sent." Monitoring queries that reference the old column names will need updating.

Operational Reliability
Rolling upgrades between Spock 5.x and 6.0 are now supported. In a multi-master cluster, you can't take all nodes down at once to swap in a new version of the replication extension, and you shouldn't have to. Spock 6 includes multi-protocol negotiation (protocol version 5, with backward compatibility to version 4) that handles mixed-version clusters during the transition window, so you can upgrade nodes one at a time while the cluster stays up and serving traffic. The full cycle works: start on v5, bring up v6 nodes, run INSERT/UPDATE/DELETE/DDL/TRUNCATE across the mixed cluster, and complete the migration without downtime.

ZODAN (Zero Downtime Add Node) improvements address several reliability issues that surfaced in real deployments. ZODAN is Spock's mechanism for adding a new node to a running cluster without stopping writes, and the process involves coordinating slot creation, progress capture, and data synchronization across the existing nodes. Apply workers are now paused during slot creation to prevent a race condition that could lose resume_lsn data (this is a sub-second operation). A new timeout_sec parameter lets operators control how long add_node() waits before giving up, useful when network latency varies. The operation is now restricted to uninitialized nodes only, preventing accidental re-initialization. Spock also adds pause timeout recovery logic so a failed pause doesn't leave the cluster in an indeterminate state.
Read-only mode has been refactored to use PostgreSQL's native XactReadOnly mechanism instead of Spock's previous approach of filtering by command type. Read-only mode is used when you want a subscriber to receive replication data but not allow local writes, a common pattern for read replicas in distributed deployments. The new implementation is simpler, more correct, and aligns with how PostgreSQL itself enforces read-only transactions. The READONLY_ALL mode now sends keepalive feedback to the publisher while skipping DML application, so the publisher doesn't think the subscriber has fallen behind. The old READONLY_USER mode has been renamed to READONLY_LOCAL for clarity. A fix also prevents data loss when attempting to resync a read-only subscriber, which was previously allowed and would silently lose data.
Cascade replication (A to B to C topologies) now tracks LSN advancement correctly. In a cascade setup, node A publishes to node B, which in turn publishes to node C. Previous versions used last_received_lsn for progress tracking in these chains, which could stall replication when the intermediate node fell behind. Spock 6 switches to end_lsn to match PostgreSQL's origin tracking semantics, keeping the full chain moving.
sync_event delivery has been reworked for greater control. Sync events are Spock's barrier synchronization mechanism, used during operations like node addition to coordinate state across the cluster. The function may now optionally use a transactional LogLogicalMessage, instead of always bypassing the reorder buffer that resulted in immediate delivery and triggering flushing WAL. On the apply side, replorigin_session_advance() is called explicitly so the progress appears in pg_replication_origin_status right away rather than waiting for the next unrelated COMMIT.
Liveness detection has moved from wal_sender_timeout-based approaches to TCP keepalive with tighter tuning (idle=10s, interval=5s, count=3, giving roughly 25 seconds to detect a dead connection). The old approach generated false positives when subscribers were busy processing large transactions, because the subscriber couldn't send feedback while it was applying a big batch. The removed feedback_frequency GUC has been replaced with spock.apply_idle_timeout (default 300s), and a new connect_timeout=10 on replication connections speeds up detection of unreachable hosts.
Native logical slot failover on PostgreSQL 17 and later. Spock's replication slots are now created with the FAILOVER flag, allowing PG's native slotsync mechanism to handle slot synchronization to standbys. On PG18 and later, Spock's own failover slots worker steps aside entirely in favor of the native implementation. That's one less Spock-specific worker to run and one less place where Spock diverges from core PostgreSQL.
Subscription management gets a new spock.sub_alter_options() function for changing multiple subscription options in a single call. The function validates input, skips no-op restarts when nothing changed, and makes bulk configuration changes less error-prone than altering options one at a time.
Bug Fixes
Crash recovery no longer loses progress. Previously, remote_insert_lsn could vanish after a crash, causing the subscriber to replay transactions it had already applied. We now send a keep-alive on startup and get a reply with the last remote_insert_lsn from the provider.
Use-after-free in the replay queue was fixed: when the spill-to-disk mechanism transitions between in-memory and disk storage, a pointer lifecycle issue could reference freed memory. The fix corrects buffer ownership tracking so the handover is safe.
Memory context handling in exception paths received multiple fixes across the INSERT, UPDATE, DELETE, and DDL apply code paths. Memory contexts are now correctly reset, preventing leaks that accumulated during long-running exception handling.
TRUNCATE in exception modes was incorrectly being executed when the subscription was in TRANSDISCARD or SUB_DISABLE mode. It's now logged and skipped, matching the behavior of other DML operations in those modes.
Generated columns now replicate correctly. Tables with GENERATED ALWAYS AS columns previously caused replication errors across PostgreSQL versions.
Partial unique index handling in conflict resolution now correctly applies predicate filtering, preventing false conflict matches against rows that don't satisfy the index's WHERE clause. Previously, a partial unique index could match against a row that existed in the table but fell outside the index's predicate, triggering a false conflict that blocked the apply worker.
NULLS NOT DISTINCT indexes are now handled correctly on PG16+. Unique indexes created with NULLS NOT DISTINCT treat two NULL values as equal, and Spock's conflict detection now respects that semantic. NULL-keyed rows also skip the proactive unique-index probe entirely, which improves apply performance on tables with nullable unique columns.
Hot standby feedback errors no longer spam the logs when hot_standby_feedback is off on physical standbys. The error messages were harmless but generated noise in log aggregation, making it harder to spot genuine problems.
Stuck sync subscriptions that enter an unrecoverable state are now disabled automatically instead of restarting in a loop. This prevents a failing subscription from consuming resources indefinitely and makes the problem visible in spock.sub_show_status().
DSN password obfuscation masks passwords in connection strings that appear in error messages and log output. Without this, a replication connection failure could dump the full DSN, including the password, into your centralized logging system.
Exception behavior in TRANSDISCARD and SUB_DISABLE modes now logs discarded DMLs to the exception log via exception row capture. Duplicate DDL logging has also been eliminated.
Under the Hood
AutoDDL is refactored into a dedicated module (spock_autoddl.c) with centralized logic, simplified control flow, and transaction-context guards for extension-script subcommands. AutoDDL is Spock's mechanism for automatically capturing and replicating DDL statements across nodes, and the previous implementation had grown complex enough that edge cases around extension operations and transaction boundaries were hard to reason about. The new module also adds replication-set stickiness on ALTER TABLE, so tables stay in their assigned replication sets unless a primary key or replica identity change forces reassignment.
SPI apply path removed. The spock.use_spi GUC and the entire SPI-based apply code path have been removed. All apply operations now use the native heap manipulation path, which is faster and avoids the overhead of constructing and parsing SQL for every replicated row.
SpockCtrl removed. The spockctrl CLI utility for cluster management has been removed from the Spock repository. Cluster management operations are now handled through SQL functions and Control-Plane, HELM. or Ansible.
Snowflake functions relocated. spock.convert_column_to_int8() and spock.convert_sequence_to_snowflake() have been moved to a separate repository, keeping the Spock extension focused on replication.
Excluded schemas can now be kept out of replication sets via a skip_schema parameter, preventing local extension metadata from being copied during node addition. This is useful when certain schemas contain a node-local state that shouldn't be replicated across the cluster.
Build and CI infrastructure grew alongside the release. The pipeline now runs a three-node cluster installcheck workflow and builds multiplatform Docker images for both amd64 and arm64.
Upgrading to Spock 6
Spock 6 provides a direct upgrade path from Spock 5.0.10 via the spock--5.0.10--6.0.0.sql migration script. The following changes may require updates to your configuration or monitoring:
spock.progress is now a view backed by shared memory. Existing SELECT queries work unchanged, but any code that directly writes to this table will need updating to work with the new architecture.
Conflict type names in spock.resolutions have been updated to match PostgreSQL 18's naming convention (update_update becomes update_exists, update_delete becomes update_missing, delete_delete becomes delete_missing). The migration handles the data conversion automatically, but custom monitoring queries that filter on the old names will need updating.
spock.lag_tracker columns have changed: last_received_lsn is now commit_lsn, and new received_lsn and remote_insert_lsn columns have been added. Dashboards and alerting rules that reference the old column name will need adjusting to take advantage of the more granular lag information.
spock.exception_replay_queue_size is now specified in MB (default 4) instead of bytes (old default 4194304). The value is functionally equivalent, but if you had customized this GUC, note the unit change.
Removed GUCs: spock.use_spi, spock.batch_inserts, and spock.feedback_frequency are no longer recognized. Remove them from postgresql.conf to avoid startup warnings.
Get Started
Spock 6 Beta is available now. If you're already running Spock 5.x, the rolling upgrade path lets you migrate nodes one at a time with zero downtime. If you're evaluating distributed Postgres for the first time, grab the code from GitHub, spin up a cluster, throw writes at every node, and watch Spock sort them out.

