SaaS従量課金メータリング設計|使用量集計・締め処理・冪等な請求連携の実装
コセケン
テクラル合同会社

SaaSの従量課金を本番運用に乗せる鍵は、決済APIの呼び方ではなく「使用量をどう集計し、いつ締め、どうやって二重請求を出さずに請求連携するか」というバックエンド設計にあります。本記事では、利用イベントの記録から集計テーブルの設計、締め処理(period cut)、そして請求基盤(Stripeなど)への冪等な連携までを、実装の解像度で整理します。AI SaaSのようにトークン消費や推論回数で課金するプロダクトを設計する立場の方が、収益基盤を「数えられて・締められて・二重請求しない」状態に持っていくための設計指針です。
従量課金は「メーター(使用量計測器)」をプロダクトの中に作る作業です。決済そのもの(カード処理やWebhook受信)は請求基盤に委ねられますが、何を・どれだけ使ったかを正確に数え、請求対象として確定させる責任はSaaS側に残ります。ここを曖昧にすると、請求のたびに金額がブレ、顧客からの問い合わせ対応に追われ、収益の根拠を自分で説明できない事業になります。逆に、メータリングの設計が堅牢であれば、後から課金軸(トークン単価・API回数・ストレージ容量など)を足し引きしても破綻しません。
メータリング設計の全体像|記録・集計・締め・連携の4層に分ける
従量課金のバックエンドは「①利用イベントの記録」「②使用量の集計」「③請求期間の締め」「④請求基盤への連携」の4層に分離して設計します。理由は、各層で求められる性質(書き込み速度・正確性・冪等性)が異なり、密結合させると一箇所の障害が請求金額に直接波及するためです。
具体的には、リアルタイムに大量発生する利用イベントは「追記専用(append-only)」で軽く記録し、集計は別プロセスで行い、締めは期間境界で一度だけ確定させ、請求連携は失敗しても安全に再実行できる形にします。この4層を最初から分けておくと、トラフィックが増えても各層を独立にスケールでき、不具合の切り分けも容易になります。
| 層 | 役割 | 重視する性質 | 失敗時の影響 |
|---|---|---|---|
| ① 記録(ingest) | 利用イベントを追記 | 書き込みスループット・耐障害性 | 計測漏れ=収益取りこぼし |
| ② 集計(aggregate) | 顧客×指標×期間で合算 | 正確性・再計算可能性 | 金額のブレ |
| ③ 締め(period cut) | 請求期間を確定 | 一度だけ・不変性 | 二重計上・期ずれ |
| ④ 連携(billing sync) | 請求基盤へ反映 | 冪等性・再実行安全 | 二重請求・未請求 |
利用イベントの記録|追記専用テーブルと冪等キーを最初から持たせる
利用イベントは「追記専用(append-only)」のテーブルに、必ず冪等キー(idempotency key)を付けて記録します。なぜなら、計測の入口(SDK呼び出し・バッチ取り込み・ワーカー・Webhook)はすべて再試行されうるため、同じ利用が二重に記録されると集計と請求が膨らむからです。
冪等キーは「顧客ID+指標+時間バケット」のように決定論的に生成すると、ネットワーク再送やリトライがあっても同じキーになり、ユニーク制約で二重挿入を弾けます。イベントは更新せず追記だけにすることで、後から集計をやり直せる「真実の記録」として機能します。
-- 利用イベント(追記専用・冪等キーで二重記録を防ぐ)
create table usage_events (
id bigint generated always as identity primary key,
account_id uuid not null,
metric text not null, -- 例: 'api_calls' / 'tokens'
quantity bigint not null check (quantity >= 0),
occurred_at timestamptz not null, -- 利用が発生した時刻
idempotency_key text not null, -- 顧客ID+指標+時間バケット等
created_at timestamptz not null default now()
);
-- 同一の利用を二度書き込まないためのユニーク制約
create unique index uq_usage_events_idem
on usage_events (idempotency_key);
ここで重要なのは、occurred_at(利用が起きた時刻)とcreated_at(記録した時刻)を分けることです。ネットワーク遅延やバッチ取り込みで記録が遅れても、締め処理はoccurred_at基準で行うため、どの請求期間に属するかを正しく判定できます。この一手間が、月末締めのときの「期ずれ」を防ぎます。
使用量の集計|事前集約でAPIコールとコストを抑える
集計は「顧客×指標×期間」で事前に合算(pre-aggregate)し、請求基盤へは集約済みの値を送ります。理由は、利用イベント1件ごとに請求基盤のAPIを叩くとコール数が膨大になり、レート制限とコストの両方で破綻するためです。
実装としては、生イベントを定期的に集計テーブルへロールアップ(rollup)し、請求にはこの集計値を使います。集計は冪等であるべきで、同じ期間を何度再集計しても同じ結果になるよう、合算は「再計算(上書き)」で行います。インクリメンタルな足し込みは一見速いですが、途中で失敗すると二重加算するため、集計の正確性を最優先するなら期間を指定した再計算型が安全です。
-- 日次ロールアップ(指定期間を再計算で上書き=冪等)
insert into usage_daily (account_id, metric, usage_date, total_quantity)
select account_id, metric, date_trunc('day', occurred_at)::date, sum(quantity)
from usage_events
where occurred_at >= $1 and occurred_at < $2
group by account_id, metric, date_trunc('day', occurred_at)::date
on conflict (account_id, metric, usage_date)
do update set total_quantity = excluded.total_quantity;
集計の粒度は「日次」を基本にすると、月次請求への合算が容易で、かつ顧客への利用状況ダッシュボード表示にも転用できます。リアルタイム表示が必要な場合のみ、別途キャッシュ層やストリーム集計を足す判断になりますが、請求の正本は必ずこの永続化された集計テーブルに置きます。
締め処理(period cut)|請求期間を一度だけ確定させる
締め処理は「請求期間のスナップショットを一度だけ取り、以後は不変(immutable)にする」のが原則です。理由は、締めた後に集計値が動くと、すでに請求した金額と矛盾し、二重計上や返金処理が発生するためです。
具体的には、請求期間の終了時刻で集計を確定し、確定値を別テーブル(請求明細スナップショット)に書き出します。締め後に到着した遅延イベント(late-arriving events)は、現在の期間に確定済みであれば次期間へ繰り越すか、調整(adjustment)として明示的に記録します。締め処理自体も冪等にし、同じ期間に対して二度走っても結果が変わらないようにします。
-- 締め: 期間の確定値をスナップショット化(締め済みは上書きしない)
insert into billing_period_lines (account_id, metric, period_start, period_end, quantity, closed_at)
select account_id, metric, $1::date, $2::date, sum(total_quantity), now()
from usage_daily
where usage_date >= $1 and usage_date < $2
group by account_id, metric
on conflict (account_id, metric, period_start, period_end)
do nothing; -- 締め済みは固定(不変性を担保)
締めのタイミングは「請求基盤の請求サイクルに合わせる」のが原則です。たとえば月初課金なら月末24時を期間境界とし、タイムゾーンを固定して期ずれを防ぎます。締め処理は決済の直前ではなく、独立したバッチ(締めジョブ)として走らせ、締め結果を確認してから請求連携へ進む二段構えにすると、誤請求を出す前に異常を検知できます。
請求基盤への冪等な連携|Stripe Meter Eventsを例に
請求連携は「同じ締め結果を何度送っても二重請求にならない」冪等性が絶対条件です。なぜなら、連携処理はタイムアウトやデプロイ中断で必ず再試行が発生し、再試行のたびに請求が積み増されると顧客に直接損害を与えるためです。
Stripeを例にすると、現行の従量課金は2024年9月に追加された Meter Events API(POST /v1/billing/meter_events)を使い、event_name・payload・identifier・timestamp を送ります。このうち identifier が冪等キーに相当し、Stripeは少なくとも24時間のローリング期間で同一 identifier の重複を排除します(公式ドキュメント記載)。つまり、自社の締め結果に決定論的な identifier(顧客ID+指標+請求期間)を付けて送れば、再送しても二重計上されません。
// 締め結果を Stripe Meter Events へ冪等に送る(Stripe Node SDK)
// identifier を「決定論的キー」にすることで再送時の二重計上を防ぐ
await stripe.billing.meterEvents.create({
event_name: "api_calls",
identifier: `${accountId}:api_calls:${periodStart}`, // 冪等キー
timestamp: Math.floor(periodEndDate.getTime() / 1000),
payload: {
stripe_customer_id: stripeCustomerId,
value: String(quantity),
},
});
大量イベントを高頻度で送る場合は、v1の同期APIではなく v2 の Meter Event Stream を使います。これは先にセッション(認証トークン・有効期限15分)を作り、ストリーム経由で非同期に送る方式で、livemodeで毎秒最大10,000リクエスト・1リクエストあたり最大100イベントまで処理できます(公式ドキュメント記載)。ただし本記事の設計方針では、生イベントをそのまま流すのではなく、前述の事前集約で締め結果を送るのが基本です。連携コール数を抑えられ、突合(reconciliation)も容易になります。
| 方式 | エンドポイント | 向く用途 | 注意点 |
|---|---|---|---|
| v1 Meter Events | POST /v1/billing/meter_events |
集約済みの締め結果を定期送信 | 同期・コール数を抑える前提 |
| v2 Meter Event Stream | セッション+ストリーム | 高頻度の生イベント送信 | セッション15分・非同期検証 |
なお、決済そのもの(カード処理・サブスクリプションの請求書発行・支払いWebhookの受信)は請求基盤側の責務であり、本記事の射程外です。SaaS側が握るのは「正しい使用量を冪等に届けるところまで」で、ここを堅牢にすれば決済側の実装は標準的なパターンに収まります。
突合と監視|自社の合計と請求基盤の合計を毎日照合する
運用に乗せたら「自社の集計合計」と「請求基盤側の合計」を毎日突合(reconciliation)し、差分を検知する仕組みを必ず入れます。理由は、計測漏れ・連携失敗・遅延イベントは静かに発生し、月末の請求で初めて気づくと顧客対応と返金で大きなコストになるためです。
突合ジョブは、締め済みのbilling_period_linesの合計と、請求基盤から取得したメーターサマリーを照合し、許容誤差を超えたらアラートを出します。あわせて、連携の冪等キー送信ログ・締めジョブの実行記録・遅延イベントの調整記録を残し、「なぜこの金額になったか」を後から追跡できる状態にします。従量課金は金額の根拠を顧客に説明できることが信頼に直結するため、監視と監査ログは請求機能の一部として設計します。
内製と外注の判断軸|どこまで自前で持つか
メータリング設計は「記録・集計・締め・突合の中核ロジックは自前で持ち、決済そのものは請求基盤に委ねる」のが現実的な切り分けです。理由は、課金軸はプロダクト固有で頻繁に変わるため外部任せにしにくい一方、カード処理やコンプライアンスは請求基盤に任せた方が安全だからです。
判断軸を整理すると、次のようになります。まず、利用イベントの定義・集計の正確性・締めの不変性は「収益の根拠」そのものなので、仕様を自社で完全に把握できる体制を持つべきです。一方、ここを最初から堅牢に作るには、追記専用の記録設計・冪等な集計と連携・突合監視まで含めた経験が必要で、初回の自前構築でつまずくと、リリース後に二重請求や期ずれといった「顧客の信頼を直接損なう不具合」として表面化します。
したがって判断としては、課金軸が定まらない立ち上げ期は、設計の骨格(4層分離・冪等キー・締めの不変性・突合)を最初から正しく敷くことに投資し、ここを設計段階から経験のある体制で固めるか、内製で進めるなら本記事の各層を確認項目として用いるのが堅実です。メータリングは「後から直す」がもっとも高くつく領域であり、収益基盤として最初の設計品質がそのまま事業の説明責任に直結します。
参考(一次情報):
- Stripe Meter Events API: https://docs.stripe.com/api/billing/meter-event
- Stripe Meter Event Stream(v2): https://docs.stripe.com/api/v2/billing-meter-stream
- Stripe 使用量の記録(API): https://docs.stripe.com/billing/subscriptions/usage-based/recording-usage-api
この記事を書いた人

コセケン
テクラル合同会社
スタートアップでのCTO経験を経て、現在はテクラル合同会社にてシステム開発全般を牽引しています。アプリおよびWebの開発から、バックエンド、インフラ構築に至るまで幅広い技術領域に対応可能です。スピード感を持った品質の高いシステム開発を得意としており、新規プロダクトの立ち上げを一気通貫で支援します。本ブログでは実践的な開発ノウハウを発信していきます。


