IL-0582 Kanmu · 2024-12-08 (published)databaseaws
Jobs piled up in Kanmu's PostgreSQL-based qg job queue on Aurora as dequeue slowed and dead tuples kept growing
Kanmu runs its product database on Amazon Aurora PostgreSQL, a cluster with a writer instance and a reader instance. The same database is a transactional job queue through qg, a Go implementation of Que: jobs are rows in the que_jobs table, enqueued by the api and worker workloads and deleted by workers when finished. Workers dequeue with a recursive CTE that tries pg_try_advisory_lock on candidate jobs.
pg_stat_activity showed an unintended query from the PostgreSQL user created for Redash. Redash was meant to query the reader instance, but when the author had registered the Aurora database as a Redash data source in the past, the author had set the cluster endpoint, which points at the writer, instead of the reader endpoint. The query combined large table scans with CTEs, showed wait_event io:BufFileWrite, and kept running after the Redash client had disconnected, because the user had no query timeout such as statement_timeout. Under READ COMMITTED the query took its snapshot when it started, so que_jobs rows deleted after that were still visible to it and VACUUM would not remove them; the query's backend_xmin, 1226078887, matched the oldest xmin in the VACUUM output. The dequeue query filters only on queue and run_at, so the index scan also reached rows that were already dead and had to fetch each one to check its visibility, which slowed dequeue and raised the database load.
The team changed the Redash data source to the reader endpoint, ended the query's process with pg_terminate_backend and ran VACUUM again by hand, which cleaned up the dead tuples in one go, and dequeue performance returned to normal. Performance Insights had shown the query prominently, but it was missed at the time.
- Severity
- –
- Scope
- Asynchronous jobs in the qg job queue on the product database piled up and were processed slowly
- Users affected
- –
- Region
- –
- Time to detect
- –
- Time to resolve
- –
Symptoms
A Datadog alert fired because a large number of unprocessed jobs had piled up in Kanmu's job queue and were not being worked off. The jobs were the batch enqueued at about the same time every day, in the usual volume, but the workers took far longer than normal to dequeue and process them.
Worker logs showed the job fetch time, lock_time, longer than normal. The Datadog dead tuple metric postgresql.dead_rows showed the dead tuples of the que_jobs table growing steadily from a certain time. The team already knew that growing dead tuples degrade qg performance. autovacuum, which should remove them, did not seem to be working.
A manual VACUUM VERBOSE que_jobs reported "found 0 removable, 768195 nonremovable row versions in 15623 out of 15623 pages" and "679473 dead row versions cannot be removed yet, oldest xmin: 1226078887", so running VACUUM did not reclaim anything.
Root cause
Root cause Operator error
- Trigger
- A heavy ad hoc analysis query from the BI tool Redash ran for a long time on the writer instance, because the Redash data source pointed at the cluster endpoint.
Detection
A Datadog alert on unprocessed jobs piling up in the job queue.
Remediation
- Changed the Redash data source to the reader endpoint
- Ended the long-running Redash query with pg_terminate_backend
- Ran VACUUM on que_jobs by hand again, which cleaned up the dead tuples
Action items
- Make Redash unable to connect to the writer instance even if the writer endpoint is set, by mapping its host name to a TEST-NET-1 address in the Redash container's /etc/hosts
- Set statement_timeout on PostgreSQL users so that long queries stop automatically
- Consider improving qg's dequeue algorithm or adopting a job queue such as river that uses SELECT FOR UPDATE SKIP LOCKED
Lessons
A long-running query on the same database instance, even one unrelated to que_jobs, can keep VACUUM from removing que_jobs dead tuples, so the table keeps growing and the job queue slows down. qg's design with advisory locks and a recursive CTE is sensitive to dead tuples.
Sources
References
Related cases
IL-0577Ficilcom's production Neon PostgreSQL database was reset by prisma migrate reset meant for the local databaseFicilcom · 2026same root cause · shared Database
IL-0576Every dataset in CareNet's production BigQuery data warehouse was deleted, and UNDROP SCHEMA could not restore themCareNet · 2025same root cause · shared Database
IL-0242Both PostgreSQL servers behind matrix.org were lost during a disk expansion, and the homeserver was down for 24 hoursMatrix.org Foundation · 2025same root cause · shared Database
IL-0556A transaction left open during database maintenance stopped uploads and then downloads on Storj's US1 satelliteStorj · 2024same root cause · shared Database
IL-0531Azure Database for MySQL Single Server and MariaDB in West US rejected every new connection for a batch of serversMicrosoft Azure · 2024same root cause · shared Database
IL-0565Cookie Run: Kingdom's CockroachDB cluster failed when 16 of 24 nodes lost their filesystems, causing a 36-hour outageDevsisters · 2022same root cause · shared Database
IL-0207A routine task run from an engineer's local environment switched to production and deleted GitLab.com's Consul release, so Patroni lost Consul and GitLab.com returned 5xx errors to all trafficGitLab · 2022same root cause · shared Database
IL-0332A test run against the production database truncated every table and travis-ci.com was down for about 5.5 hoursTravis CI · 2018same root cause · shared Database
IL-0431During Mackerel's move to AWS, PostgreSQL failed over to a replica that had stopped replicating, losing an hour of dataHatena · 2017same root cause · shared Database
IL-0506UC Berkeley DataHub logins failed and students got empty home directories after the JupyterHub database was overwrittenUC Berkeley · 2017same root cause · shared Database
IL-0218GitLab.com's primary PostgreSQL data was wiped by mistake and, with no working backup, the site was down about 18 hoursGitLab · 2017same root cause · shared Database
IL-0573A developer's mistake dropped nine main tables of the Baemin billing MariaDB database and payments briefly stoppedWoowa Brothers · 2016same root cause · shared Database
IL-0319Dropping databases on the MySQL replica mysql2 also dropped them on mysql1, as the pair actually ran master-masterOregon State University Open Source Lab · 2015same root cause · shared Database
IL-0337EBS volumes in one US East Availability Zone got stuck after a network change cut EBS nodes off from their replicasAmazon Web Services · 2011same root cause · shared Database
Check your cluster for this
For AI tools and scripts
- This case as JSON: https://incidentlib-api.benchlet.com/cases/IL-0582
- The cases most like it: https://incidentlib-api.benchlet.com/cases/IL-0582/similar
- MCP server (Streamable HTTP, no key):
https://incidentlib-mcp.benchlet.com/mcp
The API needs no key. The MCP server offers search_incidents, get_incident, list_incidents, get_topic and vocab; both are explained on API and MCP.