TL;DR
- Supabase 無料枠 5GB に対し 10.26GB(205% 超過) を消費(2026-05-31 確認)
- 犯人:
collection-queue-formの 5 秒 polling ×select='*'(raw_input_text最大 20 万字含む) - 止血: ① 生成列
has_raw_inputでフラグ化 ② select 列を絞る ③ polling 30s + 裏タブ停止
問題の構造
collection-queue-form
└── useEffect(setInterval 5s)
└── supabase.from('if_jobs').select('*')
└── raw_input_text (max 200,000 chars) ← これが毎回流れる
Vercel Function 経由(client=node)で叩く構成のため、egress は Supabase 側でカウントされる。5 秒 × 20 万字 × 稼働時間 = 数 GB が溶ける。
修正 1: 生成列でフラグ化
raw_input_text の有無だけ判定したいなら、本文を運ぶ必要はない。
ALTER TABLE if_jobs
ADD COLUMN has_raw_input boolean GENERATED ALWAYS AS (raw_input_text IS NOT NULL) STORED;
生成列は INSERT/UPDATE 時に自動計算されるため、アプリ側の変更ゼロでフラグが使える。
修正 2: select 列を絞る
// Before — 全列(raw_input_text 含む)
const { data } = await supabase.from('if_jobs').select('*')
// After — 一覧に必要な列のみ
const { data } = await supabase
.from('if_jobs')
.select('id, status, has_raw_input, created_at, updated_at')
詳細が必要な画面だけ select('id, raw_input_text') で個別取得するように分離する。
修正 3: polling 間隔 + visibilitychange
useEffect(() => {
let interval: ReturnType<typeof setInterval> | null = null
const startPolling = () => {
interval = setInterval(fetchQueueStatus, 30_000) // 5s → 30s
}
const stopPolling = () => {
if (interval) clearInterval(interval)
}
// 裏タブ中は polling を止める
document.addEventListener('visibilitychange', () => {
document.hidden ? stopPolling() : startPolling()
})
startPolling()
return () => stopPolling()
}, [])
判断軸: 同じ問題が起きる条件
| 条件 | リスク |
|---|---|
select('*') + テキスト系大列あり |
高 |
| polling 間隔 10s 以下 | 高 |
| Vercel Function 経由の構成 | 中 |
| 裏タブでも polling 継続 | 中 |
参考
この実装の背景と全体の設計判断は掲示元記事に詳しく書いています。
- masatoman.net: Supabase 無料枠の egress 5GB を 10.26GB で超過した話