How to fix SQLITE_READONLY_CANTLOCK: Unable to obtain a read lock in SQLite

SQLiteINTERMEDIATEMEDIUMUpdated Dec 29, 2025· 227 views

The SQLITE_READONLY_CANTLOCK error occurs when SQLite cannot obtain a read lock on a WAL (Write-Ahead Logging) mode database because the shared-memory file is read-only. This typically happens when file permissions prevent SQLite from accessing or modifying the .db-shm file needed for WAL coordination.

Quick fix

Fixes SQLITE_READONLY_CANTLOCK: Unable to obtain a read lock

# List all database-related files
ls -la /path/to/database.db*

# Example output (problematic permissions):
# -rw-r--r-- 1 user  group  8192 Dec 22 database.db
# -r--r--r-- 1 user  group   512 Dec 22 database.db-shm  <- READ-ONLY!
# -rw-r--r-- 1 user  group  4096 Dec 22 database.db-wal
  1. Verify .db-shm file existence and permissions
  2. Fix .db-shm file permissions
  3. Ensure directory has write permissions
  4. Convert database from WAL mode to DELETE mode
sqlite — error
# List all database-related files
ls -la /path/to/database.db*
# Example output (problematic permissions):
# -rw-r--r-- 1 user group 8192 Dec 22 database.db
# -r--r--r-- 1 user group 512 Dec 22 database.db-shm <- READ-ONLY!
# -rw-r--r-- 1 user group 4096 Dec 22 database.db-wal
SQLITE_READONLY_CANTLOCK: Unable to obtain a read lock

What this error means

The "SQLITE_READONLY_CANTLOCK: Unable to obtain a read lock" error is a specific extended result code of SQLITE_READONLY, indicating that SQLite cannot acquire the necessary read lock for a WAL mode database because the shared-memory file (.db-shm) is read-only or inaccessible. **Understanding WAL Mode and Locking:** SQLite's WAL (Write-Ahead Logging) mode uses a three-file architecture: 1. **database.db**: The main database file 2. **database.db-wal**: The write-ahead log containing recent transactions 3. **database.db-shm**: The shared memory file used for lock coordination Even for read-only access, WAL mode requires write access to the .db-shm file because SQLite uses specific bytes in this file to coordinate between readers and writers. The locks are implemented as atomic modifications to this shared memory file. **Why the Error Occurs:** The SQLITE_READONLY_CANTLOCK error specifically means: - SQLite is trying to open a WAL mode database - The process has read access to the main database file - The process CANNOT write to the .db-shm file (either it doesn't exist and can't be created, or it exists but is read-only) - WAL mode requires write access to .db-shm for lock coordination, making the database effectively unusable This is different from SQLITE_READONLY_RECOVERY which occurs when WAL recovery fails. CANTLOCK means the error happens before recovery even starts—SQLite can't get the fundamental locks needed to operate safely. **Common Scenarios:** 1. **Database deployed to read-only media** (CD-ROM, read-only filesystem, application bundle) 2. **File permissions on .db-shm are too restrictive** (0444 instead of 0664) 3. **Directory containing database is read-only** (prevents creating .db-shm if missing) 4. **Running in read-only container or sandbox** with restrictions on specific files 5. **SELinux or AppArmor** enforcing restrictions on the shared memory file specifically 6. **Database files copied from read-only source** with restrictive permissions preserved 7. **Network filesystem** with sync issues preventing proper shared memory access

Typical symptoms

  • Database connection fails with "SQLITE_READONLY_CANTLOCK" error message
  • Error occurs specifically when trying to read from database, not write
  • Opening database works in DELETE journal mode but fails in WAL mode
  • sqlite3 command-line tool shows error when accessing the database
  • Application crashes or throws exception on database open
  • Error appears in logs when app tries to initialize read-only database
  • Problem only occurs with certain WAL databases, not all databases
  • Works when you have write permissions, fails in read-only environments

Common causes

  1. The .db-shm file has read-only permissions (mode 0444 or similar)
  2. The directory containing the database is read-only and prevents .db-shm modification
  3. Database is deployed to a read-only filesystem, CD-ROM, or read-only mount point
  4. The .db-shm file exists but is owned by a different user with no group write permission
  5. SELinux or AppArmor policies deny write access to the .db-shm file specifically
  6. Database files were copied from a read-only source preserving restrictive permissions
  7. Running inside a read-only container or chroot environment
  8. Network filesystem caching or sync issues preventing shared memory access
  9. Database is bundled in application package with inherited read-only permissions
  10. Process user lacks write permissions to the directory containing database files

