AI Features and extensions pgvector
David Sterling  

pg_partman 5.5 + pg_cron: Automate Partition Maintenance

Partitioned tables in Postgres are a bargain with a catch: you get cheap drops and instant pruning, and in exchange something has to keep creating tomorrow’s child tables and retiring last year’s. That “something” is pg_partman, and the question every operator eventually asks is whether to let its background worker do the job or schedule the maintenance yourself with pg_cron.

Both extensions shipped changes worth knowing about. pg_partman 5.5.0 changed a default that can silently stop partition maintenance on an upgraded cluster — if you run partman and have not read the changelog, start there. pg_cron 1.6.8 (September 8, 2026) added PostgreSQL 19 support, a new cron.dom_dow_and_logic setting, and fixes for a shutdown hang under synchronous replication and a shared-memory queue handle leak in the launcher — see the v1.6.8 release notes. Together they are the entire maintenance loop for a time-series schema.

Background worker or pg_cron? Pick deliberately

pg_partman’s background worker (BGW) is, per its own reference documentation, “basically just a scheduler that runs the run_maintenance_proc() procedure for you.” If you control postgresql.conf, that is the fewest moving parts.

Reach for pg_cron instead when the BGW is not an option — you cannot add pg_partman_bgw to shared_preload_libraries on your platform, or you are on a managed service that permits the extension but not the library preload. Also when you want a different cadence per partition set (a high-write telemetry set every five minutes, a monthly archive set nightly, sequenced with the maintenance_order column), when you want queryable history instead of log lines, or when maintenance must run in more than one database.

The 5.5.0 change that stops maintenance quietly

In 5.5.0 the default for the pg_partman_bgw.role GUC changed to an arbitrary role name, partman_maintainer, as a mitigation for running the worker as a superuser. If your configuration never set that parameter explicitly, the worker no longer runs successfully: it throws errors until a role with that name exists with suitable privileges. Nothing crashes loudly. Writes simply start landing in the default partition while child tables stop appearing.

The fix is the documented non-superuser setup, worth adopting even if you set the GUC yourself, since it is also the mitigation for the batch of privilege-escalation CVEs fixed in this release (CVE-2026-61781, CVE-2026-61817 through CVE-2026-61821):

# postgresql.conf
shared_preload_libraries = 'pg_cron,pg_partman_bgw'   # requires restart
cron.timezone           = 'America/Chicago'
pg_partman_bgw.interval = 600
pg_partman_bgw.role     = 'partman_maintainer'   # the new 5.5 default value
pg_partman_bgw.dbname   = 'mydb'                 # a reload is enough for these

-- run as a superuser, once per cluster
CREATE ROLE partman_maintainer WITH LOGIN;
GRANT ALL ON SCHEMA partman TO partman_maintainer;
GRANT ALL ON ALL TABLES IN SCHEMA partman TO partman_maintainer;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA partman TO partman_maintainer;
GRANT EXECUTE ON ALL PROCEDURES IN SCHEMA partman TO partman_maintainer;
GRANT ALL ON SCHEMA telemetry TO partman_maintainer;
GRANT TEMPORARY ON DATABASE mydb TO partman_maintainer;

-- the maintainer needs rights on every partition set it manages
GRANT telemetry_owner TO partman_maintainer;

Two more 5.5 rules to check before your next maintenance window. If you use retention_schema to move expired child tables into a cold schema instead of dropping them, that schema must now be owned by the same role that owns the child table (CVE-2026-61821). And if several roles manage their own partition sets in one database, 5.5.0 added a maintenance_role column to part_config so you can put a row-level security policy on the configuration tables — the README’s RLS example is the template.

Create the partition set

The parent must already be declared PARTITION BY RANGE; partman only manages the children. The creation function was renamed from create_parent() to create_partition() in 5.4.0 — the old name still works, but new scripts should use the new one.

CREATE TABLE telemetry.well_readings (
  well_id      bigint      NOT NULL,
  recorded_at  timestamptz NOT NULL,
  pressure_psi numeric,
  flow_bpd     numeric
) PARTITION BY RANGE (recorded_at);

CREATE INDEX ON telemetry.well_readings (well_id, recorded_at DESC);

