DEV Community

Akkarapon Phikulsri
Akkarapon Phikulsri

Posted on AI-assisted

PostgreSQL Cheat Sheet: Investigating and Terminating Database Blocking Sessions

If you run PostgreSQL in production for a long time, you will probably see this problem at some point:

  • Queries start waiting.
  • Connections increase.
  • The database becomes slower.
  • Connection usage gets close to max_connections.

One common cause is a session that is blocking other sessions.

A common example is a connection in the idle in transaction state. The application started a transaction but did not COMMIT or ROLLBACK it.

In this article, we will use pg_stat_activity and several PostgreSQL functions to investigate these problems.

We will also look at when to use:

  • pg_cancel_backend()
  • pg_terminate_backend()
  • pg_blocking_pids()

The goal is not only to kill a bad session. The goal is to understand why it happened and prevent it from happening again.


Why Can PostgreSQL Sessions Block Each Other?

PostgreSQL uses MVCC (Multi-Version Concurrency Control) and locking to support many users at the same time.

For example:

BEGIN;

UPDATE orders
SET status = 'PROCESSING'
WHERE id = 100;
Enter fullscreen mode Exit fullscreen mode

The transaction may hold locks until it finishes with:

COMMIT;
Enter fullscreen mode Exit fullscreen mode

or:

ROLLBACK;
Enter fullscreen mode Exit fullscreen mode

The problem happens when the application starts a transaction but never finishes it.

The connection may then appear as:

idle in transaction
Enter fullscreen mode Exit fullscreen mode

The session is not running a query anymore, but the transaction is still open.

This can cause several problems:

  • Locks may still be held.
  • Other queries may have to wait.
  • Old transaction snapshots may stay open.
  • VACUUM may not be able to clean some dead rows.
  • Tables may become larger over time.

This is why it is important to understand the difference between:

active
Enter fullscreen mode Exit fullscreen mode

and:

idle in transaction
Enter fullscreen mode Exit fullscreen mode

An active session is currently running a query.

An idle in transaction session is waiting for the client, but its transaction is still open.


Step 1: Inspect All Non-Idle Sessions

I normally start an investigation with this query:

SELECT
    pid,
    usename,
    application_name,
    state,
    query_start,
    now() - query_start AS duration,
    query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;
Enter fullscreen mode Exit fullscreen mode

This removes normal idle connections from the result.

You can now see sessions that are:

  • running queries,
  • waiting,
  • or idle inside a transaction.

The application_name field is very useful when you have many services.

For example, your PostgreSQL connection may include:

application_name=order-service
Enter fullscreen mode Exit fullscreen mode

Then you may see something like:

pid     application_name   state
12345   order-service      active
12346   payment-service    idle in transaction
Enter fullscreen mode Exit fullscreen mode

This is much easier than trying to guess which application owns a PID.

For Go, Node.js, or other microservice systems, I strongly recommend setting application_name for every service.


Step 2: Use a Quick Triage Query

During an incident, sometimes you do not need every column.

You just want to quickly see what is happening.

Use:

SELECT
    pid,
    age(clock_timestamp(), query_start),
    state,
    query
FROM pg_stat_activity
WHERE state <> 'idle';
Enter fullscreen mode Exit fullscreen mode

This shows:

  • PID
  • query age
  • session state
  • query text

I use this kind of query as a quick first check.

For example:

PID 1596183
State: active
Duration: 00:08:32
Query: SELECT ...
Enter fullscreen mode Exit fullscreen mode

Now you know that one query has been running for more than eight minutes.

The next question is:

Is this query really slow, or is it waiting for another session?

That is an important difference.


Step 3: Find Idle-in-Transaction Sessions

To check only sessions that are idle inside a transaction:

SELECT
    pid,
    usename,
    application_name,
    state,
    xact_start,
    state_change,
    now() - xact_start AS transaction_age,
    query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start;
Enter fullscreen mode Exit fullscreen mode

Pay special attention to:

xact_start
Enter fullscreen mode Exit fullscreen mode

This tells you when the transaction started.

A transaction that has been open for a few seconds may be normal.

