Skip to main content
DatabasePool provides concurrent database access with a pool of read-only connections and a single write connection.

Overview

DatabasePool opens multiple connections to an SQLite database, enabling concurrent reads while maintaining serialized writes. It uses SQLite’s WAL mode for optimal concurrency.
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
Use DatabasePool when you need:
  • Concurrent read access from multiple threads
  • Maximum read throughput
  • Production applications with high concurrency
For simpler serial access, use DatabaseQueue.

Initialization

Opening a Database File

path
String
required
The path to the database file
configuration
Configuration
default:"Configuration()"
Optional database configuration
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")

// With configuration
var config = Configuration()
config.maximumReaderCount = 10
let dbPool = try DatabasePool(path: dbPath, configuration: config)
DatabasePool automatically enables WAL mode on the database.

Properties

configuration
Configuration
The database configuration
path
String
The path to the database file

Reading from the Database

read(_:)

Reads from the database using a reader connection.
value
(Database) throws -> T
required
A closure that accesses the database
let players = try dbPool.read { db in
    try Player.fetchAll(db)
}
Reads execute concurrently and don’t block each other or writes.

asyncRead(_:)

Asynchronously reads from the database.
dbPool.asyncRead { result in
    switch result {
    case .success(let db):
        let count = try Player.fetchCount(db)
        print("Player count: \(count)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

asyncConcurrentRead(_:)

Performs an asynchronous concurrent read that sees the database state at the moment this method is called.
This method must be called from the writer dispatch queue, outside of any transaction.
try dbPool.asyncWriteWithoutTransaction { db in
    try Player.deleteAll(db)
    
    // Count players concurrently (guaranteed to be zero)
    dbPool.asyncConcurrentRead { dbResult in
        let count = try dbResult.get().fetchOne(sql: "SELECT COUNT(*) FROM player")
        print(count) // 0
    }
    
    try Player(name: "Arthur").insert(db)
}

unsafeRead(_:)

Reads from the database without transaction isolation.
let value = try dbPool.unsafeRead { db in
    try Player.fetchOne(db, key: 1)
}

Writing to the Database

write(_:)

Synchronously writes to the database in an implicit transaction.
updates
(Database) throws -> T
required
A closure that updates the database
try dbPool.write { db in
    try Player(name: "Arthur", score: 100).insert(db)
    try Player(name: "Barbara", score: 50).insert(db)
}

writeInTransaction(::)

Executes operations in an explicit transaction.
try dbPool.writeInTransaction { db in
    try Player(name: "Arthur").insert(db)
    try Player(name: "Barbara").insert(db)
    return .commit
}

writeWithoutTransaction(_:)

Writes to the database without an implicit transaction.
try dbPool.writeWithoutTransaction { db in
    try db.execute(sql: "PRAGMA optimize")
}

barrierWriteWithoutTransaction(_:)

Writes to the database and waits for all concurrent reads to complete first.
try dbPool.barrierWriteWithoutTransaction { db in
    // No concurrent reads are running
    try db.execute(sql: "VACUUM")
}
This method blocks until all concurrent reads complete, which may take time.

Async/Await Support

// Async read
let players = try await dbPool.read { db in
    try Player.fetchAll(db)
}

// Async write
try await dbPool.write { db in
    try Player(name: "Arthur").insert(db)
}

Database Snapshots

makeSnapshot()

Creates a snapshot that provides a stable, immutable view of the database.
let snapshot = try dbPool.makeSnapshot()

// The snapshot sees the database as it was when created
let players = try snapshot.read { db in
    try Player.fetchAll(db)
}
Do not call makeSnapshot() from inside a transaction. This will raise a fatal error.

Memory Management

releaseMemory()

Synchronously frees as much memory as possible.
dbPool.releaseMemory()

releaseMemoryEventually()

Asynchronously frees memory without blocking current operations.
dbPool.releaseMemoryEventually()
On iOS, GRDB automatically manages memory when the app enters the background or receives memory warnings.

invalidateReadOnlyConnections()

Closes all read-only connections. New connections will be created as needed.
dbPool.invalidateReadOnlyConnections()

Database Interruption

interrupt()

Cancels any running database operations.
dbPool.interrupt()

Closing the Database

close()

Closes all database connections.
try dbPool.close()
Once closed, the database pool cannot be reopened. Create a new instance instead.

WAL Checkpoints

Since DatabasePool uses WAL mode, you can manually checkpoint the WAL file:
try dbPool.write { db in
    try db.checkpoint(.passive)
}

Configuration Options

Reader Count

var config = Configuration()
config.maximumReaderCount = 5 // Default is based on active CPUs
let dbPool = try DatabasePool(path: dbPath, configuration: config)

Persistent Connections

var config = Configuration()
config.persistentReadOnlyConnections = true // Keep connections alive
let dbPool = try DatabasePool(path: dbPath, configuration: config)

Performance Tips

Use DatabasePool for production apps that need concurrent reads. The overhead is minimal, and the performance gains are significant.
Reads are non-blocking and can execute in parallel. Writes are serialized but don’t block reads thanks to WAL mode.
Avoid long-running read transactions as they can prevent the WAL file from being checkpointed.

See Also

Build docs developers (and LLMs) love