사용량 측정, 할당량 및 결제 연결 지점
테넌트별 사용량을 추적하고 요금제 할당량을 적용하며 사용량 기반 SaaS 가격 책정을 위한 결제 이벤트를 생성합니다.
사용량 측정, 할당량 및 결제 연결 지점은(는) CoddyKit의 무료 FastAPI Backend Development Bootcamp 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 FastAPI Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Why Metering Matters in SaaS
In a multi-tenant SaaS, every tenant shares the same FastAPI app but pays based on what they actually use. To support plans like Free / Pro / Enterprise you need three cooperating subsystems:
- Metering — count consumed resources (API calls, tokens, rows, GB).
- Quotas — block or throttle once a tenant exceeds its plan limit.
- Billing hooks — emit events that downstream billing (Stripe, etc.) turns into invoices.
The hard part is doing this per tenant, accurately, and without adding latency to every request. We will build each piece in this lesson.
Modeling Plans and Quotas
Start by modeling what each plan allows. A plan maps a metric (what we count) to a limit and a period (when the counter resets). Keeping this as plain data makes it easy to load from a DB or config.
Below, a metered plan tracks API requests and AI tokens with monthly limits. None means unlimited.
from dataclasses import dataclass
@dataclass(frozen=True)
class Quota:
metric: str
limit: int | None # None = unlimited
period: str # 'day' or 'month'
PLANS = {
"free": [Quota("api_calls", 1_000, "month"), Quota("ai_tokens", 50_000, "month")],
"pro": [Quota("api_calls", 100_000, "month"), Quota("ai_tokens", 5_000_000, "month")],
"enterprise": [Quota("api_calls", None, "month"), Quota("ai_tokens", None, "month")],
}
def limit_for(plan: str, metric: str) -> int | None:
for q in PLANS[plan]:
if q.metric == metric:
return q.limit
return 0 # metric not allowed on this plan
print(limit_for("free", "api_calls")) # 1000
print(limit_for("enterprise", "ai_tokens")) # None (unlimited)
print(limit_for("free", "unknown")) # 0An Atomic Usage Counter
The core of metering is a counter you can increment atomically per (tenant, metric, period). In production this is usually Redis (INCRBY + EXPIRE) or a Postgres UPSERT with an atomic +=.
Here is the concept modeled in pure Python so you can see the key and the increment logic. The period key (e.g. 2026-06) is what makes the counter reset every month automatically.
from collections import defaultdict
from datetime import datetime, timezone
usage = defaultdict(int)
def period_key(period: str, now: datetime) -> str:
if period == "month":
return now.strftime("%Y-%m")
return now.strftime("%Y-%m-%d")
def incr(tenant: str, metric: str, amount: int, period: str, now: datetime) -> int:
key = (tenant, metric, period_key(period, now))
usage[key] += amount
return usage[key]
now = datetime(2026, 6, 10, tzinfo=timezone.utc)
print(incr("acme", "api_calls", 1, "month", now)) # 1
print(incr("acme", "api_calls", 1, "month", now)) # 2
print(incr("acme", "ai_tokens", 1200, "month", now)) # 1200Enforcing a Quota
Quota enforcement combines the plan limit with the current counter. The decision is simple but the order matters: you usually want to check-then-increment so that a request that would exceed the limit is rejected before doing the work.
For atomicity in real systems you increment first, then compare against the limit, and if you went over, you reject and optionally decrement (or simply let the counter sit over-limit, blocking further calls).
def check_quota(current: int, amount: int, limit: int | None) -> bool:
"""Return True if `amount` more units are allowed."""
if limit is None:
return True # unlimited
return current + amount <= limit
print(check_quota(995, 1, 1000)) # True (996 <= 1000)
print(check_quota(1000, 1, 1000)) # False (1001 > 1000)
print(check_quota(9_000_000, 1, None)) # True (unlimited)Wiring It Into FastAPI with a Dependency
In FastAPI the clean place to enforce quotas is a dependency. It resolves the tenant (from auth/JWT), looks up the plan, checks the quota, and raises HTTPException(429) when the limit is hit.
Returning 402 Payment Required is also common for hard plan caps, while 429 Too Many Requests fits rate-style limits. This is framework code, so it is not standalone-runnable.
from fastapi import Depends, HTTPException, Request
async def enforce_quota(metric: str, cost: int = 1):
async def _dep(request: Request):
tenant = request.state.tenant_id
plan = await get_plan(tenant)
limit = limit_for(plan, metric)
current = await store.get(tenant, metric, "month")
if not check_quota(current, cost, limit):
raise HTTPException(
status_code=429,
detail=f"Quota exceeded for {metric}",
headers={"X-Quota-Limit": str(limit)},
)
await store.incr(tenant, metric, cost, "month")
return current + cost
return _dep
@app.post("/v1/complete")
async def complete(used=Depends(enforce_quota("ai_tokens", cost=500))):
return {"ok": True, "tokens_used_this_month": used}Metering AFTER the Work for Variable Costs
Some costs are not known up front. AI token usage, bytes processed, or rows returned are only known after the handler runs. For these, do a cheap pre-check (is the tenant already over?) and record the real cost afterward.
A reliable pattern is a context manager that admits the request, runs the body, then reports actual usage — even on exceptions, so you don't lose metering data.
from contextlib import contextmanager
@contextmanager
def meter(record, tenant, metric):
actual = {"amount": 0}
try:
yield actual # caller sets actual['amount']
finally:
if actual["amount"]:
record(tenant, metric, actual["amount"])
log = []
with meter(lambda t, m, a: log.append((t, m, a)), "acme", "ai_tokens") as use:
# ... do real work, then we learn the true cost ...
use["amount"] = 1342
print(log) # [('acme', 'ai_tokens', 1342)]Soft Limits, Hard Limits and Overage
Real billing rarely flips from 'works' to '403' at exactly the limit. Plans usually define:
- Soft limit — allow but warn (email,
X-Quota-Warningheader) around ~80%. - Hard limit — block once exceeded (Free plans).
- Overage — keep serving but bill each extra unit at a metered rate (Pro/Enterprise).
Encode this as a policy so the enforcement code stays declarative.
def classify(current: int, limit: int | None, overage_allowed: bool) -> str:
if limit is None:
return "ok"
if current < 0.8 * limit:
return "ok"
if current < limit:
return "warn"
return "overage" if overage_allowed else "blocked"
print(classify(700, 1000, False)) # ok
print(classify(850, 1000, False)) # warn
print(classify(1200, 1000, True)) # overage
print(classify(1200, 1000, False)) # blockedEmitting Billing Events
Metering produces a stream of usage events. Billing systems (Stripe Billing Meters, Orb, Metronome) consume these to compute invoices. The golden rules for billing events:
- Include an idempotency key so retries don't double-charge.
- Carry a timestamp so events land in the correct billing period.
- Make them immutable — append-only, never edit.
Build the event payload as a plain, serializable record.
import uuid, json
from datetime import datetime, timezone
def billing_event(tenant: str, metric: str, qty: int, ts: datetime, request_id: str) -> dict:
return {
"id": str(uuid.uuid4()),
"idempotency_key": f"{tenant}:{metric}:{request_id}",
"tenant_id": tenant,
"metric": metric,
"quantity": qty,
"occurred_at": ts.isoformat(),
}
evt = billing_event("acme", "ai_tokens", 1342,
datetime(2026, 6, 10, tzinfo=timezone.utc), "req_9f3")
print(json.dumps(evt, indent=2))Decoupling with a Background Queue
Never call your billing provider synchronously inside the request path — a slow or down Stripe API would break your API. Instead, write the usage event to a durable buffer and flush it asynchronously.
FastAPI's BackgroundTasks works for low volume; for real scale use an outbox table, Redis stream, or a worker (Celery/ARQ). The handler stays fast; a separate consumer ships events to billing with retries and idempotency.
from fastapi import BackgroundTasks
@app.post("/v1/complete")
async def complete(bg: BackgroundTasks, request: Request):
tenant = request.state.tenant_id
result, tokens = await run_completion(request)
await store.incr(tenant, "ai_tokens", tokens, "month")
evt = billing_event(tenant, "ai_tokens", tokens,
datetime.now(timezone.utc), request.state.request_id)
bg.add_task(emit_to_billing, evt) # fire-and-forget, off the hot path
return resultReconciliation: Trust but Verify
Counters drift. A crashed worker, a lost event, or a Redis flush can desync your metering from reality. Robust SaaS systems treat the append-only event log as the source of truth and periodically reconcile the fast counters against it.
A nightly job re-aggregates raw events per tenant/metric/period and corrects the cached counter. This also lets you regenerate billing if a provider rejected a batch.
from collections import defaultdict
def reconcile(events: list[dict]) -> dict:
"""Rebuild authoritative totals from the immutable event log."""
totals = defaultdict(int)
seen = set()
for e in events:
if e["idempotency_key"] in seen:
continue # dedupe replays
seen.add(e["idempotency_key"])
period = e["occurred_at"][:7] # YYYY-MM
totals[(e["tenant_id"], e["metric"], period)] += e["quantity"]
return dict(totals)
events = [
{"idempotency_key": "acme:t:1", "tenant_id": "acme", "metric": "t", "quantity": 100, "occurred_at": "2026-06-01T10:00:00"},
{"idempotency_key": "acme:t:1", "tenant_id": "acme", "metric": "t", "quantity": 100, "occurred_at": "2026-06-01T10:00:00"},
{"idempotency_key": "acme:t:2", "tenant_id": "acme", "metric": "t", "quantity": 50, "occurred_at": "2026-06-02T09:00:00"},
]
print(reconcile(events)) # {('acme','t','2026-06'): 150}Tenant Isolation and Hot Keys
A few operational rules keep per-tenant metering correct and fast:
- Always scope the counter key by tenant_id — never share a global counter, or one noisy tenant blocks others.
- Watch for hot keys: a huge tenant hammering one Redis key can become a bottleneck; shard the key (
tenant:metric:shard) and sum on read. - Make limit checks fail-open or fail-closed deliberately — if the counter store is down, decide whether to allow (availability) or block (revenue protection).
- Reset boundaries must respect the tenant's billing-cycle timezone, not just UTC month.
Quick Check
Test your understanding of the metering decision flow.
Recap
You built a complete per-tenant metering and billing pipeline:
- Plans & quotas as data: metric to limit to reset period, with
Nonefor unlimited. - Atomic counters keyed by
(tenant, metric, period)so limits reset automatically. - Enforcement via a FastAPI dependency returning
429/402; check-then-increment for fixed costs, meter-after for variable costs. - Policies for soft limits, hard limits, and overage instead of a single hard cutoff.
- Billing events that are immutable, timestamped, and idempotent, shipped asynchronously off the request path.
- Reconciliation from an append-only event log as the source of truth, plus tenant-isolation and hot-key care.
With these pieces you can support usage-based pricing safely without slowing down your API.
AI 튜터와 함께 FastAPI Backend Development Bootcamp을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 21
- 레슨
- 84
자주 묻는 질문
“사용량 측정, 할당량 및 결제 연결 지점” 강의는 무료인가요?
네 — “사용량 측정, 할당량 및 결제 연결 지점” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 FastAPI Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
“사용량 측정, 할당량 및 결제 연결 지점”에서 뭘 배우나요?
테넌트별 사용량을 추적하고 요금제 할당량을 적용하며 사용량 기반 SaaS 가격 책정을 위한 결제 이벤트를 생성합니다. 브라우저에서 직접 실행하는 실습 코드로 FastAPI Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
FastAPI Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 FastAPI Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“사용량 측정, 할당량 및 결제 연결 지점” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 FastAPI Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 FastAPI Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 테넌트 격리 전략과 트레이드오프
- 테넌트 컨텍스트 확인과 미들웨어
- 행 수준 보안과 데이터 분할
- 사용량 측정, 할당량 및 결제 연결 지점