From 5b301e1ffa05bf4ad47ec6eb575e88088b242b42 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 11 Aug 2026 20:09:43 +0700 Subject: [PATCH] Add docs for diagnosing stuck and blocked queries (#48920) ## Context Related to the dashboard work for [Database Connections](https://github.com/orgs/supabase/discussions/48639) - updates the "Connection Management" docs page to include a section about "Diagnosing stuck and blocked queries". Content is intentionally agnostic to the UI, but more focused on Postgres. Preview: https://docs-cdukolvgy-supabase.vercel.app/docs/guides/database/connection-management Covers the following sub-topics: - Reading a session's state - Finding out what's blocking a query - How to stop the session responsible - Small footer to link to the dashboard's Database Connections page Also adding a cross-reference in 2 areas - Troubleshooting: How to check if my queries are being blocked by other queries - Monitoring and Debugging MDX -> Related to observability skills ## Summary by CodeRabbit * **Documentation** * Expanded connection-management guidance with clearer explanations of session states. * Added instructions for diagnosing stuck or blocked queries, identifying blocking sessions and chains, and choosing when to cancel or terminate them. * Documented required permissions and available dashboard tools for managing sessions. * Added cross-references and telemetry updates to make troubleshooting guidance easier to discover. * Clarified how to use PostgreSQL activity information when investigating blocked queries. --- .../guides/database/connection-management.mdx | 94 ++++++++++++++++--- ...-being-blocked-by-other-queries-NSKtR1.mdx | 2 + .../data/content-listings/telemetry.data.ts | 5 + supa-mdx-lint/Rule003Spelling.toml | 1 + 4 files changed, 90 insertions(+), 12 deletions(-) diff --git a/apps/docs/content/guides/database/connection-management.mdx b/apps/docs/content/guides/database/connection-management.mdx index b94395c6338..1d3910dbea4 100644 --- a/apps/docs/content/guides/database/connection-management.mdx +++ b/apps/docs/content/guides/database/connection-management.mdx @@ -81,18 +81,18 @@ ON pg_stat_ssl.pid = pg_stat_activity.pid; Interpreting the query: -| Column | Description | -| ------------------ | --------------------------------------------------- | -| `connection_id` | connection id | -| `ssl` | Indicates if SSL is in use | -| `database` | Name of the connected database (usually `postgres`) | -| `usename` | Role of the connected user | -| `application_name` | Name of the connecting application | -| `client_addr` | IP address of the connecting server | -| `query` | Last query executed by the connection | -| `query_start` | Time when the last query was executed | -| `state` | Querying state: active or idle | -| `backend_start` | Timestamp of the connection's establishment | +| Column | Description | +| ------------------ | ---------------------------------------------------------------------------------------------------------- | +| `connection_id` | connection id | +| `ssl` | Indicates if SSL is in use | +| `database` | Name of the connected database (usually `postgres`) | +| `usename` | Role of the connected user | +| `application_name` | Name of the connecting application | +| `client_addr` | IP address of the connecting server | +| `query` | Last query executed by the connection | +| `query_start` | Time when the last query was executed | +| `state` | Querying state. See [Session states](#session-states) for the full list of values and what each one means. | +| `backend_start` | Timestamp of the connection's establishment | The username can be used to identify the source: @@ -105,3 +105,73 @@ The username can be used to identify the source: | `supabase_replication_admin` | Synchronizes Read Replicas | | `postgres` | Supabase Dashboard and External Tools (e.g., Prisma, SQLAlchemy, PSQL...) | | Custom roles defined by user | External Tools (e.g., Prisma, SQLAlchemy, PSQL...) | + +## Diagnosing stuck and blocked queries + +If your application is slow or hanging, the cause is usually visible right now in `pg_stat_activity` - a query that's stuck, one session blocking others, or a transaction left open by mistake. This section covers how to read a session's state, find out what's blocking a query, and stop the session responsible. + +### Session states + +The `state` column on `pg_stat_activity` can be one of six values: + +| State | Meaning | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `active` | The session is currently executing a query. A query running longer than expected is worth investigating, but a nonzero count of active sessions is normal on its own. | +| `idle` | The session is connected and waiting for the client to send a new command. Normal for pooled or long-lived connections. | +| `idle in transaction` | The session has an open transaction but isn't currently running a query. This holds locks and blocks table cleanup for as long as it stays open, and almost always means the application forgot to commit or roll back. | +| `idle in transaction (aborted)` | The last statement in the transaction failed. Postgres releases the transaction's ordinary locks as part of the abort - the exception is session-level advisory locks (`pg_advisory_lock`, not `pg_advisory_xact_lock`), which are held until explicitly unlocked or the session ends. The client still needs to send `rollback` to close the transaction before the session will accept new commands. | +| `fastpath function call` | The session is executing a function through Postgres's low-level fastpath protocol, most commonly for large object reads or writes. Rare, and normally brief. | +| `disabled` | Activity tracking (`track_activities`) is turned off for this session, so Postgres isn't recording its state or query. | + +`idle in transaction` is the state most worth watching. Unlike a slow `active` query, which is at least making progress, an idle-in-transaction session is holding its locks indefinitely while doing nothing. + +### Finding blocked queries + +Postgres tracks which sessions are waiting on a lock held by another session. Query `pg_stat_activity` and `pg_blocking_pids()` together to see this directly: + +```sql +select + a.pid, + a.usename as role_name, + a.application_name, + a.state, + a.query, + a.wait_event_type, + a.wait_event, + a.xact_start as transaction_start, + a.query_start, + a.state_change, + pg_blocking_pids(a.pid) as blocked_by +from pg_stat_activity as a +where + a.datname = current_database() + and a.pid != pg_backend_pid() + and a.backend_type = 'client backend' +order by a.query_start asc nulls last; +``` + +The `where` clause excludes the query's own session and Postgres's internal background workers (autovacuum, the WAL writer, extensions like `pg_cron`), so the results only show sessions a client opened. + +`blocked_by` is an array, not a single value, for two reasons: + +- A session can be waiting on more than one session at once, if several sessions hold a lock that all conflict with what it's requesting. +- Blocking can chain: session A holds a lock, session B waits on A, session C waits on B. `blocked_by` only ever lists the _direct_ blocker, so tracing a long queue back to its root cause may mean following the chain through more than one session. + +### Cancelling or terminating a session + +Postgres gives you two ways to stop a session, and they behave differently: + +- `select pg_cancel_backend(pid);` cancels the session's _currently running query_ but leaves the connection open. The application gets one failed query back and can continue using that connection. +- `select pg_terminate_backend(pid);` ends the session's connection entirely. + +Which one to use depends on the session's state: + +- For an `active` session that's stuck waiting on a lock or running longer than expected, cancel it first. It's the less disruptive option, and it's usually enough to unstick the wait or stop the runaway query. +- For a session that's `idle in transaction`, cancelling does nothing - there's no running query to cancel, and the open transaction stays open regardless. Terminating the session is the only way to force the transaction closed and release its locks. +- For a session that's `idle in transaction (aborted)`, its ordinary locks were already released when the transaction aborted. The correct fix is for the client to issue `rollback`, which closes the transaction cleanly. Reserve `pg_terminate_backend` for a client that's unresponsive or a connection that needs to close regardless - it won't release anything further in this case, aside from a session-level advisory lock, which persists until the session ends. + +Both functions require you to either be a superuser, hold the `pg_signal_backend` role, or be signalling your own session - and even `pg_signal_backend` can't be used to stop a session belonging to an actual superuser role. If you hit a permission error while terminating a session, see [this troubleshooting guide](/docs/guides/troubleshooting/high-cpu-and-slow-queries-with-error-must-be-a-superuser-to-terminate-superuser-process). + +--- + +If you're on Supabase, you can see all of this - session states, blocking chains, and a way to terminate a stuck session - on your project's [Database Connections](/dashboard/project/_/observability/connections) page in the dashboard, without writing any SQL. diff --git a/apps/docs/content/troubleshooting/how-to-check-if-my-queries-are-being-blocked-by-other-queries-NSKtR1.mdx b/apps/docs/content/troubleshooting/how-to-check-if-my-queries-are-being-blocked-by-other-queries-NSKtR1.mdx index 2d08cca66f8..4344c061eb4 100644 --- a/apps/docs/content/troubleshooting/how-to-check-if-my-queries-are-being-blocked-by-other-queries-NSKtR1.mdx +++ b/apps/docs/content/troubleshooting/how-to-check-if-my-queries-are-being-blocked-by-other-queries-NSKtR1.mdx @@ -40,3 +40,5 @@ where not blockedl.granted and blockinga.datname = current_database(); ``` + +For a more detailed look at session states, blocking chains, and how to stop the session responsible, see [Diagnosing stuck and blocked queries](/docs/guides/database/connection-management#diagnosing-stuck-and-blocked-queries). diff --git a/apps/docs/data/content-listings/telemetry.data.ts b/apps/docs/data/content-listings/telemetry.data.ts index 93ba564a300..047e81c69d5 100644 --- a/apps/docs/data/content-listings/telemetry.data.ts +++ b/apps/docs/data/content-listings/telemetry.data.ts @@ -27,6 +27,11 @@ export const telemetryDebugging: ContentListingGroup = { href: '/guides/troubleshooting', description: 'Searchable index of known error codes, symptoms, and fixes.', }, + { + title: 'Diagnosing stuck and blocked queries', + href: '/guides/database/connection-management#diagnosing-stuck-and-blocked-queries', + description: 'Find sessions blocked by a lock, and cancel or terminate the one responsible.', + }, ], } diff --git a/supa-mdx-lint/Rule003Spelling.toml b/supa-mdx-lint/Rule003Spelling.toml index 5930697ca85..96390b1d785 100644 --- a/supa-mdx-lint/Rule003Spelling.toml +++ b/supa-mdx-lint/Rule003Spelling.toml @@ -73,6 +73,7 @@ allow_list = [ "[Ee]x", "[Ee]xecutables?", "[Ee]xfiltrat(e|ed|es|ing)?", + "[Ff]astpath", "[Ff]atals", "[Ff]ootguns?", "[Ff]rontend",