---
title: Workers + Durable Objectsで作るライブ投票実践
tags:  #cloudflare  
author: [nabettu🍲NotionでWebサイトが作れるサービス開発中](https://image.docswell.com/user/nabettu)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/4EZLNVK473.jpg?width=480
description: Workers + Durable Objectsで作るライブ投票実践 by nabettu🍲NotionでWebサイトが作れるサービス開発中
published: September 24, 26
canonical: https://image.docswell.com/s/nabettu/KWRNDX-cloudflare-DurableObjects
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/4EZLNVK473.jpg)

Workers + Durable Objects で作る
ライブ投票 実践
なべっつ（@nabettu）
~ Cloudflare Workers実践LT 〜移行から運用、コストの話まで〜 2026/09/24 ~
1


# Page. 2

![Page Image](https://bcdn.docswell.com/page/Y76W85QG7V.jpg)

自己紹介
なべっつ（@nabettu）
X / GitHub: @nabettu Cloudflare Workers は 2020 年ごろから使っています
イベント管理サービス（次のページで説明します）
空室の写真に AI で家具を置くサービス
2


# Page. 3

![Page Image](https://bcdn.docswell.com/page/G75M9YRX74.jpg)

こういうサービスを作ってます 👇
イベントの告知・参加登録から、当日のライブ投票・Q&amp;A・受付まで URL ひとつで
ホスティング
Cloudflare Workers（OpenNext）+ Smart Placement
リアルタイム
Durable Objects（WebSocket）
ストレージ / キャッシュ R2 / KV（incremental cache）
定期実行 / 計測
Cron Triggers / Analytics Engine
フレームワーク
Next.js 16 (App Router) + TypeScript
DB
Turso (LibSQL) + Drizzle ORM
黄色いところが Cloudflare。月 $5（Workers Paid）で SSR もリアルタイムも全部載せ
3


# Page. 4

![Page Image](https://bcdn.docswell.com/page/9J292GDQER.jpg)

デモから始めます！
📱 ここから
投票してみてください！
https://evet.jp/e/ibBUYnMp/live?tab=poll
ログイン不要です。最後に結果を出します
もし質問あれば、質問タブに書いてもらっても OK です
4


# Page. 5

![Page Image](https://bcdn.docswell.com/page/DEY4YV2YJM.jpg)

いま動いているもの: ライブ投票
誰かが投票した瞬間、全員のグラフが動く（リロード不要）
ログイン不要で投票できる（端末ごとの token で二重投票を防ぐ）
単一選択 / 複数選択、投票後に結果を発表、締め切り
同じ画面にライブ Q&amp;A（質問 + いいね）も同居
この「全員の画面が同時に動く」を
どう作ったかが今日の話です
5


# Page. 6

![Page Image](https://bcdn.docswell.com/page/VJNYPM8R78.jpg)

今日話すこと
1. Durable Objects ってなに？
2. 作り方（宣言 → つなぐ → 配る）
3. 設計の肝: 状態を持たせない
4. デプロイでハマった話
6


# Page. 7

![Page Image](https://bcdn.docswell.com/page/YE9P3NKZJ3.jpg)

Durable Objects は Workers の一機能
Worker
起動のしか アクセスごとに並列で何個でも立
た
つ
状態
持てない（立つたびに別物）
向いてる用途 普通のリクエスト処理
Durable Object
名前ごとに世界で 1 つだけ立つ
持てる
（同じ名前なら同じ 1 つに繋がる）
接続を 1 か所に束ねたいもの
別サービスの契約は不要。同じ wrangler deploy で一緒に出ます
7


# Page. 8

![Page Image](https://bcdn.docswell.com/page/GE8DMK2YED.jpg)

作り方 ① 宣言する
wrangler.toml
に 2 ブロック書くだけで生えます
[[durable_objects.bindings]]
name = &quot;REALTIME_ROOM&quot;
class_name = &quot;RealtimeRoom&quot;
[[migrations]]
tag = &quot;v1&quot;
new_sqlite_classes = [&quot;RealtimeRoom&quot;]
管理画面でポチポチしてリソースを作る作業はゼロ
8


# Page. 9

![Page Image](https://bcdn.docswell.com/page/LELM3Z297R.jpg)

作り方 ② つなぐ
// Worker
の fetch。WebSocket は SSR に渡す手前で横取り
if (request.headers.get(&quot;Upgrade&quot;) === &quot;websocket&quot;) {
if (!(await authorize(request))) return new Response(&quot;ng&quot;, { status: 403 });
const id = env.REALTIME_ROOM.idFromName(&quot;qa:evt_123&quot;);
return env.REALTIME_ROOM.get(id).fetch(request);
}
idFromName(&quot;qa:&lt;
部屋の割り当て
// ← 丸投げ
// ←
イベントID&gt;&quot;) = イベントごとに 1 部屋。部屋割りはこれで終わり
9


# Page. 10

![Page Image](https://bcdn.docswell.com/page/4JMYDV8VJW.jpg)

DO 本体は 80 行
export class RealtimeRoom {
async fetch(request) {
if (request.headers.get(&quot;Upgrade&quot;) === &quot;websocket&quot;) {
const [client, server] = Object.values(new WebSocketPair());
this.state.acceptWebSocket(server);
// ← Hibernation API
return new Response(null, { status: 101, webSocket: client });
}
サーバーからの配信を
const payload = await request.text();
//
for (const ws of this.state.getWebSockets()) ws.send(payload);
return new Response(&quot;ok&quot;);
//
全員へ
}
}
acceptWebSocket
= アイドル中は実行時間が課金されない
10


# Page. 11

![Page Image](https://bcdn.docswell.com/page/PJR9425W79.jpg)

作り方 ③ 配る
const res = await voteLivePoll(eventId, pollId, optionIds);
に書いて
// 集計をそのまま配る
// DB
await broadcast(`qa:${eventId}`, &quot;poll.vote.updated&quot;, {
pollId: res.pollId, counts: res.counts, totalVoters: res.totalVoters,
});
DO はドメインを何も知らないので、同じ 80 行を 3 機能で使い回し
ライブ投票 / ライブ Q&amp;A（ qa:&lt;eventId&gt; ）
当日の受付チェックイン（ reception:&lt;eventId&gt; ）
11


# Page. 12

![Page Image](https://bcdn.docswell.com/page/PEXQ2DKVJX.jpg)

設計の肝: DO に状態を持たせない
持っているもの
Durable Object 生きている接続だけ
Turso (DB)
投票・質問・いいね・唯一のデータソース
DO は状態を持てるけど、あえて持たせない（ただの pub/sub リレーに徹する）
12


# Page. 13

![Page Image](https://bcdn.docswell.com/page/3EK9MX55ED.jpg)

データが DB にあるから安心
デプロイで DO が再起動しても、失われるのは接続だけ
データは 1 件も消えない
二重管理が構造的に起きない
「DO と DB、どっちが正しい？」が発生しない
配信は best-effort（失敗しても throw しない）
WebSocket が塞がれた環境でも機能は成立。ライブ更新が止まるだけ
リアルタイムが壊れても、データは壊れない
13


# Page. 14

![Page Image](https://bcdn.docswell.com/page/L73WY5K175.jpg)

ハマった話: デプロイのたびにエラー 173 件
症状: 本番デプロイごとにエラーアラートがバースト（173 件 / 15 分）
原因: 切断時に律儀に ws.close() を呼んでいた
→ デプロイ = DO 再起動 = 接続数に比例して例外
compatibility_flags = [&quot;web_socket_auto_reply_to_close&quot;]
+ ハンドラを空にするだけ（後始末すべき状態が無いから消せる）
14


# Page. 15

![Page Image](https://bcdn.docswell.com/page/87DK5D3KJG.jpg)

まとめ
Durable Objects は Workers の一機能 — 別サービスを立てなくていい
状態は持たせない — DB を唯一のデータソースにする
切断は前提 — 再接続を最初から作る
ぜひ DO を使ってみてください！
記事 → zenn.dev/nabettu
サービス → evet.jp
15


# Page. 16

![Page Image](https://bcdn.docswell.com/page/VJPKGX43E8.jpg)

🗳 みなさんの投票結果
時間が余ったら、質問あれば答えます
（ここでライブ投票の画面に切り替える。質問タブも見る）
16


