Database Advanced

Resolving PostgreSQL VACUUM FULL Table Locks Blocking Queries on Alpine Linux

Troubleshoot and resolve VACUUM FULL operations locking PostgreSQL tables, preventing SELECT queries on Alpine Linux, ensuring high availability.

👨‍💻
Senior Systems Architect • Verified in Staging Labs

Troubleshoot and resolve VACUUM FULL operations locking PostgreSQL tables, preventing SELECT queries on Alpine Linux, ensuring high availability.

Introduction

As a seasoned Systems Administrator and DevOps engineer, you've likely encountered the challenges of maintaining high availability and performance in dynamic production environments. One particularly disruptive issue in PostgreSQL is when a VACUUM FULL operation locks a critical table, preventing essential SELECT queries and bringing your application to a grinding halt. This guide will walk you through diagnosing, mitigating, and preventing such scenarios specifically on Alpine Linux, a popular choice for lightweight containerized deployments.

While VACUUM FULL is a powerful command for reclaiming disk space and improving performance by rewriting entire tables, its aggressive locking mechanism (ACCESS EXCLUSIVE) makes it unsuitable for routine use on busy production databases. Understanding its impact and implementing proactive strategies are key to avoiding unexpected downtime.

Symptom & Error Signature

When a VACUUM FULL command locks a table, applications attempting to query that table will experience delays, timeouts, or outright errors. Here's what you might observe:

1. Application/Web Server Logs: Your application logs will typically show connection timeouts or query cancellation errors:

# Example from a web application log (e.g., Ruby on Rails, Django)
ActiveRecord::ConnectionTimeoutError: could not obtain a connection from the pool within 5.000 seconds (waited 5.000 seconds)
# Or for a specific query:
ActiveRecord::StatementInvalid: PG::QueryCanceled: ERROR:  canceling statement due to user request

2. PostgreSQL Logs (if log_lock_waits is enabled): If log_lock_waits = on is configured in postgresql.conf, you will see messages indicating processes waiting for locks:

LOG:  process 12345 still waiting for ShareLock on transaction 67890 after 1000.000 ms
DETAIL:  Process 54321 holds ExclusiveLock on relation 16384 of database 16385
HINT:  Consider increasing deadlock_timeout.

3. pg_stat_activity Output: The most direct way to observe the issue is by querying pg_stat_activity from the PostgreSQL prompt. You'll see the VACUUM FULL process in an active state, holding a lock, and other queries in a waiting state.

-- Connect to PostgreSQL using psql
psql -U your_user -d your_database

-- Query pg_stat_activity for active and waiting queries
SELECT
    pid, usename, application_name, client_addr, backend_start, state,
    wait_event_type, wait_event, query_start, query, age(now(), query_start) AS duration
FROM pg_stat_activity
WHERE state = 'active' OR wait_event IS NOT NULL
ORDER BY duration DESC;

You would typically see output resembling this, with a VACUUM FULL command holding an AccessExclusiveLock and other queries waiting:

  pid  | usename  | application_name |  client_addr   |      backend_start      |   state   | wait_event_type |  wait_event   |         query_start         |               query                |      duration
-------+----------+------------------+----------------+-------------------------+-----------+-----------------+---------------+-----------------------------+------------------------------------+--------------------
 20001 | postgres | psql             |                | 2026-11-11 10:00:00.123 | active    |                 |               | 2026-11-11 10:05:00.123     | VACUUM FULL my_table;              | 00:10:00.000
 20002 | app_user | MyApp            | 192.168.1.100  | 2026-11-11 10:02:00.456 | active    | Lock            | RelationLock  | 2026-11-11 10:06:00.456     | SELECT * FROM my_table WHERE id=1; | 00:09:00.000
 20003 | app_user | MyApp            | 192.168.1.101  | 2026-11-11 10:02:10.789 | active    | Lock            | RelationLock  | 2026-11-11 10:06:10.789     | INSERT INTO my_table (...) VALUES (...) | 00:08:50.000

Root Cause Analysis

