この記事は ひとりCloudflareを使い倒す Advent Calendar 2025 の11日目です
「Queue」って聞いたことありますか。「くえうえ」と読みたくなりますが「キュー」です。
「列」とかって訳されることが多いですね。身近なものだと、印刷キューとか…
Cloudflare にも Queue があります。
課金で解除されるサービスで、使ったことないのでこの際使ってみようと思います。
Cloudflare Queues のユースケース、何?
個人開発していて「あ、今 Queue ほしいな」ってなったことありますか?
私はありません。
なので、そもそも Queue が必要なケースから学んでいきましょう。
CF 公式ドキュメント
チュートリアルにユースケースがいくつかありますね。
- Use event notification to summarize PDF files on upload (イベント通知を使用して、アップロード時に PDF ファイルを要約する)
- Handle rate limits of external APIs (外部 API のレート制限を処理する)
- Build a web crawler with Queues and Browser Rendering (Queues と Browser Rendering を使って Web クローラーを作る)
- Log and store upload events in R2 with event notifications (イベント通知を使用し、アップロードイベントを R2 に記録して保存する)
1 番目の例を詳しく読んでみますと、R2 にオブジェクトが作成された (= PDF ファイルがアップロードされた) ことが Queues に入り、別に立てた Workers の queue ハンドラが実行されて PDF ファイルを要約する、という流れですね。
これは、Cloudflare がデフォルトで提供するイベントの通知をトリガーに処理を行うものですね。
一方で 2 番目の例では Resend を使ったメール再送管理で、自分で Queues にキューを突っ込んで、自分で取り出して処理をしています。
イベントをトリガーに処理をすることもできるし、自分でキューに突っ込んで処理することもできるんですね。
Todo リストを作ってみる
みんな大好き Todo リストを作ります。
ただの Todo リストだと面白くないので、Todo の作成処理を全部バッチ化します。
意味がわからないかもしれませんが、世の中そんなもんです。
Todo リストとして成立していませんが、Todo 完了とか編集とか全部無視して、Todo の作成とリストアップのみを実装します。
まずは打ち慣れました create から。
pnpm create cloudflare@latest
Hello World Example, Workers Only, TypeScript で作成したら、pnpm run cf-typegen で型をつくって、pnpm run dev でサーバーが立ち上がるか確かめます。
そしたら、Queues の Binding を追加します。
{
"queues": {
"producers": [
{
"queue": "todo",
"binding": "TodoQueue"
}
],
"consumers": [
{
"queue": "todo",
"max_batch_size": 10,
"max_batch_timeout": 20,
}
]
},
}
producers はキューにメッセージを書き込むやつです。
ここでは、todo をキューに使うこと、呼び出しは env.TodoQueue を使うことを定義しています。
そして consumers はキューからメッセージを呼び出すやつです。
consumers には「メッセージがキューに入った時点で consumer を呼び出すやつ」と「メッセージにキューが入ってるか聞いて、取り出すやつ」の 2 種類がありますが、今回は前者を使います。
ここでは、todo をキューに使うこと、max_batch_size で「キューに 10 件貯まったら comsumer を呼び出す」こと、max_batch_timeout で「キューに 10 件貯まってなくても、20 秒立ったら consumer を呼び出すこと」が定義されています。
ここまで来たら、一度 pnpm run cf-typegen で型定義を作り直しておきます。
そして一度サーバーを起動すると、おそらく次のように、Binding に env.TodoQueue が追加されているはずです。
⛅️ wrangler 4.54.0
───────────────────
Your Worker has access to the following bindings:
Binding Resource Mode
env.TodoQueue (todo) Queue local ← これ
そしたら src/index.ts をいじって、まずはキューに突っ込むエンドポイントを作ります。
export default {
async fetch(request, env, ctx): Promise<Response> {
const pathname = new URL(request.url).pathname;
if (pathname === '/todo') {
if (request.method === 'GET') {
// Todo: Fetch Todo list
}
if (request.method === 'POST') {
const { id, task } = await request.json();
await env.TodoQueue.send({ id, task }); // ここでキューに突っ込む!
return new Response('Todo added', { status: 201 });
}
return new Response('Method Not Allowed', { status: 405 });
}
return new Response('Hello World!');
}
} satisfies ExportedHandler<Env>;
すごい簡単。send に全部オブジェクトを入れておけば、あとは勝手にキューで待っててくれます。
早速 Postman とか使って /todo に POST してみましょう。
201 Created が返ってきました!
ただ、コンソールを見てみましたら、なんか怪しい表示が…
先程 wrangler.jsonc には consumer の定義をしましたよね。
Workers では、queue() を宣言することで consumer と定義されます。
ということで、一旦 Queue に何が入っているのか教えてくれる調整をば。
async queue(batch, env, ctx) {
for (const message of batch.messages) {
console.debug(message.body);
}
}
これで Todo を Post すると、consumer を呼び出してコンソールに何を受け取ったのかが表示されました。
よし、Queue は一通り使えるようになりました。
ということで、ここからは気合で Durable Objects をストレージとした Todo リストエンドポイントを実装します。
なお、変更は割愛します。
変更後のファイルたち
{
+ "durable_objects": {
+ "bindings": [
+ {
+ "name": "TodoDO",
+ "class_name": "Todo",
+ }
+ ]
+ }
}
import { DurableObject } from "cloudflare:workers";
type TodoId = string
type TodoItem = {
id: TodoId;
text: string;
completed: boolean;
};
export class Todo extends DurableObject {
async fetchTodos(): Promise<TodoItem[]> {
const todos = Array.from((await this.ctx.storage.list<TodoItem>()).values());
return todos;
}
async addTodo(item: TodoItem): Promise<void> {
await this.ctx.storage.put(item.id, item);
}
}
import { Todo } from "./todo";
export { Todo }
export default {
async fetch(request, env, ctx): Promise<Response> {
const pathname = new URL(request.url).pathname;
const todoStore = env.TodoDO.get(env.TodoDO.idFromName("my-todo-store"));
if (pathname === '/todo') {
if (request.method === 'GET') {
const todos = await todoStore.fetchTodos();
return new Response(JSON.stringify(todos), {
headers: { 'Content-Type': 'application/json' },
});
}
if (request.method === 'POST') {
const { id, task } = await request.json();
await env.TodoQueue.send({ id, task });
return new Response('Todo added', { status: 201 });
}
return new Response('Method Not Allowed', { status: 405 });
}
return new Response('Hello World!');
},
async queue(batch, env, ctx) {
const todoStore = env.TodoDO.get(env.TodoDO.idFromName('my-todo-store'));
for (const message of batch.messages) {
const { id, task } = message.body;
const todoItem = {
id,
text: task,
completed: false,
};
await todoStore.addTodo(todoItem);
}
}
} satisfies ExportedHandler<Env>;
ということで、バッチ処理を入れました。
ここで私は思いました。「どれくらい性能差が出るんやろ」と。
ということで、Postman の機能に負荷テストがあるので、使っていきます。
2 分間、100 ユーザーがひたすらエンドポイントを叩いている想定で、実験してみます。
※実験条件は対して正確ではありませんので、参考値です。
バッチ処理したほうが遅くなってしまいました。
同じ Workers スクリプトの中でバッチ処理が動いているので、それで平均値に影響するレベルのスパイクが出ている印象でした。
しかし、毎度 DO を触っていないバッチ処理では、ユーザーの POST リクエスト最短速度は 1ms 勝っています。
ということで、検証の余地こそあれど、バッチ化して他の Workers で実行してあげれば、処理速度いい感じになるかも、という仮説が立ちました。
おわりに
今回は超軽量の処理なのであまりバッチ処理の恩恵を受けることができませんでした。
しかし、ユースケース調査で見た「PDF ファイルの要約自動作成」や「ファイルエンコーディング、サムネイル処理」など、時間がかかる処理で一回ユーザーに返したいときには大変重宝する機能だと思います。
あと実装がすごい簡単。Amazon SQS (AWS 版の Queues) とか使ったことないからわかんないけど。
ただ、1 行でキューに放り込めて、1 関数実装すればキュー取り出しが動作して、しかも R2 等のイベントもキューに流せるというのは、Cloudflare ロックインの私からすれば、色々な活用法がありそうです。
これが使えそうな Slidev 拡張も思いついているので、早く冬休みになって実装してみたいと思います。