How to fix "SQLITE_READONLY_CANTLOCK: Unable to obtain a read lock"

1Verify .db-shm file existence and permissions

First, check whether the .db-shm file exists and what permissions it has. The shared memory file must be writable by the process accessing the database.

Check for existence of WAL-related files:

bash
# List all database-related files
ls -la /path/to/database.db*

# Example output (problematic permissions):
# -rw-r--r-- 1 user  group  8192 Dec 22 database.db
# -r--r--r-- 1 user  group   512 Dec 22 database.db-shm  <- READ-ONLY!
# -rw-r--r-- 1 user  group  4096 Dec 22 database.db-wal

Check if .db-shm is readable but not writable:

bash
# Test specific file permissions
ls -l /path/to/database.db-shm

# Check if process can write
if [ -w /path/to/database.db-shm ]; then
    echo "File is writable"
else
    echo "File is NOT writable - this is likely the problem"
fi

# Check directory permissions too
ls -ld /path/to/

Test with stat command for detailed information:

bash
# Get detailed permission info
stat /path/to/database.db-shm
stat /path/to/

# Look at "Access" permissions in output
2Fix .db-shm file permissions

If the .db-shm file exists but has read-only permissions, make it writable for the process that needs to access it.

Make .db-shm writable:

bash
# Add write permission to the shared memory file
chmod 664 /path/to/database.db-shm

# Verify the change
ls -l /path/to/database.db-shm
# Should now show: -rw-rw-r-- (0664)

Make all database files consistent:

bash
# Ensure consistent permissions on all WAL-related files
chmod 664 /path/to/database.db
chmod 664 /path/to/database.db-wal
chmod 664 /path/to/database.db-shm

# Verify all files
ls -l /path/to/database.db*

If ownership is the issue:

bash
# Check current ownership
ls -l /path/to/database.db*

# Change ownership to the user running the application
sudo chown myuser:mygroup /path/to/database.db*

# Also set directory ownership if needed
sudo chown myuser:mygroup /path/to/

# Verify
ls -l /path/to/

For application running as specific user (e.g., web server):

bash
# For nginx/www-data user
sudo chown www-data:www-data /path/to/database.db*
sudo chmod 664 /path/to/database.db*
sudo chmod 755 /path/to/

# Verify the www-data user can access
su - www-data -s /bin/bash -c "ls -la /path/to/database.db"

Check in application code if permissions need fixing:

python
import os
import stat

db_path = '/path/to/database.db'
shm_path = db_path + '-shm'

# Check if .db-shm is readable but not writable
if os.path.exists(shm_path):
    st = os.stat(shm_path)
    mode = stat.filemode(st.st_mode)
    print(f"Current permissions: {mode}")

    # Check if writable
    if not os.access(shm_path, os.W_OK):
        print("WARNING: .db-shm is not writable!")
        print("Try: chmod 664", shm_path)
3Ensure directory has write permissions

The directory containing the database must have write permissions so SQLite can create or modify the .db-shm file if needed.

Check directory permissions:

bash
# Check the directory's permissions
ls -ld /path/to/

# Good: drwxr-xr-x or drwxrwxr-x (755 or 775)
# Bad: dr-xr-xr-x (555 - read-only)

# Check if directory is writable by current user
if [ -w /path/to/ ]; then
    echo "Directory is writable"
else
    echo "Directory is NOT writable"
fi

Fix directory permissions:

bash
# Add write permission to the directory
chmod 755 /path/to/

# Or with group write access
chmod 775 /path/to/

# Verify
ls -ld /path/to/

Handle parent directory issues:

bash
# Sometimes parent directories need permissions too
# Check each level
ls -ld /path/
ls -ld /path/to/

# Make sure each level is accessible and writable
chmod 755 /path/
chmod 755 /path/to/

If directory is on read-only mount:

bash
# Check if filesystem is mounted read-only
mount | grep /path/

# Look for "ro" flag in output
# If found, the mount is read-only

# Remount as read-write (if you have permissions)
sudo mount -o remount,rw /path/

# Verify
mount | grep /path/
4Convert database from WAL mode to DELETE mode

If the database must be deployed to a read-only location, convert it to DELETE journal mode which doesn't require shared memory files. This is the most permanent solution for read-only deployments.

IMPORTANT: Perform this conversion on a writable copy of the database first, then deploy the converted version.

Convert using SQLite CLI:

bash
# First, make a backup
cp /path/to/database.db /path/to/database.db.backup

# Convert to DELETE mode (requires write access)
sqlite3 /path/to/database.db "PRAGMA journal_mode=DELETE;"

# Output should show: delete

# Verify the conversion
sqlite3 /path/to/database.db "PRAGMA journal_mode;"