SELECT partman.create_partition(
    p_parent_table    := 'telemetry.well_readings',
    p_control         := 'recorded_at',
    p_interval        := '1 day',
    p_premake         := 7,            -- always a week ahead
    p_start_partition := '2026-09-01'
);

-- retention: keep 13 months, actually drop the detached tables
UPDATE partman.part_config
SET retention            = '13 months',
    retention_keep_table = false,
    retention_keep_index = false,
    -- set this when another table has a foreign key into the partition set
    detach_before_drop   = true
WHERE parent_table = 'telemetry.well_readings';

detach_before_drop is new in 5.5.0 because a partition that a foreign key points at must be detached before it can be dropped. If retention keeps failing on a set another table references, that column is why.

Schedule maintenance with pg_cron

With pg_cron loaded, the background worker can be replaced by one job. run_maintenance_proc() is a procedure, so it is called with CALL, and each set is only extended when it falls behind its premake value — running it often is cheap and safe.

-- keep every managed partition set current (runs as the user who scheduled it)
SELECT cron.schedule(
  'partman-maintenance', '*/10 * * * *',
  'CALL partman.run_maintenance_proc()'
);

-- nightly: sweep stray rows out of the default partitions, in commit batches
SELECT cron.schedule(
  'partman-fix-default', '15 2 * * *',
  $$CALL partman.partition_data_proc('telemetry.well_readings', p_loop_count := 50)$$
);

-- housekeeping for a second database
SELECT cron.schedule_in_database(
  'partman-maintenance-analytics', '5,25,45 * * * *',
  'CALL partman.run_maintenance_proc()', 'analytics'
);

Two operational details. A job runs as whoever created it, so schedule maintenance as partman_maintainer (or a role with equivalent grants) rather than assuming the cron worker’s identity carries privileges — and note that 1.6.8 tightened role ownership checks to be case-sensitive, which matters if you created roles with mixed-case names. And if you have ever needed “the first Sunday of the month,” the new cron.dom_dow_and_logic setting switches pg_cron from Vixie cron’s OR combination of day-of-month and day-of-week to AND, so 0 5 1-7 * 0 finally means what you meant.

Monitor the loop, not just the cron

A green cron job does not prove partitions were created. pg_cron records every run; partman records when each set last completed.

-- jobs that did not succeed in the last day
SELECT jobid, status, return_message, start_time, end_time
FROM cron.job_run_details
WHERE status <> 'succeeded' AND start_time > now() - interval '1 day'
ORDER BY start_time DESC;

-- partition sets that have not completed maintenance recently
SELECT parent_table, partition_interval, premake, retention, maintenance_last_run
FROM partman.part_config
WHERE maintenance_last_run IS NULL
   OR maintenance_last_run < now() - interval '1 hour';

-- anything sitting in a default partition is a problem waiting to grow
SELECT * FROM partman.check_default(p_exact_count := true);

The second query matters more since 5.5.0: a failure in one partition set no longer aborts maintenance for every other set. Instead partman logs a warning and sets that set’s last_run to NULL, which makes maintenance_last_run IS NULL exactly the signal to alert on. check_default() is the other half — rows in a default partition usually mean maintenance fell behind, and a large default partition makes every later maintenance run slower.

A Houston upgrade checklist

For teams running telemetry and event tables here — well and facility sensor reads partitioned daily, drayage and yard-move events off the Ship Channel, appointment audit logs — the path is short:

  1. Read the 5.5.0 breaking-change and CVE notes first.
  2. Install the new binaries, then ALTER EXTENSION pg_partman UPDATE TO '5.5.0';
  3. Create or verify a non-superuser maintenance role, point pg_partman_bgw.role at it, and confirm child tables start appearing again.
  4. Check that every retention_schema is owned by the same role as its child tables.
  5. Set detach_before_drop on sets referenced by a foreign key.
  6. Add the monitoring queries above to whatever already pages you.
  7. Update pg_cron to 1.6.8 — especially if you run a synchronous standby.

Nothing here is exotic. It is the unglamorous half of partitioning: a role with the right grants, a job that runs every ten minutes, and one alert that fires when the child tables stop coming. Sources: the pg_partman changelog, its reference documentation, the pg_cron v1.6.8 release notes, and the PostgreSQL partitioning docs.

Leave A Comment