September 13, 2026

A Decade-Long PostgreSQL Technical Debt Finally Paid: Unpacking the New max_active_replication_origins Parameter in PostgreSQL 18

a-decade-long-postgresql-technical-debt-finally-paid-unpacking-the-new-max_active_replication_origins-parameter-in-postgresql-18

a-decade-long-postgresql-technical-debt-finally-paid-unpacking-the-new-max_active_replication_origins-parameter-in-postgresql-18

DATABASE ADMINISTRATION — After nearly ten years of borrowing configuration space, PostgreSQL has finally codified a dedicated parameter to control replication origins. In PostgreSQL 18, database administrators are introduced to max_active_replication_origins, a long-overdue architectural fix that resolves a decade of parameter overlap, silent worker failures, and potentially catastrophic standby crashes.

While the change itself is functionally straightforward—allocating a fixed shared memory array for tracking replication states—its operational implications are profound. Database administrators upgrading to PostgreSQL 18 must understand how this parameter behaves, how it interacts with physical standbys, and why misconfiguring it can lead to abrupt cluster-wide panics.


Main Facts: What is max_active_replication_origins?

To understand why max_active_replication_origins matters, one must first look at how PostgreSQL handles logical replication tracking. A replication origin is an internal identifier assigned to a specific source of data changes, recorded as a two-byte ID inside the pg_replication_origin system catalog.

Every logical subscription gets its own persistent origin (e.g., pg_<subscription oid>). Furthermore, every active table synchronization worker spins up its own temporary origin for the duration of its initial data copy.

When a subscriber’s apply worker commits a remote transaction locally, the local commit record embeds both the origin ID and the remote commit Log Sequence Number (LSN). This clever bookkeeping mechanism ensures that the subscriber remains entirely crash-safe under asynchronous commit configurations. During a post-crash recovery, the engine can accurately rebuild the tracking state—answering the crucial question of "how far did I get from each data source?"—without forcing an expensive fsync operation for every single remote transaction.

Historically, from PostgreSQL 9.5 through PostgreSQL 17, the configuration of how many active origins a subscriber could track was tethered to max_replication_slots. However, max_replication_slots is a publisher-centric parameter whose true domain is managing output plugins on the sending server. This parameter overlap created documentation headaches, configuration ambiguities, and an administrative blind spot that persisted for years.

In PostgreSQL 18, max_active_replication_origins officially takes over this responsibility on the subscriber side.

  • Default Value: 10
  • Allowed Range: 0 to 262,143
  • Context: postmaster (Requires a full server restart to change)
  • Memory Footprint: Each entry consumes a negligible 56 bytes of shared memory.

Chronology: A Ten-Year Wait for a Dedicated Parameter

The genesis of max_active_replication_origins is a classic tale of open-source technical debt resolution.

  • 2014–2016 (PostgreSQL 9.5): Logical replication features mature, introducing replication origins. Due to time constraints or architectural shortcuts, developers repurpose the max_replication_slots parameter to bound the active origin tracking array, leaving a source-code comment marked /* XXX? */.
  • The PostgreSQL 14 Pivot: PostgreSQL 14 introduces parallel table synchronization, allowing initial copies to commit across multiple transactions. This drastically increases the dynamic demand for temporary replication origins, putting unprecedented pressure on the shared memory array originally sized by max_replication_slots.
  • 2021: The first formal proposal to decouple the subscriber’s origin tracking limit into a separate, dedicated parameter stalls and goes nowhere. The community continues to patch over the limitation.
  • PostgreSQL 16: Documentation efforts attempt to clarify the confusion, explicitly noting that max_replication_slots "also applies on a sending server, but with a different meaning" when referenced in subscriber contexts.
  • September 2025: Edge cases involving physical standbys crashing due to mismatched origin tracking limits are formally reported on the pgsql-bugs mailing list, underscoring the urgent need for a clean break from the legacy implementation.
  • PostgreSQL 18 Release: The decade-old XXX code comment is finally paid off. max_active_replication_origins is introduced as a first-class citizen in the PostgreSQL configuration ecosystem.

Supporting Data and Operational Mechanics

Under the hood, the active replication origins are managed via a shared memory array containing 56-byte entries. This array is periodically checkpointed to disk at pg_logical/replorigin_checkpoint and reloaded into memory upon server startup.