# Checkpoint and cleanup
sqlite3 /path/to/database.db "PRAGMA wal_checkpoint(TRUNCATE);"
sqlite3 /path/to/database.db "VACUUM;"

# Remove the WAL files (they're no longer needed)
rm -f /path/to/database.db-wal
rm -f /path/to/database.db-shm

Convert using Python:

python
import sqlite3
import os

db_path = '/path/to/database.db'

# Create backup first
import shutil
shutil.copy(db_path, db_path + '.backup')

try:
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    # Convert to DELETE mode
    cursor.execute("PRAGMA journal_mode=DELETE")
    mode = cursor.fetchone()[0]
    print(f"Journal mode: {mode}")

    # Checkpoint to clean up WAL
    cursor.execute("PRAGMA wal_checkpoint(TRUNCATE)")
    result = cursor.fetchone()
    print(f"Checkpoint: busy={result[0]}, log_size={result[1]}, checkpointed={result[2]}")

    # Optimize database
    cursor.execute("VACUUM")
    print("VACUUM complete")

    conn.close()

    # Remove WAL files
    for ext in ['-wal', '-shm']:
        path = db_path + ext
        if os.path.exists(path):
            os.remove(path)
            print(f"Removed {path}")

    print("Database converted successfully")
except Exception as e:
    print(f"Error: {e}")
    # Restore backup on failure
    shutil.copy(db_path + '.backup', db_path)

Convert using Node.js (better-sqlite3):

javascript
const Database = require('better-sqlite3');
const fs = require('fs');
const path = require('path');

const dbPath = '/path/to/database.db';

// Backup first
fs.copyFileSync(dbPath, dbPath + '.backup');

try {
    const db = new Database(dbPath);

    // Convert to DELETE mode
    const mode = db.pragma('journal_mode = DELETE');
    console.log('Journal mode:', mode);

    // Checkpoint
    const checkpoint = db.pragma('wal_checkpoint(TRUNCATE)');
    console.log('Checkpoint:', checkpoint);

    // Vacuum
    db.pragma('vacuum');
    console.log('VACUUM complete');

    db.close();

    // Remove WAL files
    ['-wal', '-shm'].forEach(ext => {
        const p = dbPath + ext;
        if (fs.existsSync(p)) {
            fs.unlinkSync(p);
            console.log('Removed ' + path.basename(p));
        }
    });

    console.log('Database converted successfully');
} catch (error) {
    console.error('Error:', error);
    fs.copyFileSync(dbPath + '.backup', dbPath);
}

After conversion, the database can be safely deployed to read-only media as a single .db file.

5Remove or delete problematic .db-shm file and recreate

If the .db-shm file is corrupted or has incorrect permissions, deleting it and allowing SQLite to recreate it can fix the issue.

WARNING: Only do this when no other connections to the database exist.

Delete and recreate the shared memory file:

bash
# First, ensure no processes are using the database
# Use lsof to check
lsof | grep database.db

# If database is not in use, delete the .db-shm file
rm -f /path/to/database.db-shm

# SQLite will recreate it with proper permissions when next opened
# Try opening the database
sqlite3 /path/to/database.db "PRAGMA integrity_check;"

# Check if .db-shm was recreated
ls -l /path/to/database.db-shm

Before deleting, verify no processes hold locks:

bash
# Check which processes have database file open
lsof /path/to/database.db

# If processes are listed, stop them first
# Example for Node.js app
pkill -f "node app.js"

# Or graceful restart for web server
sudo systemctl restart nginx
sudo systemctl restart apache2

# Then delete and recreate
rm -f /path/to/database.db-shm

Script to safely handle .db-shm recreation:

python
import os
import sqlite3
import time

db_path = '/path/to/database.db'
shm_path = db_path + '-shm'

# Check if process is using the database
def check_process_lock(db_path):
    try:
        conn = sqlite3.connect(db_path, timeout=1.0)
        conn.execute("PRAGMA query_only=1")
        conn.close()
        return False
    except sqlite3.OperationalError as e:
        if 'database is locked' in str(e):
            return True
        raise

# If safe, delete and recreate
try:
    if check_process_lock(db_path):
        print("Database is locked - wait for processes to finish")
        time.sleep(5)

    # Delete the .db-shm file
    if os.path.exists(shm_path):
        os.remove(shm_path)
        print(f"Deleted {shm_path}")

    # Open database to trigger recreation
    conn = sqlite3.connect(db_path)
    conn.execute("PRAGMA integrity_check")
    conn.close()

    # Verify recreation
    if os.path.exists(shm_path):
        st = os.stat(shm_path)
        mode = oct(st.st_mode)[-3:]
        print(f".db-shm recreated with permissions {mode}")