The core of the issue lies in the locking behavior of the VACUUM FULL command:

  1. VACUUM FULL Acquires an ACCESS EXCLUSIVE Lock: Unlike a standard VACUUM (which reclaims space from dead tuples without rewriting the entire table and holds a less restrictive ShareUpdateExclusiveLock), VACUUM FULL rewrites the entire table contents to a new file, compacting it and reclaiming space. To do this safely, it acquires the most restrictive lock available: ACCESS EXCLUSIVE.
  2. Total Blocking of Operations: An ACCESS EXCLUSIVE lock prevents all other operations on the table, including SELECT, INSERT, UPDATE, DELETE, and even other VACUUM commands. Any query attempting to access the locked table will be forced to wait until the VACUUM FULL operation completes or is terminated.
  3. Duration and Impact: For large tables, VACUUM FULL can take minutes or even hours to complete. During this entire period, the locked table is completely unavailable, leading to severe application downtime and degraded user experience.
  4. Alpine Linux Context (Exacerbating Factors): While Alpine Linux itself doesn't cause this lock, its typical usage pattern can worsen the problem:
    • Resource Constraints: Alpine is often used in lightweight Docker containers or VMs with limited CPU and I/O resources. A VACUUM FULL operation on such an environment can run significantly slower than on a more powerful host, extending the lock duration.
    • Default Configuration: Default PostgreSQL configurations might not be optimized for I/O performance in resource-constrained Alpine environments, further slowing down table rewrites.
    • Improper Scheduling: Running VACUUM FULL without careful planning or during peak hours, especially on resource-limited systems, inevitably leads to blocking issues.
  5. Accumulated Bloat: The underlying reason for why someone might feel compelled to run VACUUM FULL is often severe table bloat (dead tuples taking up excessive space) due to inadequate autovacuum tuning or heavy transaction workloads.

Step-by-Step Resolution

When faced with a live VACUUM FULL blocking your production system, immediate action is required, followed by long-term preventative measures.

1. Identify the Blocking Process

First, you need to confirm that VACUUM FULL is indeed the culprit and identify its process ID (PID).

# Connect to your PostgreSQL database
psql -U your_user -d your_database -h localhost

Execute the following query to find the blocking and blocked processes:

SELECT
    a.pid AS blocking_pid,
    a.usename AS blocking_user,
    a.application_name AS blocking_app,
    a.query AS blocking_query,
    a.backend_start AS blocking_start,
    b.pid AS blocked_pid,
    b.usename AS blocked_user,
    b.application_name AS blocked_app,
    b.query AS blocked_query,
    b.backend_start AS blocked_start,
    pg_blocking_pids(b.pid) AS blocked_by,
    age(now(), b.query_start) AS blocked_duration
FROM pg_stat_activity a
JOIN pg_stat_activity b ON pg_blocking_pids(b.pid) @> ARRAY[a.pid]
WHERE a.state = 'active'
AND a.query ILIKE '%VACUUM FULL%'
ORDER BY blocked_duration DESC;

This query will show you which VACUUM FULL process (the blocking_pid) is holding a lock and which other queries (blocked_pid) are currently waiting because of it.

Carefully identify the VACUUM FULL process by its blocking_pid and blocking_query. Misidentifying and terminating the wrong process can lead to data corruption or further service interruption.

2. Terminate the Blocking VACUUM FULL Process (Emergency Mitigation)

Once you've identified the VACUUM FULL process, you can terminate it. There are two primary functions for this:

  • pg_cancel_backend(PID): Attempts to gracefully cancel the query. The backend process will continue to run but will stop executing the current query. This is the preferred method for less critical scenarios.
  • pg_terminate_backend(PID): Forces the termination of the entire backend process. This is more aggressive and should be used when pg_cancel_backend is insufficient or for immediate relief. The terminated process's transaction will be rolled back.
-- Connect to PostgreSQL using psql
psql -U your_user -d your_database

-- Attempt to gracefully cancel the blocking VACUUM FULL process
SELECT pg_cancel_backend(blocking_pid_from_step_1);

-- Wait a few seconds and check pg_stat_activity again.
-- If queries are still blocked, you might need to forcefully terminate.
-- USE WITH EXTREME CAUTION IN PRODUCTION!
SELECT pg_terminate_backend(blocking_pid_from_step_1);

Terminating a VACUUM FULL forcefully using pg_terminate_backend() will cause the entire operation to roll back. This can be a time-consuming process for large tables, potentially causing further I/O strain and temporary performance degradation until the rollback completes. Only use pg_terminate_backend() as a last resort in an emergency to restore service.

After terminating, immediately check pg_stat_activity to ensure the blocked queries are now executing. Your application should start recovering.

3. Proactive Measures & Long-Term Solutions

Preventing future occurrences is crucial. Implement the following strategies:

a. Avoid VACUUM FULL in Production (if possible)
  • VACUUM FULL should almost never be run on busy production tables due to its blocking nature.
  • It's primarily intended for reclaiming disk space after significant deletions in development/staging or as part of a planned downtime.
  • For routine maintenance and bloat management, rely on VACUUM (non-FULL) and autovacuum.
b. Configure and Tune Autovacuum