A transaction that has been open for 30 minutes or several hours is much more suspicious.

Do not immediately kill every idle in transaction connection.

First check:

  • Which application created it?
  • How long has the transaction been open?
  • Is this normal for the application?
  • Is it blocking another session?

Step 4: Terminate an Idle Transaction

If you confirm that a session is stuck and should not still be open, you can terminate it.

For one PID:

SELECT pg_terminate_backend(1596183);
Enter fullscreen mode Exit fullscreen mode

This closes the PostgreSQL session.

PostgreSQL will roll back its open transaction and release its locks.

For old idle transactions, you can also use a controlled query like:

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND state_change < now() - interval '5 minutes'
  AND pid <> pg_backend_pid();
Enter fullscreen mode Exit fullscreen mode

This example only terminates sessions that have been idle inside a transaction for more than five minutes.

The condition:

pid <> pg_backend_pid()
Enter fullscreen mode Exit fullscreen mode

prevents the query from terminating your own PostgreSQL session.

Be careful with this command in production.

It is usually better to filter by things such as:

datname
Enter fullscreen mode Exit fullscreen mode
usename
Enter fullscreen mode Exit fullscreen mode

or:

application_name
Enter fullscreen mode Exit fullscreen mode

For example:

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND application_name = 'order-service'
  AND state_change < now() - interval '5 minutes'
  AND pid <> pg_backend_pid();
Enter fullscreen mode Exit fullscreen mode

This is much safer than terminating every idle transaction in the cluster.


Step 5: Cancel a Running Query

Sometimes the connection itself is fine, but one query is taking too long.

In that case, try:

SELECT pg_cancel_backend(1596183);
Enter fullscreen mode Exit fullscreen mode

pg_cancel_backend() stops the current query but keeps the database connection alive.

This makes it a good first choice for things like:

  • a very expensive SELECT,
  • a report query,
  • an accidental full-table operation,
  • or a query that a user wants to cancel.

Think about it like this:

pg_cancel_backend()
        |
        v
Stop the current query
        |
        v
Keep the connection
Enter fullscreen mode Exit fullscreen mode

pg_cancel_backend() vs pg_terminate_backend()

These two functions are similar, but they solve different problems.

Function What happens Good for
pg_cancel_backend(pid) Stops the current query but keeps the connection Long-running or unwanted active queries
pg_terminate_backend(pid) Closes the whole database session Stuck sessions, idle transactions, serious blocking problems

My normal approach is:

Active bad query
     |
     v
pg_cancel_backend()
     |
     | still causing problems
     v
pg_terminate_backend()
Enter fullscreen mode Exit fullscreen mode

For an idle in transaction session, pg_cancel_backend() normally does not solve the problem because there is no active query to cancel.

In that situation, you may need:

pg_terminate_backend()
Enter fullscreen mode Exit fullscreen mode

Step 6: Find Who Is Blocking a Query

A long-running query is not always slow.

Sometimes it is simply waiting for another transaction.

PostgreSQL provides a very useful function:

pg_blocking_pids()
Enter fullscreen mode Exit fullscreen mode

For example:

SELECT
    pid,
    usename,
    application_name,
    state,
    pg_blocking_pids(pid) AS blocking_pids,
    query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
Enter fullscreen mode Exit fullscreen mode

You may get something like:

pid       blocking_pids
-----------------------
20002     {20001}
Enter fullscreen mode Exit fullscreen mode

This means:

PID 20001
    |
    | blocks
    v
PID 20002
Enter fullscreen mode Exit fullscreen mode

Do not immediately kill PID 20002.

It is the blocked session, not necessarily the real problem.

You should investigate PID 20001.


Finding the Root Blocker

A production problem can be more complicated:

Session A
   |
   | blocks
   v
Session B
   |
   | blocks
   v
Session C
Enter fullscreen mode Exit fullscreen mode

If you kill Session B, Session C may improve, but you have not fixed the real source of the problem.

The real blocker is:

Session A
Enter fullscreen mode Exit fullscreen mode

That is why pg_blocking_pids() is useful.

You want to follow the blocking chain until you find the session that is not waiting for another backend.

