Writestop › demo
See it work
Run one read-only query on your own Postgres, paste the output, and see the days left before each ID sequence and transaction ID counter reaches its limit; nothing you paste is stored.
Input
2 ID columns · 1 database
Show the raw query output
4 rows of the Writestop query's output: 2 ID columns fed by a sequence, and transaction and multixact ID age for database shop.
kind | name | type | sequence | used | max | per_day | vacuum_at | since
------+------------------+---------+----------------------+------------+---------------------+----------+-----------+----------------------
id | public.events.id | bigint | public.events_id_seq | 8412559031 | 9223372036854775807 | 41208113 | | 2026-10-06 04:12 UTC
id | public.orders.id | integer | public.orders_id_seq | 2104481029 | 2147483647 | 2312040 | | 2026-10-06 04:12 UTC
mxid | shop | | | 1048576 | 2144483647 | 4096 | 400000000 | 2026-10-06 04:12 UTC
xid | shop | | | 498213557 | 2144483647 | 14024704 | 200000000 | 2026-10-06 04:12 UTC
(4 rows)
Output
3 limits checked: 1 act now, 1 plan, 1 fine.
Act now public.orders.id 18 days left
What to do
Migrate the column to bigint: check its lock risk first.
For a serial column, also run
ALTER SEQUENCE public.orders_id_seq AS bigint;Changing a serial column's type leaves its sequence at the integer limit.Plan shop: transaction IDs 117 days left
What to do
The age is above
autovacuum_freeze_max_age(200,000,000), so autovacuum should already be freezing this database: something is holding it back.Check long-running transactions (
backend_xmininpg_stat_activity), replication slots (xminandcatalog_xmininpg_replication_slots) and prepared transactions (pg_prepared_xacts). Then runVACUUM (FREEZE)on the tables with the oldestage(relfrozenxid).Docs: wraparound, VACUUM.
Fine public.events.id over 100 years left
Rates are estimated from activity since 2026-10-06 04:12 UTC. Act now: under 30 days left. Plan: 30 to 180. Fine: over 180.
What Writestop did
Writestop: Read every sequence and ID column's type limit and current value
By hand: know which columns are integer and what their limits are
Writestop: Estimated each insert rate and projected the day each limit is reached
By hand: a query shows today's number, not the day you run out
Writestop: Checked transaction ID age against the wraparound limit
By hand: remember to run the check every week
Want this watching your database? The first 100 get $9/database/mo. Nothing is charged when you join.
Hold a founding spot
The first 100 on the waitlist get $9/database/mo, locked for as long as you stay subscribed. Then it's $19.
We store your email, a source tag, your consent and the signup time in Cloudflare D1, to email you about Writestop. Privacy
Try it on your own database
Connect to the database you want checked, with psql or any SQL client. The query reads the catalog and statistics, never table rows.
1. Run this query
Show the query to run
-- Writestop: how close each ID sequence and transaction ID counter is to its limit.
-- Read-only. Run it in each database you want checked; it never reads table rows.
WITH fed AS ( -- sequences and the columns they feed: serial, identity, nextval() defaults
SELECT objid AS seq, refobjid AS tbl, refobjsubid AS col FROM pg_depend
WHERE classid = 'pg_class'::regclass AND refclassid = 'pg_class'::regclass
AND deptype IN ('a', 'i') AND refobjsubid > 0
UNION
SELECT d.refobjid, ad.adrelid, ad.adnum
FROM pg_depend d JOIN pg_attrdef ad ON ad.oid = d.objid
WHERE d.classid = 'pg_attrdef'::regclass AND d.refclassid = 'pg_class'::regclass
), ids AS (
SELECT f.seq, f.tbl, sn.nspname AS seq_schema, q.relname AS seq_name,
format('%I.%I.%I', tn.nspname, t.relname, a.attname) AS name, a.atttypid
FROM fed f
JOIN pg_class q ON q.oid = f.seq AND q.relkind = 'S'
JOIN pg_namespace sn ON sn.oid = q.relnamespace
JOIN pg_class t ON t.oid = f.tbl AND NOT t.relispartition
JOIN pg_namespace tn ON tn.oid = t.relnamespace
JOIN pg_attribute a ON a.attrelid = f.tbl AND a.attnum = f.col
WHERE a.atttypid IN ('int2'::regtype, 'int4'::regtype, 'int8'::regtype)
), win AS (
SELECT coalesce(stats_reset, pg_postmaster_start_time()) AS since
FROM pg_stat_database WHERE datname = current_database()
), slru AS ( -- commit-log pages started: block_size * 4 XIDs, or block_size / 4 MXIDs, each
SELECT CASE WHEN name IN ('Xact', 'transaction') THEN 'xid' ELSE 'mxid' END AS kind,
blks_zeroed * CASE WHEN name IN ('Xact', 'transaction') THEN 4.0 ELSE 0.25 END
* current_setting('block_size')::int AS ids,
coalesce(stats_reset, pg_postmaster_start_time()) AS since
FROM pg_stat_slru
WHERE name IN ('Xact', 'transaction', 'MultiXactOffset', 'multixact_offset')
)
SELECT 'id' AS kind, i.name, format_type(i.atttypid, NULL) AS type,
format('%I.%I', i.seq_schema, i.seq_name) AS sequence,
s.last_value AS used,
least(s.max_value, CASE i.atttypid WHEN 'int2'::regtype THEN 32767
WHEN 'int4'::regtype THEN 2147483647 ELSE 9223372036854775807 END) AS max,
round((SELECT coalesce(sum(st.n_tup_ins), 0) FROM pg_stat_all_tables st
WHERE st.relid IN (SELECT coalesce(p.relid, i2.tbl) FROM ids i2
LEFT JOIN LATERAL pg_partition_tree(i2.tbl) p ON true WHERE i2.seq = i.seq))
* 86400 / greatest(extract(epoch FROM now() - w.since), 1))::bigint AS per_day,
NULL::bigint AS vacuum_at,
to_char(w.since AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI "UTC"') AS since
FROM ids i
JOIN pg_sequences s ON s.schemaname = i.seq_schema AND s.sequencename = i.seq_name
CROSS JOIN win w
WHERE s.increment_by > 0
UNION ALL
-- Postgres stops assigning XIDs and MXIDs at an age of 2^31 - 1 - 3,000,000.
SELECT z.kind, d.datname, NULL, NULL,
CASE z.kind WHEN 'xid' THEN age(d.datfrozenxid) ELSE mxid_age(d.datminmxid) END,
2144483647,
round(z.ids * 86400 / greatest(extract(epoch FROM now() - z.since), 1))::bigint,
current_setting(CASE z.kind WHEN 'xid' THEN 'autovacuum_freeze_max_age'
ELSE 'autovacuum_multixact_freeze_max_age' END)::bigint,
to_char(z.since AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI "UTC"')
FROM pg_database d CROSS JOIN slru z
WHERE d.datname = current_database()
ORDER BY 1, 2;2. Paste the output
psql's default output, psql --csv or tab-separated, up to 20,000 characters.
Limits. Rates come from one snapshot of Postgres statistics, so they are estimates. Nothing is sent to Slack.