A Connection Is Just an Open File
SQLite has no server. There's no daemon listening on a port, no host to dial, no credentials to negotiate. "Connecting" means your driver opens a file on disk and starts reading and writing pages of it. That's the whole mental model.
Every language has a driver that wraps the SQLite C library. The shapes differ, but the moving parts are the same: a path to the database file, an open call, a handle you run statements on, and a close call when you're done.
-- Conceptually, every driver does this:
-- 1. Open or create the file at the given path.
-- 2. Acquire a handle.
-- 3. Run SQL via prepared statements.
-- 4. Close the handle.
The rest of this page is what that looks like in real code, and the few settings worth setting before your first query.
Python: sqlite3 in the Standard Library
Python ships with sqlite3 — no install needed. The basic shape:
-- Python
import sqlite3
conn = sqlite3.connect("app.db")
conn.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
conn.execute("INSERT INTO notes (body) VALUES (?)", ("first note",))
conn.commit()
for row in conn.execute("SELECT id, body FROM notes"):
print(row)
conn.close()
A few things worth knowing:
sqlite3.connect("app.db")creates the file if it doesn't exist. Pass":memory:"for a database that lives only in RAM.sqlite3.connect("file:app.db?mode=ro", uri=True)opens read-only via the URI form.- The
?in the SQL is a placeholder — use parameter binding, never string concatenation. The next chapter goes deeper on that. conn.commit()is required unless you use a context manager (with conn:) which auto-commits.
For a long-running app, set a busy timeout so concurrent writers wait instead of erroring:
-- Python
conn.execute("PRAGMA busy_timeout = 5000") -- wait up to 5s
conn.execute("PRAGMA journal_mode = WAL") -- better concurrency
Node.js: better-sqlite3
The Node ecosystem has a few options, but better-sqlite3 is the one most teams reach for. It's synchronous (which sounds wrong for Node but is actually faster for SQLite, since the queries return in microseconds).
-- Node.js
const Database = require("better-sqlite3");
const db = new Database("app.db");
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
const insert = db.prepare("INSERT INTO notes (body) VALUES (?)");
insert.run("first note");
const rows = db.prepare("SELECT id, body FROM notes").all();
console.log(rows);
db.close();
db.prepare(...) returns a reusable statement object. .run() is for writes, .all() returns all rows, .get() returns one. Same pattern as most SQL drivers.
Set pragmas on startup:
-- Node.js
db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000");
db.pragma("foreign_keys = ON"); -- off by default, almost always wanted
foreign_keys = ON is worth calling out: SQLite doesn't enforce foreign keys unless you ask it to, per connection. If you forget, your REFERENCES clauses are decorative.
Go: database/sql with a Driver
Go's standard database/sql package is driver-agnostic. For SQLite, modernc.org/sqlite (pure Go, no CGO) and github.com/mattn/go-sqlite3 (CGO) are the common choices.
-- Go
import (
"database/sql"
_ "modernc.org/sqlite"
)
db, err := sql.Open("sqlite", "app.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)")
if err != nil { panic(err) }
defer db.Close()
_, err = db.Exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
_, err = db.Exec("INSERT INTO notes (body) VALUES (?)", "first note")
rows, _ := db.Query("SELECT id, body FROM notes")
defer rows.Close()
for rows.Next() {
var id int; var body string
rows.Scan(&id, &body)
fmt.Println(id, body)
}
The query string after the file name is how this driver passes pragmas at connection time — the format varies by driver, so check the docs for whichever one you pick.
sql.Open doesn't actually open a connection; the first query does. db is a connection pool. For SQLite, a small pool (or even db.SetMaxOpenConns(1) for write-heavy workloads) is usually right.
Java: JDBC
The org.xerial:sqlite-jdbc driver is the standard. JDBC URLs look like jdbc:sqlite:<path>:
-- Java
import java.sql.*;
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:app.db")) {
try (Statement st = conn.createStatement()) {
st.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
st.execute("PRAGMA journal_mode = WAL");
st.execute("PRAGMA busy_timeout = 5000");
}
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO notes (body) VALUES (?)")) {
ps.setString(1, "first note");
ps.executeUpdate();
}
try (PreparedStatement ps = conn.prepareStatement("SELECT id, body FROM notes");
ResultSet rs = ps.executeQuery()) {
while (rs.next()) System.out.println(rs.getInt(1) + " " + rs.getString(2));
}
}
In-memory: jdbc:sqlite::memory:. Read-only: append ?open_mode=1 or use a SQLiteConfig object.
PHP: PDO
PDO's SQLite DSN is sqlite:<path>:
-- PHP
$db = new PDO("sqlite:app.db");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
$db->exec("PRAGMA journal_mode = WAL");
$db->exec("PRAGMA busy_timeout = 5000");
$stmt = $db->prepare("INSERT INTO notes (body) VALUES (?)");
$stmt->execute(["first note"]);
foreach ($db->query("SELECT id, body FROM notes") as $row) {
echo $row["id"] . " " . $row["body"] . "\n";
}
sqlite::memory: for an in-memory database. Always set ATTR_ERRMODE to exceptions — silent failures are rough to debug.
Connection Strings and File Paths
Across drivers, you'll see two flavors of "connection string":
- Plain path:
app.db,./data/app.db,/var/lib/myapp/app.db. Relative paths are relative to the process's working directory — which is rarely what you want in production. Prefer absolute paths. - URI form:
file:app.db?mode=rwc&cache=shared. Lets you set flags likemode=ro(read-only),mode=rwc(read-write-create, the default),cache=shared, andnolock=1.
Special values you'll meet:
:memory:— a private in-memory database. Each connection gets its own.file::memory:?cache=shared— an in-memory database that multiple connections in the same process can share.""(empty string) — a private, temporary on-disk database that's deleted when closed.
JDBC prefixes the URI with jdbc:sqlite:. PDO uses sqlite:. Go drivers and Python's sqlite3 accept the path or URI directly.
What About Connection Pools?
SQLite is a single-writer database. At any moment, exactly one connection holds the write lock; everyone else waits. Pooling many writers doesn't make writes faster — it just gives you more contenders for the same lock.
That said, a small pool is useful for:
- Concurrent reads in WAL mode, where readers don't block each other or the writer.
- Avoiding head-of-line blocking where one slow query stalls the whole app.
Reasonable defaults for a web app:
- WAL mode on.
busy_timeoutof a few seconds so contention waits politely instead of erroring.- A pool size of 1 writer + N readers, or just one shared connection if traffic is light.
- Foreign keys turned on, on every connection.
-- Apply these on every new connection:
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL; -- safe with WAL; faster than FULL
synchronous = NORMAL is the typical WAL pairing — durable across app crashes, slightly looser across OS crashes, noticeably faster than the default FULL.
Closing Connections (and Why It Matters)
Every driver has a close call: conn.close(), db.Close(), db.close(). Not closing leaks file descriptors and can leave the WAL file growing.
In long-running services, the more common pattern is one connection (or pool) for the lifetime of the process, not open-close around each request. Opening a SQLite connection is cheap, but reapplying pragmas every time is wasteful and easy to forget.
-- Python — connection per process, reused across requests
DB = sqlite3.connect("app.db", check_same_thread=False)
DB.execute("PRAGMA journal_mode = WAL")
DB.execute("PRAGMA busy_timeout = 5000")
DB.execute("PRAGMA foreign_keys = ON")
For Python specifically, check_same_thread=False is needed if you'll use the connection from multiple threads — and you'll want a lock or a pool to serialize calls.
A Checklist Before Shipping
Before pointing real traffic at a SQLite database:
- Use an absolute path for the database file.
- Enable WAL mode (
PRAGMA journal_mode = WAL). - Set a
busy_timeoutof 2-10 seconds. - Turn foreign keys on, every connection.
- Use prepared statements with parameter binding — never string interpolation.
- Make sure the directory containing the database is writable by the process (SQLite writes a
-waland-shmfile alongside the main file in WAL mode). - Think about backups before you need them —
VACUUM INTOand the.backupcommand are covered later.
Next: Migrations
Connecting is the easy part. The harder part is evolving your schema over time without hand-editing production databases. Migrations are how you turn ALTER TABLE into a repeatable, version-controlled process — that's the next page.
Frequently Asked Questions
How do I connect to a SQLite database from code?
Point your driver at a file path. In Python that's sqlite3.connect('app.db'); in Node new Database('app.db') with better-sqlite3; in Go sql.Open("sqlite", "app.db"). SQLite doesn't have a server, so the 'connection' is really just opening a file — if it doesn't exist, SQLite creates it.
What does a SQLite connection string look like?
Most drivers accept either a plain file path (./data/app.db) or a URI form (file:app.db?mode=rwc&cache=shared). The URI form lets you set flags like read-only mode, shared cache, or :memory: databases. JDBC uses jdbc:sqlite:app.db; PDO uses sqlite:app.db.
Do I need a connection pool with SQLite?
Usually not in the same way as Postgres or MySQL. SQLite serializes writes at the database level, so a pool of writers doesn't speed anything up. A small pool helps for concurrent reads, especially in WAL mode. Many apps run fine with a single shared connection plus PRAGMA journal_mode=WAL and a sensible busy_timeout.
How do I avoid 'database is locked' errors?
Set a busy timeout so your driver waits instead of failing immediately: PRAGMA busy_timeout = 5000 (milliseconds). Enable WAL mode with PRAGMA journal_mode=WAL so readers don't block writers. Keep transactions short, and don't hold a write transaction open while doing slow non-database work.