That session is usually the root blocker.

Investigate that session first.


Step 7: Check Which Database Uses the Most Connections

If your PostgreSQL server contains several databases, check how connections are distributed:

SELECT
    datname,
    count(*)
FROM pg_stat_activity
GROUP BY datname
ORDER BY count(*) DESC;
Enter fullscreen mode Exit fullscreen mode

Example:

datname        count
-------------  -----
order_prod       82
notification     15
postgres          3
Enter fullscreen mode Exit fullscreen mode

Now you know where most connections are being used.

If one database has a very high number, investigate:

  • application connection pool size,
  • PgBouncer settings,
  • connection leaks,
  • long-running queries,
  • idle transactions.

A high connection count does not always mean PostgreSQL itself is the problem.

The application may simply be opening too many connections.


Step 8: Check Connection Headroom

You should also check how close PostgreSQL is to its configured connection limit.

SELECT
    (SELECT count(*) FROM pg_stat_activity) AS used_connections,
    (
        SELECT setting::int
        FROM pg_settings
        WHERE name = 'max_connections'
    ) AS max_connections;
Enter fullscreen mode Exit fullscreen mode

For example:

used_connections | max_connections
-----------------+----------------
87               | 100
Enter fullscreen mode Exit fullscreen mode

You are now using:

87 / 100
Enter fullscreen mode Exit fullscreen mode

connections.

That means there is very little room left.

This becomes dangerous because when an incident happens, you may not even be able to open another connection to investigate the problem.

For production systems, I normally monitor connection usage before it gets close to the maximum.


Preventing Idle Transactions

Manually killing sessions is useful during an incident.

But it is not the real fix.

PostgreSQL can automatically terminate connections that stay idle inside a transaction for too long.

For example:

ALTER DATABASE mydb
SET idle_in_transaction_session_timeout = '2min';
Enter fullscreen mode Exit fullscreen mode

Now PostgreSQL will close sessions that remain idle inside a transaction longer than the configured timeout.

You should choose the timeout based on your workload.

For example:

30 seconds
Enter fullscreen mode Exit fullscreen mode

may work for a fast OLTP API.

While:

5 minutes
Enter fullscreen mode Exit fullscreen mode

may be safer for applications with longer business operations.

Do not copy a timeout value without understanding your system first.


Protect Against Long-Running Queries

You can also use:

statement_timeout
Enter fullscreen mode Exit fullscreen mode

For example:

ALTER DATABASE mydb
SET statement_timeout = '30s';
Enter fullscreen mode Exit fullscreen mode

This prevents individual SQL statements from running forever.

However, be careful.

A timeout that is good for API requests may be too short for:

  • reports,
  • migrations,
  • ETL jobs,
  • batch processing.

Different applications may need different timeout values.


Log Lock Problems

Another useful PostgreSQL setting is:

log_lock_waits
Enter fullscreen mode Exit fullscreen mode

You can enable it with:

ALTER SYSTEM SET log_lock_waits = on;
Enter fullscreen mode Exit fullscreen mode

PostgreSQL can then log queries that wait too long for a lock.

This works together with:

deadlock_timeout
Enter fullscreen mode Exit fullscreen mode

For example:

ALTER SYSTEM SET deadlock_timeout = '1s';
Enter fullscreen mode Exit fullscreen mode

This makes lock problems much easier to investigate because PostgreSQL logs useful information when sessions wait for locks.

Remember to reload the configuration when required.


Fix the Problem in the Application

Database commands help during an incident.

But if idle in transaction keeps happening, the real bug is usually inside the application.

For example, in Go:

tx, err := db.Begin(ctx)
if err != nil {
    return err
}

defer tx.Rollback(ctx)
Enter fullscreen mode Exit fullscreen mode

Then later:

if err := tx.Commit(ctx); err != nil {
    return err
}
Enter fullscreen mode Exit fullscreen mode

The important idea is simple:

BEGIN
  |
  +---- success ----> COMMIT
  |
  +---- error ------> ROLLBACK
Enter fullscreen mode Exit fullscreen mode

Every transaction must have a clear ending.

