This walkthrough shows what Hydra ETL does on a real PostgreSQL 16 to protect the source of CDC mirror, in four scenes:
- A normal mirror.
- The mirror breaks and the WAL piles up: Hydra ETL warns, and a workflow sends an alert.
-
max_slot_wal_keep_sizeprotects the disk and PostgreSQL invalidates the slot: Hydra ETL stops instead of going on with a gap. - Rebuilding the mirror.
Every screenshot below comes from this exact sequence, run end to end.
0. Prerequisites
- Docker
- Python 3.10+
- The Hydra ETL repository: https://github.com/bejaouibechir/Hydra
Start PostgreSQL with logical replication:
docker run --name hydra-pg-cdc -e POSTGRES_PASSWORD=hydra -p 5433:5432 -d postgres:16 -c wal_level=logical Go to the example, create its .env, and create the two databases: shop (the application) and analytics (the mirror):
git clone https://github.com/bejaouibechir/hydra-tuto-cdc-slot-protection.git cd examples/postgres-cdc-mirror printf 'PG_HOST=127.0.0.1\nPG_PORT=5433\nPG_USER=postgres\nPG_PASSWORD=hydra\n' > .env docker exec -i hydra-pg-cdc psql -U postgres < db/initdb.sql docker exec -i hydra-pg-cdc psql -U postgres < db/seed.sql On Windows PowerShell,
<does not redirect: useGet-Content db/initdb.sql | docker exec -i hydra-pg-cdc psql -U postgres, and the same for everydb/*.sqlscript below.
Get-Content db/initdb.sql | docker exec -i hydra-pg-cdc psql -U postgres Get-Content db/seed.sql | docker exec -i hydra-pg-cdc psql -U postgres Start Studio, then open the folder examples/postgres-cdc-mirror (Open a project):
hdrctl serve --open - A normal mirror
The job
Open the sync workflow, then the job orders_mirror. It has two nodes:
- the source
order_changes, of type PostgreSQL CDC: tablepublic.orders, slothydra_orders, publicationhydra_orders; - the destination
mirror: PostgreSQL, tableanalytics.orders, modeupsertonorder_id, atomic load.
Double-click the source to see its settings:
Lower the warning threshold for the demo
By default, Hydra ETL warns when the slot keeps more than 1 GB of WAL. For the demo we lower it to 5 MB. Add one line under connection in jobs/orders_mirror/sources.yaml:
slot: hydra_orders publication: hydra_orders lag_warning_mb: 5 # demo: warn from 5 MB of WAL kept The node form does not show this key. Studio reloads the file and keeps the key when it saves the job. Hydra DSL shows
First run
Go to the Workflow configuration tab and clic Run Workflow. On this first run, Hydra ETL creates the publication and the slot, then copies the whole table.

Check both tables. Studio has a terminal (the >_ button of the canvas, tab TERMINAL):
docker exec -i hydra-pg-cdc psql -U postgres < db/check.sql Get-Content db/check.sql | docker exec -i hydra-pg-cdc psql -U postgres - The mirror breaks, the WAL piles up
Break the mirror
The mirror refuses negative amounts. The shop updates 500 orders and records one order at -15.00:
docker exec -i hydra-pg-cdc psql -U postgres < db/break.sql Get-Content db/break.sql | docker exec -i hydra-pg-cdc psql -U postgres Run the project. The run fails on the negative amount, the mirror does not change, and the slot does not move: the 500 updates will be read again at the next run.
The source keeps working
Meanwhile, the application keeps writing. db/wal.sql writes about 30 MB of WAL into a table that is not mirrored, then shows the slot:
docker exec -i hydra-pg-cdc psql -U postgres < db/wal.sql Get-Content db/wal.sql | docker exec -i hydra-pg-cdc psql -U postgres What Hydra ETL does before each run
Run the project again. Before reading anything, the CDC source reads the state of its slot in pg_replication_slots. The slot keeps more than lag_warning_mb, and the run says so:
A warning in a run log is not an alert, though: nobody reads logs on a Saturday. The next step sends it somewhere.
2 bis. Send an alert with a fallback
A fallback replaces a workflow when a condition is true, checked just before the workflow starts. Two conditions read the slot:
| Condition | Returns |
|---|---|
cdc.slot_lag('order_changes') | WAL kept by the slot, compared with 5MB, 2GB… |
cdc.slot_status('order_changes') | reserved, extended, unreserved, lost or missing |
1) The alert URL as a secret
To try it, take a free URL on https://webhook.site: it shows every request it receives. Any chat webhook (Slack, Teams…) works the same way.
In Project settings › Secrets, add ALERT_WEBHOOK_URL:
2) The replacement workflow
Create workflows/sync_with_alert.yaml. It sends the alert, then runs the same job:
version: "1.0" workflow: name: sync_with_alert steps: - name: alert type: action action: webhook params: url: ${SECRET:ALERT_WEBHOOK_URL} body: text: "CDC slot of order_changes is falling behind: the mirror has not loaded for a while." on_failure: continue # an alert that fails must not block the load - name: orders type: job job: ../jobs/orders_mirror depends_on: [alert] Studio picks it up:

The replacement runs the job too: the alert does not stop the mirror from catching up once it is repaired.
3) The fallback on sync
In Project settings › Execution order, click the fallback button of sync:
- replace with:
sync_with_alert; - when: build the condition with the assistant (insert a function, its source, the comparison, the value);
- if a check cannot answer: keep
sync.
lostis tested first: on an invalidated slotcdc.slot_lagcannot answer, andorstops at the first true condition. The savedworkflows/thread.yaml`:
`yaml
thread:
- mode: sequential workflows:
- name: sync fallback: use: [sync_with_alert] when: "cdc.slot_status('order_changes') == 'lost' or cdc.slot_status('order_changes') == 'unreserved' or cdc.slot_lag('order_changes') > 5MB" `
- name: sync fallback: use: [sync_with_alert] when: "cdc.slot_status('order_changes') == 'lost' or cdc.slot_status('order_changes') == 'unreserved' or cdc.slot_lag('order_changes') > 5MB" `
4) Run
Run the project. The run says which workflow ran, and why: the value of each condition.
The alert was received (here by a local test receiver; webhook.site shows the same body):
3. Protect the disk: max_slot_wal_keep_size
The alert tells someone. If nobody fixes the mirror over the weekend, the disk still fills up. PostgreSQL 13+ can cap the WAL a slot holds:
sql
ALTER SYSTEM SET max_slot_wal_keep_size = '20GB';
SELECT pg_reload_conf();
This is a trade-off. Past the limit, PostgreSQL invalidates the slot (wal_status = lost): the WAL is removed, and the changes made since the last success can no longer be read. The disk is safe, but the change stream now has a gap.
For the demo, db/limit.sql sets the limit to 1 MB, and the source writes some more:
bash
docker exec -i hydra-pg-cdc psql -U postgres < db/limit.sql
powershell
Get-Content db/limit.sql | docker exec -i hydra-pg-cdc psql -U postgres
What Hydra ETL does
Run the project:
- the fallback switches on
cdc.slot_status('order_changes') = lost, and the alert is sent; - the job fails on purpose, before reading anything, and says how to rebuild the mirror.
Resuming after the gap would have made a mirror that looks fine and is wrong. Hydra ETL refuses to do that.
4. Rebuild the mirror
Fix the wrong order at the source, then follow the three steps of the message. db/rebuild.sql drops the slot, empties the mirror and gives the server back its default limit:
bash
docker exec -i hydra-pg-cdc psql -U postgres < db/repair.sql
docker exec -i hydra-pg-cdc psql -U postgres < db/rebuild.sql
powershell
Get-Content db/repair.sql | docker exec -i hydra-pg-cdc psql -U postgres
Get-Content db/rebuild.sql | docker exec -i hydra-pg-cdc psql -U postgres
Run the project. The slot is gone, so cdc.slot_status is missing and the fallback is not needed. sync creates a new slot and copies the table again.
bash
docker exec -i hydra-pg-cdc psql -U postgres < db/check.sql
powershell
Get-Content db/check.sql | docker exec -i hydra-pg-cdc psql -U postgres
Summary
| State of the slot | Before each run, the job | The cdc.* fallback conditions |
|---|---|---|
WAL kept above lag_warning_mb | writes a warning, goes on | cdc.slot_lag(…) > 2GB → alert |
wal_status = unreserved | writes a warning, goes on | cdc.slot_status(…) == 'unreserved' → alert |
wal_status = lost | fails, with the steps to rebuild | cdc.slot_status(…) == 'lost' → alert |
To watch the slot from your own monitoring:
sql
SELECT slot_name, active, wal_status, safe_wal_size,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
Clean up:
bash
docker rm -f hydra-pg-cdc


















