Vercel + Supabaseの本番コストが予想の3倍になりかけた話と、どう削減したか
はじめに
宅建試験対策アプリ「takkenai.jp」を個人開発しています。技術スタックは Next.js(App Router)+ Supabase + Vercel というモダン構成です。
開発中は「Vercel の Pro プランが月$20、Supabase の Pro が月$25、合計$45(約7,000円)くらいかな」と楽観的に見積もっていました。
蓋を開けたら、最初の月の請求額は約$62(約9,300円)。予想の3倍とまではいかないものの、このまま成長したら確実に3倍コースでした。
ユーザー数わずか200人程度でこの金額。何が起きていたのか、そしてどう対処したかを共有します。
💸 請求内訳を見て血の気が引いた
Vercel のダッシュボードと Supabase の Usage を睨み合わせた結果、以下の内訳でした。
| 項目 | 予想(月額) | 実際(月額) |
|---|---|---|
| Vercel Pro 基本料 | $20 | $20 |
| Vercel 画像最適化 | $0 | $8 |
| Vercel Edge Function 実行数 | $0 | $3 |
| Supabase Pro 基本料 | $25 | $25 |
| Supabase Edge Functions 呼び出し | $0 | $4 |
| Supabase Realtime 接続数超過 | $0 | $2 |
| 合計 | $45(約7,000円) | $62(約9,300円) |
「無料枠の範囲内だろう」と思っていた従量課金部分が、じわじわと積み上がっていたのです。
🔍 原因の特定:3つの「コスト爆弾」
爆弾1:Next.js Image の最適化が毎回走る
next/image の <Image> コンポーネントを問題画像や解説図に使っていましたが、キャッシュ戦略を何も設定していなかったため、同じ画像が何度も最適化 API を叩いていました。Vercel の画像最適化は Pro プランで月1,000枚まで無料ですが、宅建の過去問画像だけで1日100回以上の最適化リクエストが発生していました。
爆弾2:学習進捗のリアルタイム同期が過剰
「複数デバイスで学習進捗をリアルタイム同期したい」と Supabase Realtime を使っていました。しかし、ユーザーがタブを開いたまま放置するとコネクションが張りっぱなしになります。200人のユーザーでも同時接続が常時50〜80件。Supabase Pro の Realtime 同時接続上限(200)に対して、成長すれば一瞬で超過するペースでした。
爆弾3:Edge Function での AI 解説生成が無駄に多い
AI による解説生成を Supabase Edge Functions で実装していましたが、同じ問題の解説を毎回生成していました。キャッシュの概念がゼロ。同じ過去問をユーザーが解くたびに Edge Function が起動し、OpenAI API 代とは別に Supabase 側の呼び出し回数も積み上がっていました。
🛠 実施した対策(コード付き)
対策1:画像を Supabase Storage + CDN キャッシュに移行
next/image の最適化に頼るのをやめ、Supabase Storage にアップロード済みの画像を CDN 経由で配信するようにしました。
// before: 毎回 Vercel の画像最適化が走る
<Image src="/questions/q001.png" width={600} height={400} alt="問題図" />
// after: Supabase Storage の変換機能 + 長期キャッシュ
const getOptimizedImageUrl = (path: string, width: number) => {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
return `${supabaseUrl}/storage/v1/render/image/public/questions/${path}?width=${width}&quality=75`;
};
// コンポーネント側
<img
src={getOptimizedImageUrl("q001.png", 600)}
alt="問題図"
loading="lazy"
decoding="async"
/>
Supabase Storage の画像変換は Pro プランに含まれているため、追加コスト$0。next.config.js で images.unoptimized: true を設定し、Vercel 側の最適化を完全に停止しました。
効果:画像最適化コスト $8 → $0
対策2:Realtime をやめてポーリング + SWR に変更
正直、宅建の学習進捗にリアルタイム性は不要でした。「秒単位の同期」ではなく「アプリを開いたときに最新に追いつく」で十分です。
// before: Supabase Realtime で常時接続
const channel = supabase
.channel('progress')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'user_progress' },
(payload) => setProgress(payload.new)
)
.subscribe();
// after: SWR でフォーカス時に再取得(接続数ゼロ)
import useSWR from 'swr';
const fetcher = async (userId: string) => {
const { data, error } = await supabase
.from('user_progress')
.select('*')
.eq('user_id', userId)
.single();
if (error) throw error;
return data;
};
export const useProgress = (userId: string) => {
return useSWR(
userId ? `progress-${userId}` : null,
() => fetcher(userId),
{
revalidateOnFocus: true, // タブ復帰時に再取得
revalidateOnReconnect: true,
dedupingInterval: 30000, // 30秒間は重複リクエスト防止
}
);
};
効果:Realtime 接続数 50〜80 → 0、超過料金 $2 → $0
対策3:AI 解説をDBキャッシュして再利用
同じ問題に対する AI 解説は一度生成したら DB に保存し、2回目以降はキャッシュから返すようにしました。
// Supabase Edge Function: generate-explanation
serve(async (req) => {
const { questionId } = await req.json();
// 1. キャッシュ確認
const { data: cached } = await supabase
.from('ai_explanations')
.select('content, created_at')
.eq('question_id', questionId)
.single();
if (cached) {
return new Response(JSON.stringify({
explanation: cached.content,
source: 'cache'
}), { headers: { 'Content-Type': 'application/json' } });
}
// 2. キャッシュなし → AI生成
const explanation = await generateWithOpenAI(questionId);
// 3. DBに保存(次回以降はキャッシュヒット)
await supabase
.from('ai_explanations')
.upsert({ question_id: questionId, content: explanation });
return new Response(JSON.stringify({
explanation,
source: 'generated'
}), { headers: { 'Content-Type': 'application/json' } });
});
導入後、Edge Function の呼び出し自体は変わりませんが、実行時間が大幅に短縮(AI API呼び出しをスキップするため平均50ms以下に)。加えて、OpenAI 側のコストも激減しました。
効果:Edge Functions コスト $4 → $1、OpenAI API 代も約70%削減
📊 Before / After まとめ
| 項目 | Before | After |
|---|---|---|
| Vercel 画像最適化 | $8 | $0 |
| Vercel Edge Function | $3 | $1 |
| Supabase Edge Functions | $4 | $1 |
| Supabase Realtime 超過 | $2 | $0 |
| 基本料合計 | $45 | $45 |
| 月額合計 | $62(約9,300円) | $47(約7,100円) |
従量課金部分を $17 → $2 に削減。約88%カットできました。ユーザーが10倍に増えても従量部分の増加は緩やかになる構成に変わっています。
✅ 個人開発者向け:本番前コスト項目チェックリスト
本番デプロイ前に、以下を必ず確認してください。過去の自分に送りたいリストです。
-
next/imageの最適化回数を見積もったか?画像枚数 × 想定PV で計算する - Supabase Realtime は本当に必要か?ポーリングや SWR で代替できないか
- Edge Functions / Serverless Functions の結果をキャッシュしているか
- Supabase の Database サイズが Pro の上限(8GB)に対してどの程度か把握しているか
- Vercel の 帯域幅(Pro: 1TB/月)に対する見積もりはあるか
- **外部 API(OpenAI等)**の呼び出しにレート制限・キャッシュを入れているか
- Vercel ダッシュボードの **Spend Limit(課金上限)**を設定したか
- Supabase の **Cost Control(Spend Cap)**を有効にしているか
-
vercel.jsonで Function の maxDuration を必要最小限に設定しているか - 本番デプロイ後 最初の3日間は毎日 Usage を確認すると決めているか
おわりに
個人開発で Vercel + Supabase の組み合わせは本当に快適ですが、「無料枠があるから大丈夫」は危険な思い込みでした。特にユーザーが増え始めるタイミングで従量課金が一気に効いてきます。
takkenai.jp はまだ小規模ですが、今回の経験で「コストも設計の一部」だと痛感しました。これから本番デプロイする個人開発者の方は、ぜひチェックリストを活用してください。
最初の請求書で驚くのは、一度で十分です。