Ensure autovacuum is enabled and properly tuned to keep table bloat in check, reducing the temptation to run VACUUM FULL.

  1. Locate postgresql.conf: On Alpine Linux, PostgreSQL configuration files are often found in /etc/postgresql/<version>/postgresql.conf or /var/lib/postgresql/data/postgresql.conf depending on how Postgres was installed (e.g., from apk or a custom build/Docker image).

  2. Edit postgresql.conf: Verify or set these parameters:

    # /etc/postgresql/<version>/postgresql.conf (or similar path)
    autovacuum = on
    log_autovacuum_min_duration = 0 # Log all autovacuum actions for monitoring
    autovacuum_max_workers = 3      # Number of concurrent autovacuum processes, adjust based on CPU
    autovacuum_vacuum_cost_delay = 10ms # Delay between vacuum cycles (lower = more aggressive)
    autovacuum_vacuum_scale_factor = 0.2 # % of table size to trigger vacuum
    autovacuum_analyze_scale_factor = 0.1 # % of table size to trigger analyze
    autovacuum_vacuum_cost_limit = -1 # Use global limit, or set for specific databases
    

    For specific tables experiencing heavy churn, you can set autovacuum parameters at the table level:

    ALTER TABLE your_table SET (autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_cost_delay = 5);
    
  3. Restart PostgreSQL: After modifying postgresql.conf, you must restart the PostgreSQL service.

    • For Alpine Linux with OpenRC (common native init system):
      sudo rc-service postgresql restart
      
    • If running PostgreSQL in Docker on Alpine:
      docker restart your_postgres_container_name_or_id
      
    • If using Systemd (less common on Alpine by default, but possible):
      sudo systemctl restart postgresql
      
c. Address Table Bloat with pg_repack

For severely bloated tables where a regular VACUUM isn't sufficient to reclaim disk space, pg_repack is an invaluable tool. It allows you to remove bloat from tables and indexes online, without holding an ACCESS EXCLUSIVE lock for extended periods.

  1. Install pg_repack:

    • On Alpine Linux, you might find pg_repack within postgresql-contrib or as a standalone package.
      sudo apk add postgresql-contrib # Check if pg_repack is included
      # Or, if available as a dedicated package:
      # sudo apk add pg_repack
      
    • After installation, enable the extension in your database:
      psql -U your_user -d your_database
      CREATE EXTENSION pg_repack;
      
  2. Usage: To repack a specific table:

    pg_repack -d your_database -t your_table -U your_user -h localhost
    

    To repack all tables in a database (can still be I/O intensive, but non-blocking):

    pg_repack -d your_database -U your_user -h localhost
    

    pg_repack is highly recommended. It works by creating a new version of the table, applying changes from concurrent transactions, and then atomically swapping the old and new tables. This process minimizes downtime to a very brief period during the final swap, instead of the entire table rewrite time.

d. Schedule VACUUM FULL During Maintenance Windows

If pg_repack is not an option or for specific extreme cases (e.g., massive table size reduction on a non-critical table), schedule VACUUM FULL during planned maintenance windows.

  • Communicate downtime to users.
  • Put the application into maintenance mode.
  • Scale down database connections or stop application processes during the operation.
e. Monitoring and Alerting

Proactive monitoring can help identify potential issues before they become critical:

  • Long-Running Queries: Monitor pg_stat_activity for queries running longer than expected. Tools like Prometheus/Grafana or Zabbix can pull this data and alert you.

  • Lock Waits: Set up alerts for state = 'waiting' or wait_event_type = 'Lock' on critical tables.

  • Table Bloat: Regularly monitor n_dead_tup and n_live_tup in pg_stat_user_tables to identify tables with high dead tuple ratios, indicating the need for more aggressive autovacuum tuning or a pg_repack.

    SELECT
        relname,
        n_live_tup,
        n_dead_tup,
        round(100.0 * n_dead_tup / (n_live_tup + n_dead_tup + 1), 2) AS dead_ratio
    FROM pg_stat_user_tables
    ORDER BY dead_ratio DESC;
    
f. Review Database Schema and Indexes

Inefficient schema design, missing indexes, or poorly written queries can contribute to excessive dead tuples and overall database bloat. Regularly analyze your slow queries using EXPLAIN ANALYZE and optimize your indexes. This holistic approach reduces the need for aggressive VACUUM FULL operations.

By understanding the mechanisms of VACUUM FULL and implementing these proactive measures, you can ensure your PostgreSQL database on Alpine Linux remains performant and highly available.

👨‍💻

Johnathon Wheeler

Senior Systems Architect & DevOps Engineer • Austin, TX

Connect on LinkedIn →

Johnathon has over 16 years of hands-on experience designing, debugging, and scaling Linux web hosting stacks, container clusters, and high-availability database architectures. Every guide on ButItWorkedLocal is independently tested against Debian 12, Ubuntu 24.04/22.04 LTS, Rocky Linux, and Docker environments to guarantee reproducibility in production.

🛡️

Our Production Verification Guarantee

Encountering a bug not covered here or running a non-standard kernel configuration? Our solutions are continually refined against real production incidents. Submit an environment trace for our editorial team to replicate.