🙌

Durable Objects KV 違い

に公開

はじめに

Cloudflare Workersの永続化層にアクセスするためのDurable Object Storage APIにはSQL APIのほかに、Synchronous KV APIとAsynchronous KV APIという、非常に似た2つのKV APIが存在するため、まとめてみることにしました。

3行で分かるCloudflare Workers

  • Cloudflare Workers: V8 isolatesのメモリ内でJavaScriptコードを実行できる。ステートレスなので実行後はデータが消える。
  • Durable Objects: ユニークIDで識別されるJavaScriptクラスインスタンス。メモリ内に一時的な状態を保持できるほか、Durable Object Storage APIを用いてディスク上に永続化することもできる。
  • Durable Object Storage API: 強力な整合性を持つ永続ストレージで、Durable Objectに紐づいて動作する。ステートフルなので実行後もディスク上にデータが残る。

Asynchronous KV API

Durable Objectsのクローズドベータが開始された2020年9月から存在するAPIです。
KV-backedおよびSQLite-backed Durable Objectsの両方で利用可能ですが、メソッドは非同期でディスクI/Oなどの遅延が発生します。

this.ctx.storage.*にあるメソッドを使う必要があり、実行時にawaitが必要になります。

// Get
const value = await this.ctx.storage.get("key"); // await Promise<any> or undefined

// Put
await this.ctx.storage.put("key", value); // await Promise<void>

// Delete
const deleted = await this.ctx.storage.delete("key"); // await Promise<boolean>

// List
const entries = await this.ctx.storage.list(); // await Promise<Map<string, any>>
for (const [key, value] of entries) { ... }
interface (2025-09-26T00:00:00Z)
worker-configuration.d.ts
interface DurableObjectStorage {
  get<T = unknown>(
    key: string,
    options?: DurableObjectGetOptions,
  ): Promise<T | undefined>;
  get<T = unknown>(
    keys: string[],
    options?: DurableObjectGetOptions,
  ): Promise<Map<string, T>>;
  list<T = unknown>(
    options?: DurableObjectListOptions,
  ): Promise<Map<string, T>>;
  put<T>(
    key: string,
    value: T,
    options?: DurableObjectPutOptions,
  ): Promise<void>;
  put<T>(
    entries: Record<string, T>,
    options?: DurableObjectPutOptions,
  ): Promise<void>;
  delete(key: string, options?: DurableObjectPutOptions): Promise<boolean>;
  delete(keys: string[], options?: DurableObjectPutOptions): Promise<number>;
}
interface DurableObjectListOptions {
  start?: string;
  startAfter?: string;
  end?: string;
  prefix?: string;
  reverse?: boolean;
  limit?: number;
  allowConcurrency?: boolean;
  noCache?: boolean;
}
interface DurableObjectGetOptions {
  allowConcurrency?: boolean;
  noCache?: boolean;
}
interface DurableObjectPutOptions {
  allowConcurrency?: boolean;
  allowUnconfirmed?: boolean;
  noCache?: boolean;
}

KV-backed Durable Objects

https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/

KV-backed Durable ObjectsとはAsynchronous KV APIと同様に2020年9月から存在するキー・バリューストアベースのストレージバックエンドのことを指します。
SQLiteではないため、Asynchronous KV APIとAlarms APIしか使えませんし、無料枠もありません。

https://blog.cloudflare.com/introducing-workers-durable-objects/

Synchronous KV API

2025年9月に追加されたAPIです。

https://github.com/cloudflare/workerd/commit/123c205a888d09675cd9b522a44ee3b8adad2ed3

これによりctx.storage.kvが追加され、get()put()list()delete()メソッドを持つ。これら4つのメソッドは、ctx.storageの同名メソッドとシグネチャが同一だが、以下の点が異なる:

  1. 同期的に返す。Promiseではない。
  2. SQLiteベースのDOを必要とする。
  3. allowConcurrency および noCache オプションは SQLite では意味をなさないため存在しません(ただしこれらのオプションを渡すことは可能で、無視されます)。
  4. allowUnconfirmed オプションも存在しません。これは意味がないからではなく、SQLite バックエンドの DO に対して一般に実装されていないためです。将来実装される可能性があります。
  5. 細かい点:複数のキーを一度にget()した場合、返されるマップはキーが指定された順序で反復処理されます。従来のアルファベット順は、追加のソート操作を必要とする歴史的な癖でした。後方互換性のために維持されてきましたが、おそらく誰も気にしないため、新インターフェースではソートを省略しています。

storage.kvという名称はstorage.sqlとの親和性を保ちつつ、後方互換性の懸念を回避しています。長期的には旧来の非同期メソッドを「非推奨化」(ただし永続的にサポート)すべきでしょう。

