transact-rs/sqlx · critical · io::Error (OutOfMemory)
SQLite is unable to allocate memory to hold the sqlite3 obje
Error message
SQLite is unable to allocate memory to hold the sqlite3 object
What it means
`sqlite3_open_v2` returned success-path memory that is `NULL`, meaning SQLite could not allocate the `sqlite3` handle object. sqlx checks this even when the returned status might otherwise look plausible, and raises an `OutOfMemory` I/O error, closing the (uninitialized) handle path immediately.
Source
Thrown at sqlx-sqlite/src/connection/handle.rs:38
// enabled and [SQLITE_THREADSAFE] was enabled when sqlite was compiled. We refuse to work
// if these conditions are not upheld.
//
// <https://www.sqlite.org/c3ref/threadsafe.html>
// <https://www.sqlite.org/c3ref/c_config_covering_index_scan.html#sqliteconfigmultithread>
unsafe impl Send for ConnectionHandle {}
impl ConnectionHandle {
pub(crate) fn open(filename: &CStr, flags: c_int) -> Result<Self, Error> {
let mut handle = ptr::null_mut();
// <https://www.sqlite.org/c3ref/open.html>
let status = unsafe { sqlite3_open_v2(filename.as_ptr(), &mut handle, flags, ptr::null()) };
// SAFETY: the database is still initialized as long as the pointer is not `NULL`.
// We need to close it even if there's an error.
let mut handle = Self(NonNull::new(handle).ok_or_else(|| {
Error::Io(io::Error::new(
io::ErrorKind::OutOfMemory,
"SQLite is unable to allocate memory to hold the sqlite3 object",
))
})?);
if status != SQLITE_OK {
return Err(Error::Database(Box::new(handle.expect_error())));
}
// Enable extended result codes
// https://www.sqlite.org/c3ref/extended_result_codes.html
unsafe {
// This only returns a non-OK code if SQLite is built with `SQLITE_ENABLE_API_ARMOR`
// and the database pointer is `NULL` or already closed.
//
// The invariants of this type guarantee that neither is true.
sqlite3_extended_result_codes(handle.as_ptr(), 1);
}View on GitHub (pinned to 03af8bcc57)
Solutions
- Free memory in the process (drop caches/pools, fix leaks) and retry the connection.
- Raise the memory limit of the container/process (K8s memory limit, `ulimit -v`, JVM/other runtime heap caps).
- Reduce connection pool size (`max_connections`) so concurrent opens fit in available memory.
- If persistent, restart the process/host — this indicates the allocator could not satisfy a small allocation.
Example fix
// before
let pool = Pool::connect_with(
SqliteConnectOptions::new().filename("app.db")
).await?; // OOM under tight memory limits with max pool opens
// after
let pool = Pool::builder(
SqliteConnectOptions::new().filename("app.db")
)
.max_connections(2)
.build()
.await?; Defensive patterns
Strategy: retry
Validate before calling
fn can_attempt_db_open() -> bool {
// cheap heuristic: skip the attempt when the system is under severe memory pressure
!std::path::Path::new("/proc/meminfo").exists()
|| std::fs::read_to_string("/proc/meminfo")
.map(|s| !s.contains("MemAvailable: 0 kB"))
.unwrap_or(true)
} Type guard
// Rust has no runtime type to narrow for a failed allocation;
// model capacity instead.
fn pool_config_is_sane(max_connections: u32) -> bool {
max_connections > 0 && max_connections <= 64
} Try / catch
// no exceptions in Rust; wrap connection setup so OOM is not fatal to the process
let pool = std::panic::catch_unwind(|| {
tokio::runtime::Handle::current().block_on(
Pool::connect_with(opts.clone())
)
});
match pool {
Ok(Ok(p)) => p,
_ => { free_resources(); retry_with_backoff(); }
} Prevention
- Set pool `max_connections` well below what the container memory limit can sustain.
- Monitor memory (RSS/cgroup usage) and alert before hitting limits.
- Fix heap leaks in long-lived processes; cycle workers periodically.
- Avoid opening new pools in hot paths; reuse a single shared pool.
When it happens
Trigger: Opening any SQLite connection (`SqliteConnection::connect`, pools, `AnyDriver` backed by SQLite) when the process is out of memory or address space, or under extreme resource limits (ulimit, cgroup memory cap, containers with tight memory).
Common situations: Memory-constrained Docker/Kubernetes pods, CI runners with tiny RAM limits, memory leaks elsewhere in a long-lived process exhausting the heap before opening a new connection pool.
Related errors
- extension entrypoint names passed to SQLite must not contain
- extension names passed to SQLite must not contain nul bytes
- invalid column index: {}
- unimplemented!()
- expected to read {} bytes, got {} bytes at EOF
AI-assisted analysis of transact-rs/sqlx@03af8bcc57 (2026-09-03).
Data as JSON: /api/errors/d545ed7df0678594.
Report an issue: GitHub.