SQLite-backed Durable Object Storage
Last updated Sep 21, 2026 Note
This page documents the storage API for the newer SQLite-backed Durable Objects.
For the legacy KV-backed Durable Object storage API, refer to KV-backed Durable Object Storage (Legacy).
The Durable Object Storage API allows Durable Objects to access transactional and strongly consistent storage. A Durable Object's attached storage is private to its unique instance and cannot be accessed by other objects.
The Durable Object Storage API comes with several methods, including SQL, point-in-time recovery (PITR), key-value (KV), and alarm APIs. Available API methods depend on the storage backend for a Durable Objects class, either SQLite or KV.
| Methods <sup>1</sup> | SQLite-backed Durable Object class | KV-backed Durable Object class |
|---|---|---|
| SQL API | ✅ | ❌ |
| PITR API | ✅ | ❌ |
| Synchronous KV API | ✅ <sup>2, 3</sup> | ❌ |
| Asynchronous KV API | ✅ <sup>3</sup> | ✅ |
| Alarms API | ✅ | ✅ |
<details>
<summary>
Footnotes
</summary>
<sup>1</sup> Each method is implicitly wrapped inside a transaction, such that its results are atomic and isolated from all other storage operations, even when accessing multiple key-value pairs.
<sup>2</sup> KV API methods like <code>get()</code>, <code>put()</code>, <code>delete()</code>, or <code>list()</code> store data in a hidden SQLite table <code>__cf_kv</code>. Note that you will be able to view this table when listing all tables, but you will not be able to access its content through the SQL API.
<sup>3</sup> SQLite-backed Durable Objects also use <a href="https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/#synchronous-kv-api">synchronous KV API methods</a> using <code>ctx.storage.kv</code>, whereas KV-backed Durable Objects only provide <a href="https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/#asynchronous-kv-api">asynchronous KV API methods</a>.
</details>
Recommended SQLite-backed Durable Objects
Cloudflare recommends all new Durable Object namespaces use the SQLite storage backend. These Durable Objects can continue to use storage key-value API.
Additionally, SQLite-backed Durable Objects allow you to store more types of data (such as tables), and offer Point In Time Recovery API which can restore a Durable Object's embedded SQLite database contents (both SQL data and key-value data) to any point in the past 30 days.
Creating new namespaces with the key-value storage backend is no longer supported for accounts without an existing key-value-backed namespace. The key-value storage backend remains available for existing namespaces, and a migration path from the key-value storage backend to the SQLite storage backend will be available in the future.
Storage billing on SQLite-backed Durable Objects
Storage billing for SQLite-backed Durable Objects will be enabled in January 2026, with a target date of January 7, 2026 (no earlier). Only SQLite storage usage on and after the billing target date will incur charges. For more information, refer to Billing for SQLite Storage.
Access storage
Durable Objects gain access to Storage API via the DurableObjectStorage interface and accessed by the DurableObjectState::storage property. This is frequently accessed via this.ctx.storage with the ctx parameter passed to the Durable Object constructor.
The following code snippet shows you how to store and retrieve data using the Durable Object Storage API.
export class Counter extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
}
async increment() {
let value = (await this.ctx.storage.get("value")) || 0;
value += 1;
await this.ctx.storage.put("value", value);
return value;
}
}
export class Counter extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
}
async increment(): Promise<number> {
let value: number = (await this.ctx.storage.get('value')) || 0;
value += 1;
await this.ctx.storage.put('value', value);
return value;
}
}
from workers import DurableObject
class Counter(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
async def increment(self):
value = (await self.ctx.storage.get('value')) or 0
value += 1
await self.ctx.storage.put('value', value)
return value
JavaScript is a single-threaded and event-driven programming language. This means that JavaScript runtimes, by default, allow requests to interleave with each other which can lead to concurrency bugs. The Durable Objects runtime uses a combination of input gates and output gates to avoid this type of concurrency bug when performing storage operations. Learn more in our blog post ↗︎.
SQL API
The SqlStorage interface encapsulates methods that modify the SQLite database embedded within a Durable Object. The SqlStorage interface is accessible via the sql property of DurableObjectStorage class.
For example, using sql.exec() a user can create a table and insert rows.
import { DurableObject } from "cloudflare:workers";
export class MyDurableObject extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
this.sql.exec(`
CREATE TABLE IF NOT EXISTS artist(
artistid INTEGER PRIMARY KEY,
artistname TEXT
);
INSERT INTO artist (artistid, artistname) VALUES
(123, 'Alice'),
(456, 'Bob'),
(789, 'Charlie');
`);
}
}
from workers import DurableObject
class MyDurableObject(DurableObject):
def __init__(self, ctx, env):
super().__init__(ctx, env)
self.sql = ctx.storage.sql
self.sql.exec("""
CREATE TABLE IF NOT EXISTS artist(
artistid INTEGER PRIMARY KEY,
artistname TEXT
);
INSERT INTO artist (artistid, artistname) VALUES
(123, 'Alice'),
(456, 'Bob'),
(789, 'Charlie');
""")
- SQL API methods accessed with
ctx.storage.sqlare only allowed on Durable Object classes with SQLite storage backend and will return an error if called on Durable Object classes with a KV-storage backend. - When writing data, every row update of an index counts as an additional row. However, indexes may be beneficial for read-heavy use cases. Refer to Index for SQLite Durable Objects.
- Writing data to SQLite virtual tables ↗︎ also counts towards rows written.
Durable Objects support a subset of SQLite extensions for added functionality, including:
- FTS5 module ↗︎ for full-text search (including
fts5vocab). - JSON extension ↗︎ for JSON functions and operators.
- Math functions ↗︎.
Refer to the source code ↗︎ for the full list of supported functions.
exec
exec(query: string, ...bindings: any[]): SqlStorageCursor
Parameters
query:string- The SQL query string to be executed.
querycan contain?placeholders for parameter bindings. Multiple SQL statements, separated with a semicolon, can be executed in thequery. With multiple SQL statements, any parameter bindings are applied to the last SQL statement in thequery, and the returned cursor is only for the last SQL statement. ...bindings:any[]Optional- Optional variable number of arguments that correspond to the
?placeholders inquery.
Returns
A cursor (SqlStorageCursor) to iterate over query row results as objects. SqlStorageCursor is a JavaScript Iterable ↗︎, which supports iteration using for (let row of cursor). SqlStorageCursor is also a JavaScript Iterator ↗︎, which supports iteration using cursor.next().
Consume cursors synchronously
Although a cursor object can technically be held across an await, it does not provide a stable snapshot of the query results. A cursor resumed after an await may observe rows inserted, updated, or deleted after the cursor was created, including writes from a later implicit transaction that has not yet committed. If that later transaction rolls back, the cursor may have already returned data from a state that never became durable.
For predictable behavior, fully consume cursors synchronously before the next await, for example with .toArray() or Array.from(cursor). Treat cursors that cross await boundaries as having no snapshot isolation guarantees.
// ✅ Safe: cursor is fully consumed before any await
const rows = this.ctx.storage.sql.exec("SELECT * FROM users").toArray();
const result = await fetch("https://example.com", { method: "POST", body: JSON.stringify(rows) });
// ⚠️ No snapshot isolation: cursor may reflect changes made after it was created
const cursor = this.ctx.storage.sql.exec("SELECT * FROM users");
await fetch("https://example.com/notify");
// Rows returned here may include writes that occurred during the await,
// including uncommitted data from a later implicit transaction.
const rows = cursor.toArray();
SqlStorageCursor supports the following methods:
next()- Returns an object representing the next value of the cursor. The returned object has
doneandvalueproperties adhering to the JavaScript Iterator ↗︎.doneis set tofalsewhen a next value is present, andvalueis set to the next row object in the query result.doneis set totruewhen the entire cursor is consumed, and novalueis set. toArray()- Iterates through remaining cursor value(s) and returns an array of returned row objects.
one()- Returns a row object if query result has exactly one row. If query result has zero rows or more than one row,
one()throws an exception. raw():Iterator- Returns an Iterator over the same query results, with each row as an array of column values (with no column names) rather than an object.
- Returned Iterator supports
next()andtoArray()methods above. - Returned cursor and
raw()iterator iterate over the same query results and can be combined. For example:
let cursor = this.sql.exec("SELECT * FROM artist ORDER BY artistname ASC;");
let rawResult = cursor.raw().next();
if (!rawResult.done) {
console.log(rawResult.value); // prints [ 123, 'Alice' ]
} else {
// query returned zero results
}
console.log(cursor.toArray()); // prints [{ artistid: 456, artistname: 'Bob' },{ artistid: 789, artistname: 'Charlie' }]
cursor = self.sql.exec("SELECT * FROM artist ORDER BY artistname ASC;")
raw_result = cursor.raw().next()
if not raw_result.done:
print(raw_result.value) # prints [ 123, 'Alice' ]
else:
# query returned zero results
pass
print(cursor.toArray()) # prints [{ artistid: 456, artistname: 'Bob' },{ artistid: 789, artistname: 'Charlie' }]
SqlStorageCursor has the following properties:
columnNames:string[]- The column names of the query in the order they appear in each row array returned by the
rawiterator. rowsRead:number- The number of rows read so far as part of this SQL
query. This may increase as you iterate the cursor. The final value is used for SQL billing. rowsWritten:number- The number of rows written so far as part of this SQL
query. This may increase as you iterate the cursor. The final value is used for SQL billing. - Any numeric value in a column is affected by JavaScript's 52-bit precision for numbers. If you store a very large number (in
int64), then retrieve the same value, the returned value may be less precise than your original number.