This is especially important when using libraries such as:

  • pgx
  • GORM
  • database/sql
  • Node.js pg

Every error path should either commit or roll back the transaction correctly.


Connection Pools Matter Too

Also check your connection pool configuration.

For example, imagine you have:

10 services
Enter fullscreen mode Exit fullscreen mode

and every service allows:

20 connections
Enter fullscreen mode Exit fullscreen mode

Your applications may try to create:

10 × 20 = 200 connections
Enter fullscreen mode Exit fullscreen mode

But PostgreSQL may only allow:

max_connections = 100
Enter fullscreen mode Exit fullscreen mode

That architecture will eventually create problems.

Tools such as PgBouncer can help reduce the number of PostgreSQL backend connections, but they do not replace correct transaction handling.

You still need to:

  • commit transactions,
  • roll back transactions,
  • set reasonable pool limits,
  • monitor connection usage.

My Production Troubleshooting Flow

When PostgreSQL starts becoming slow, I normally think about the problem in this order:

PostgreSQL is slow
        |
        v
Check pg_stat_activity
        |
        v
Are queries active or idle in transaction?
        |
        +------ idle in transaction
        |             |
        |             v
        |      Check transaction age
        |             |
        |             v
        |      Check if it is blocking
        |             |
        |             v
        |      pg_terminate_backend()
        |
        +------ active
                      |
                      v
              Check blocking_pids
                      |
              +-------+-------+
              |               |
           blocked         not blocked
              |               |
              v               v
         Find blocker    Investigate query
                              |
                              v
                      pg_cancel_backend()
Enter fullscreen mode Exit fullscreen mode

The important point is:

Do not kill a PostgreSQL PID only because a query has been running for a long time.

First understand what that PID is doing.

Ask:

Is it running?

Is it waiting?

Is it blocking?

Is it being blocked?

Is it idle inside a transaction?
Enter fullscreen mode Exit fullscreen mode

Then choose the correct action.


Useful Commands Summary

Check non-idle sessions:

SELECT
    pid,
    usename,
    application_name,
    state,
    query_start,
    now() - query_start AS duration,
    query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;
Enter fullscreen mode Exit fullscreen mode

Find idle transactions:

SELECT
    pid,
    application_name,
    xact_start,
    now() - xact_start AS transaction_age,
    query
FROM pg_stat_activity
WHERE state = 'idle in transaction';
Enter fullscreen mode Exit fullscreen mode

Find blocked sessions:

SELECT
    pid,
    pg_blocking_pids(pid) AS blocking_pids,
    query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
Enter fullscreen mode Exit fullscreen mode

Cancel a query:

SELECT pg_cancel_backend(1596183);
Enter fullscreen mode Exit fullscreen mode

Terminate a connection:

SELECT pg_terminate_backend(1596183);
Enter fullscreen mode Exit fullscreen mode

Check connections per database:

SELECT
    datname,
    count(*)
FROM pg_stat_activity
GROUP BY datname
ORDER BY count(*) DESC;
Enter fullscreen mode Exit fullscreen mode

Check connection limit:

SELECT
    (SELECT count(*) FROM pg_stat_activity) AS used_connections,
    (
        SELECT setting::int
        FROM pg_settings
        WHERE name = 'max_connections'
    ) AS max_connections;
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

When PostgreSQL has a connection or blocking problem, pg_stat_activity is one of the first places I check.

But the most important skill is not knowing how to kill a PID.

It is knowing which PID should be killed and why.

A blocked query may only be a victim.

An idle in transaction session may be the real blocker.

And a high connection count may be caused by an application pool instead of PostgreSQL itself.

A good production investigation should therefore follow this process:

Observe
   ↓
Find the blocker
   ↓
Understand the transaction
   ↓
Mitigate the incident
   ↓
Fix the application
   ↓
Add protection and monitoring
Enter fullscreen mode Exit fullscreen mode

Commands like pg_cancel_backend() and pg_terminate_backend() are useful emergency tools.

But the long-term solution is better transaction handling, good connection pool limits, proper timeouts, and enough monitoring to find the problem before production users notice it.

Top comments (0)