except Exception as e:
    print(f"Error: {e}")
6Handle SELinux or AppArmor restrictions

On systems with mandatory access control (MAC), SELinux or AppArmor may specifically deny write access to the .db-shm file even with correct Unix permissions.

Check SELinux status and context:

bash
# Check if SELinux is enabled
getenforce
# Output: Enforcing, Permissive, or Disabled

# Check SELinux context of database files
ls -Z /path/to/database.db*

# Look for context like:
# unconfined_u:object_r:user_home_t:s0 database.db
# unconfined_u:object_r:user_home_t:s0 database.db-shm

Fix SELinux context for database files:

bash
# Set appropriate context for web application
# For Apache/httpd
sudo chcon -t httpd_sys_rw_content_t /path/to/database.db*
sudo chcon -t httpd_sys_rw_content_t /path/to/

# For systemd service
sudo chcon -t admin_home_t /path/to/database.db*

# Or use semanage for persistent rules
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/path/to/database(/.*)?"
sudo restorecon -R /path/to/

# Verify changes
ls -Z /path/to/database.db*

Check for SELinux denials:

bash
# Check audit logs for recent SELinux denials
sudo ausearch -m avc -ts recent | grep database

# If denials found, generate policy
sudo ausearch -m avc -ts recent | audit2allow -M database_policy
sudo semodule -i database_policy.pp

# List active policies
sudo semodule -l | grep database

Check AppArmor status:

bash
# Check if AppArmor is active
sudo aa-status

# Check your application's profile
sudo aa-status | grep "your-app"

# See denials
sudo tail -f /var/log/audit/audit.log | grep APPARMOR

Modify AppArmor profile:

bash
# Edit the profile (example for Node.js)
sudo nano /etc/apparmor.d/usr.bin.node

# Add these lines to allow database access:
# /path/to/database.db rw,
# /path/to/database.db-wal rw,
# /path/to/database.db-shm rw,
# /path/to/ r,

# Reload the profile
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.node

# For testing, use complain mode (logs but doesn't enforce)
sudo aa-complain /usr/bin/node

# Check for denials
sudo tail -f /var/log/audit/audit.log
7Configure proper permissions for containerized deployments

In Docker and other containerized environments, ensure the database directory has correct permissions and ownership matching the container user.

Docker with volume mounts:

dockerfile
# Dockerfile
FROM node:18

WORKDIR /app
COPY package*.json ./
RUN npm install

COPY . .

# Create database directory with proper permissions
RUN mkdir -p /app/data && chmod 755 /app/data

# Copy pre-converted database (DELETE mode, not WAL)
# COPY database.db /app/data/
# RUN chmod 644 /app/data/database.db

# Run as non-root user
RUN useradd -m dbuser
USER dbuser

CMD ["node", "server.js"]

Docker Compose with persistent volume:

yaml
version: '3.8'
services:
  app:
    build: .
    volumes:
      # Mount database directory as volume
      - ./database:/app/data
    environment:
      - DATABASE_PATH=/app/data/database.db
    user: "node"  # Run as specific user

Fix permissions from host:

bash
# Before running container, ensure database directory on host is writable
chmod 755 ./database
chmod 644 ./database/database.db

# Or use Docker volume with specific permissions
docker volume create --opt type=tmpfs --opt device=tmpfs --opt o=rw my_db_vol

Kubernetes with proper permissions:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
  - name: app
    image: myapp:latest
    volumeMounts:
    - name: database-storage
      mountPath: /app/data
    securityContext:
      runAsUser: 1000
      runAsGroup: 1000
      fsGroup: 1000  # Ensures volume permissions match pod user
  volumes:
  - name: database-storage
    persistentVolumeClaim:
      claimName: database-pvc

Systemd service with StateDirectory:

ini
[Unit]
Description=Database Application
After=network.target

[Service]
Type=simple
User=dbapp
Group=dbapp

# Use StateDirectory to auto-create with correct permissions
StateDirectory=dbapp
# This creates /var/lib/dbapp owned by dbapp:dbapp

WorkingDirectory=/opt/dbapp
Environment="DATABASE_PATH=/var/lib/dbapp/database.db"

ExecStart=/usr/bin/node server.js

[Install]
WantedBy=multi-user.target

Advanced notes

SQLite WAL Locking Mechanism:

Understanding how SQLite's WAL locking works helps explain why write access is needed even for read-only connections.