コミットメッセージをwww.DeepL.com/Translator(無料版)で翻訳しました。

SQLite-backed Durable Objectsでのみ利用可能で、this.ctx.storage.kv.*にあるメソッドを使う必要があり、非同期I/Oを待たずに完了するため、イベントループをブロックせずに高速に操作できます。

// Get
const value = this.ctx.storage.kv.get("key"); // any or undefined

// Put
this.ctx.storage.kv.put("key", value); // void

// Delete
const deleted = this.ctx.storage.kv.delete("key"); // boolean

// List
const entries = this.ctx.storage.kv.list(); // Iterable<[string, any]>
for (const [key, value] of entries) { ... }
interface (2025-09-26T00:00:00Z)
worker-configuration.d.ts
interface SyncKvStorage {
  get<T = unknown>(key: string): T | undefined;
  list<T = unknown>(options?: SyncKvListOptions): Iterable<[string, T]>;
  put<T>(key: string, value: T): void;
  delete(key: string): boolean;
}
interface SyncKvListOptions {
  start?: string;
  startAfter?: string;
  end?: string;
  prefix?: string;
  reverse?: boolean;
  limit?: number;
}

SQLite-backed Durable Objects

https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/

SQLite-backed Durable Objectsとは2024年9月以降の、2025年4月に無料枠が追加された、組み込みSQLiteデータベースベースのストレージバックエンドのことを指します。

古くからあるAsynchronous KV API, Alarms APIだけではなく、SQL API, PITR API, Synchronous KV APIも使うことができます。

https://blog.cloudflare.com/sqlite-in-durable-objects/

なお、CloudflareダッシュボードのDurable Objectsでは「SQL バックエンド」と表示されています。
(バックエンドを指しているのであって、SQL APIとKV API両方使える)

Hello, World!

要するに、今後はSQLite-backed Durable ObjectsのSynchronous KV APIを使ったほうがいいので、以下のような感じで書いたほうがいいです。

import { DurableObject } from "cloudflare:workers";

export class MyDurableObject extends DurableObject<Env> {
  kv: SyncKvStorage;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.kv = ctx.storage.kv;
  }

  async sayHello(): Promise<string> {
    this.kv.put("content", "Hello, World!");
    const result = this.kv.get("content");
    return String(result);
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/") {
      const stub = env.MY_DURABLE_OBJECT.getByName(url.pathname);
      const greeting = await stub.sayHello();
      return new Response(greeting);
    }
    return new Response("404 Not Found", { status: 404 });
  },
} satisfies ExportedHandler<Env>;

もちろん、複雑なクエリを書きたい場合は以下のようにSQL APIを使いましょう。

import { DurableObject } from "cloudflare:workers";

export class MyDurableObject extends DurableObject<Env> {
  sql: SqlStorage;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sql = ctx.storage.sql;
  }

  async sayHello(): Promise<string> {
    this.sql.exec("CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, content TEXT)");
    this.sql.exec("INSERT OR REPLACE INTO posts (id, content) VALUES (1, 'Hello, World!')");
    const result = this.sql.exec("SELECT id, content FROM posts WHERE id = 1").one();
    return String(result.content);
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/") {
      const stub = env.MY_DURABLE_OBJECT.getByName(url.pathname);
      const greeting = await stub.sayHello();
      return new Response(greeting);
    }
    return new Response("404 Not Found", { status: 404 });
  },
} satisfies ExportedHandler<Env>;

まとめ

  • Asynchronous KV API: KV-backedおよびSQLite-backed Durable Objectsの両方で利用可能。ctx.storage経由でアクセスし、メソッドは非同期でPromiseを返す。awaitが必要。最終的に非推奨化になる?(ただし永続的にサポートする?)
  • Synchronous KV API: SQLite-backed Durable Objectsでのみ利用可能。ctx.storage.kv経由でアクセスし、メソッドは同期的に実行される。Promiseを返さず、直接値を返す。これにより、イベントループをブロックせずに高速に操作できる。
  • KV-backed Durable Objects: キー・バリューストアベース。Asynchronous KV APIとAlarms APIのみサポート。シンプルだが機能が限定的で、SQLのようなリレーショナル操作は不可。レガシーで下位互換性のために残されている。
  • SQLite-backed Durable Objects: 組み込みSQLiteデータベースベース。高機能で、SQL API、PITR(Point In Time Recovery)、Synchronous KV API、Asynchronous KV API、Alarms APIをサポート。フルSQLiteサポートにより、テーブル作成、JOIN、インデックス、FTS5(フルテキスト検索)、JSON関数などが可能。PITRにより、過去30日間の任意の時点にデータベースを復元可能。

Discussion