---
title: "The Curious Case of the Table-Locking UPDATE Query"
description: "The post recounts a PostgreSQL performance incident in which metadata processing stopped as jobs accumulated and an UPDATE targeting a single UUID remained active for days. Diagnostics showed the query consuming more than 80% of database time, while manual execution did not reproduce the delay. A pg_stat_activity and pg_locks query exposed many waiting statements and an AccessExclusiveLock entry whose locktype was tuple, leading the investigation toward PostgreSQL row locking and contention. The eventual cause was threaded code that issued concurrent updates to the same row for each job; removing that concurrency, aborting old backends with pg_cancel_backend, and deploying the change restored processing and began churning through the backlog. A correction notes that the reported lock was on a tuple rather than the table, and that PostgreSQL does not have lock promotion as suggested in the debugging discussion."
---

# The Curious Case of the Table-Locking UPDATE Query

[Heroku](https://yomu.fyi/company/heroku) · Richard Schneeman · Dec 19, 2019

**Type:** Incident / postmortem

## Summary

The post recounts a PostgreSQL performance incident in which metadata processing stopped as jobs accumulated and an UPDATE targeting a single UUID remained active for days. Diagnostics showed the query consuming more than 80% of database time, while manual execution did not reproduce the delay. A pg\_stat\_activity and pg\_locks query exposed many waiting statements and an AccessExclusiveLock entry whose locktype was tuple, leading the investigation toward PostgreSQL row locking and contention. The eventual cause was threaded code that issued concurrent updates to the same row for each job; removing that concurrency, aborting old backends with pg\_cancel\_backend, and deploying the change restored processing and began churning through the backlog. A correction notes that the reported lock was on a tuple rather than the table, and that PostgreSQL does not have lock promotion as suggested in the debugging discussion.

## Context

A metadata-processing service developed a PostgreSQL performance problem: jobs stopped being processed, the backlog grew, and an UPDATE on one UUID appeared to remain active for days while many other queries waited.

## Approach / What changed

The investigation used heroku pg:diagnose, heroku pg:outliers, and a pg\_stat\_activity joined with pg\_locks. The team traced the contention to threaded code issuing concurrent updates for the same UUID, removed the concurrency so updates ran sequentially, canceled stuck backends with pg\_cancel\_backend, and deployed the change.

## Takeaways

- The reported UPDATE was shown in diagnostics as running for 5 days 18:01:26.446979, and heroku pg:outliers attributed more than 80% of database time to it.
- Each job created a concurrent promise that could update the same database row; sequential updates reduced the chance of same-row contention, at the cost of less efficient Ruby-side I/O use.
- The correction states that the lock was on a tuple, not the table, and that PostgreSQL does not have lock promotion as suggested in the original debugging discussion.

**Tags:** [Parallelism](https://yomu.fyi/topic/parallelism), [Postgres](https://yomu.fyi/topic/postgres), [Ruby](https://yomu.fyi/topic/ruby)

- Source: [Heroku](https://www.heroku.com/blog/curious-case-table-locking-update-query)
- Source URL: https://www.heroku.com/blog/curious-case-table-locking-update-query
- Ingested by Yomu: 2026-09-09T16:02:45.648Z

[Read original post](https://www.heroku.com/blog/curious-case-table-locking-update-query)