Crucially, max_active_replication_origins does not cap the number of rows in the pg_replication_origin system catalog. The catalog can hold up to 65,534 origins (the absolute mathematical limit of a 16-bit origin ID), regardless of your parameter settings. Instead, the parameter strictly controls the active runtime array represented in the pg_replication_origin_status view.

The Danger of Quiet Failures

One of the most insidious aspects of misconfiguring this parameter is how gracefully—or rather, deceptively—it fails during normal DDL operations:

All Your GUCs in a Row: max_active_replication_origins
  1. The Silent DDL Trap: When an administrator runs CREATE SUBSCRIPTION, PostgreSQL writes the necessary row to the system catalog and returns a successful status. It does not check the active replication origins array at this moment. It is only when the background apply worker attempts to spin up, tries to claim an entry, and fails that trouble begins.
  2. The Infinite Retry Loop: If no slots are available, the server logs an error: could not find free replication state slot for replication origin with ID X, accompanied by a hint to raise the parameter. The internal launcher responds by respawning the dead apply worker every five seconds (governed by wal_retrieve_retry_interval), trapping the subscription in an unyielding error loop. Because the initial CREATE SUBSCRIPTION command succeeded, administrators are left scratching their heads as to why data isn’t flowing.
  3. Table Synchronization Hazards: Setting the limit too low relative to your concurrent workloads creates subtler bugs. If a cluster has a strict limit (e.g., set to 2) and two active subscriptions are created, both apply workers claim an entry. However, if a subscription attempts to run a table synchronization worker, that worker requires an independent origin. Lacking an available slot, the table synchronization process loops indefinitely, leaving the affected table permanently stuck in sync state d within pg_subscription_rel.

Startup Panics and Zero Values

Administrators must also be wary of heavy-handed reductions:

  • If max_active_replication_origins is set lower than the number of entries already recorded in the checkpoint file, the database engine refuses to start, throwing a PANIC: could not find free replication state, increase "max_active_replication_origins" and aborting the startup sequence entirely.
  • If set strictly to zero, the logical replication launcher exits immediately upon startup, completely disabling logical replication across the entire node.

Official Responses and Standby Considerations

The introduction of PostgreSQL 18 brings specific challenges regarding physical standbys that mirror a subscriber.

Physical standby servers that replicate a primary subscriber must also replay these origin tracking records into their own local arrays, sized by their respective max_active_replication_origins values.

Unlike core configuration parameters such as max_connections, max_prepared_transactions, or max_wal_senders—which are safely hardcoded into the database control file and enforced strictly at startup—max_active_replication_origins is not currently safeguarded by control-file validation.

The Standby Crash Vector

If a physical standby is configured with a lower max_active_replication_origins value than the primary, the standby will boot up cleanly, achieve replication consistency, and happily serve read queries. However, the moment the recovery process replays a WAL record for an origin ID exceeding its local array capacity, the standby crashes catastrophically:

FATAL: could not find free replication state slot for replication origin with ID 5
CONTEXT: WAL redo ... for ReplicationOrigin/SET

The startup process immediately exits, taking the standby offline. If the base backup already contains more checkpointed origins than the standby’s configuration allows, the standby will trigger a immediate startup PANIC.

Operational Warning: Because this behavior mirrors how max_replication_slots behaved on subscribers in PostgreSQL versions 14 through 17, database architects must exercise extreme discipline. Always update standby configurations before pushing changes to the primary cluster, and lower values on the primary before scaling them down on standbys.


Implications and Best Practices for Administrators

With pg_upgrade from PostgreSQL 17 to 18 automatically checking that the new cluster’s max_active_replication_origins matches or exceeds the legacy subscription count, administrators must perform a thorough inventory during migration planning.

Given that each tracking entry costs a microscopic 56 bytes of memory, there is virtually no performance or memory penalty for over-provisioning.

Recommended Saturated Sizing Formula

Database experts recommend using the following heuristic when sizing the parameter on dedicated subscriber nodes:
$$textTarget = (textExpected Subscriptions + textmax_logical_replication_workers) times 2$$

On a dedicated subscriber node, setting max_active_replication_origins = 100 consumes a trivial six kilobytes of shared memory. This small operational buffer ensures that your infrastructure will never experience an unannounced outage, crash panic, or stalled synchronization worker the moment a new data stream or business-critical subscription is added to the topology.