SQLite uses specific bytes within the .db-shm shared memory file to implement locks:
- Bytes 0-7: Header (version, magic bytes)
- Byte 8: Lock index
- Bytes 9-15: Reader slots (each reader claims a slot to indicate it's active)

Even a pure reader must atomically write to its assigned slot to claim it, which requires write access.

Lock Types in WAL Mode:

1. READER Lock: Claimed by reading transactions. Requires atomic write to .db-shm.
2. WRITER Lock: Claimed by writing transactions
3. CKPT Lock: Claimed by checkpointing process

The SQLITE_READONLY_CANTLOCK error specifically means: "I tried to claim a READER lock but can't write to the .db-shm file."

Why Not Use Read-Only Mode?

SQLite's documentation states: "It is not practical to open read-only WAL databases." This is because:
1. The shared memory file must exist for WAL coordination
2. Creating the .db-shm requires write access to the directory
3. Updating reader slots requires atomic writes to .db-shm
4. No truly read-only mechanism exists that's safe for concurrent access

The only safe way to deploy a SQLite database read-only is to:
1. Convert to DELETE journal mode first
2. Remove .db-wal and .db-shm files
3. Deploy only the single .db file
4. Open with read-only flags

Comparison with Other Error Codes:

- SQLITE_READONLY_RECOVERY: Occurs during WAL recovery (replay of uncommitted changes). Requires write for recovery.
- SQLITE_READONLY_CANTLOCK: Occurs before recovery (can't even acquire the basic read lock). Indicates fundamental permission issue with .db-shm.
- SQLITE_READONLY_DBMOVED: Database file was moved to another location (different filesystem). Lock file invalid.
- SQLITE_READONLY_DIRECTORY: Directory containing database is read-only.
- SQLITE_READONLY_CANTINIT: Can't initialize WAL shared memory file.

Debugging WAL Lock Issues:

Enable SQLite debug output:

python
import sqlite3
import logging

# Check SQLite version and compile options
conn = sqlite3.connect(':memory:')
version = sqlite3.version
print(f"SQLite version: {version}")

# Check compile options (requires appropriate SQLite build)
cursor = conn.cursor()
cursor.execute("PRAGMA compile_options")
for row in cursor.fetchall():
    if 'WAL' in row[0]:
        print(f"Compile option: {row[0]}")

Platform-Specific Notes:

- Linux with NFS: Shared memory locking may fail. Use local storage.
- Windows with OneDrive/Dropbox: Real-time sync can interfere with WAL locks.
- macOS with iCloud sync: Similar sync issues. Use local Documents folder.
- Docker on Windows/Mac: Host filesystem behavior can cause lock failures.
- Mobile platforms (iOS/Android): WAL on read-only app bundles always fails.

Performance Trade-offs:

WAL mode vs DELETE mode for permission-sensitive deployments:

| Aspect | WAL Mode | DELETE Mode |
|--------|----------|-------------|
| Permissions required | Write access to .db-shm (complex) | Write access to directory during transaction (simpler) |
| Read-only deployment | Impossible/impractical | Fully supported |
| Concurrent readers | Excellent (no blocking) | Poor (readers block writers) |
| Storage files | 3 files (.db, .db-wal, .db-shm) | 1 file (.db) or 2 during transaction |
| Recovery after crash | WAL replay automatic | Journal rollback automatic |

When to Convert to DELETE Mode:

1. Database deployed to read-only media or mountpoint
2. Running in restricted containers or sandboxes
3. Need database as single portable file
4. Deploying to iOS/Android app bundles
5. Using network filesystems where WAL locks fail
6. Permission complexity outweighs concurrency benefits

Frequently asked questions

What does "SQLITE_READONLY_CANTLOCK: Unable to obtain a read lock" mean?
db-shm) is read-only or inaccessible. Understanding WAL Mode and Locking: SQLite's WAL (Write-Ahead Logging) mode uses a three-file architecture: 1. db: The main database file 2.
How do I fix "SQLITE_READONLY_CANTLOCK: Unable to obtain a read lock"?
The SQLITE_READONLY_CANTLOCK error occurs when SQLite cannot obtain a read lock on a WAL (Write-Ahead Logging) mode database because the shared-memory file is read-only. This typically happens when file permissions prevent SQLite from accessing or modifying the .db-shm file needed for WAL coordination.
What causes "SQLITE_READONLY_CANTLOCK: Unable to obtain a read lock"?
The .db-shm file has read-only permissions (mode 0444 or similar) The directory containing the database is read-only and prevents .db-shm modification Database is deployed to a read-only filesystem, CD-ROM, or read-only mount point
Is the "SQLITE_READONLY_CANTLOCK: Unable to obtain a read lock" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading