Storage
Unit-aware storage quota enforcement.
Unit-aware storage quota enforcement using MiB/GiB, absolute value metering with set() for sync-style usage tracking, observe mode for logging without blocking, and the pre-upload gate pattern that prevents you from accepting a file you'll then have to reject.
Policy
policy:
credits:
storage_mib:
description: Object storage in MiB
overhead_cost: 0.000023 # ~$0.023/GiB/month → $0.0000235/MiB
pricing_model: tiered
tiers:
- up_to: 10240 # first 10 GiB (in MiB)
price: { amount: 0.025 }
- up_to: 102400 # 10–100 GiB
price: { amount: 0.022 }
- # 100 GiB+
price: { amount: 0.020 }
stof_units: MiB
resets: false # storage accumulates — it does not reset
bandwidth_gib:
description: Egress bandwidth in GiB
overhead_cost: 0.00005
pricing_model: flat
price: { amount: 0.00009 }
stof_units: GiB
resets: true # bandwidth resets monthly
plans:
free:
label: Free
period: monthly
default: true
entitlements:
storage:
description: Object storage — hard limit, 1 GiB
limit:
credit: storage_mib
mode: hard
value: '1GiB' # unit string — resolves to 1024 MiB at load time
resets: false
bandwidth:
description: Egress bandwidth — hard limit, 5 GiB/month
limit:
credit: bandwidth_gib
mode: hard
value: 5
resets: true
reset_inc: 30days
pro:
label: Pro
period: monthly
entitlements:
storage:
description: Object storage — soft limit, 50 GiB, overage billed
limit:
credit: storage_mib
mode: soft
value: '50GiB'
resets: false
bandwidth:
description: Egress bandwidth — soft limit, 100 GiB/month, overage billed
limit:
credit: bandwidth_gib
mode: soft
value: 100
resets: true
reset_inc: 30days
enterprise:
label: Enterprise
period: monthly
entitlements:
storage:
description: Object storage — observe mode, metered with no enforcement
limit:
credit: storage_mib
mode: observe
resets: false
bandwidth:
description: Egress bandwidth — soft limit, 1 TiB/month, overage billed
limit:
credit: bandwidth_gib
mode: soft
value: 1024
resets: true
reset_inc: 30days
Integration
import { Limitr } from '@formata/limitr';
import { readFileSync } from 'fs';
const policy = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml');
policy.addHandler('billing', (key: string, value: unknown) => {
if (key === 'meter-overage') {
const event = JSON.parse(value as string);
billing.queueCharge({
customerId: event.customer.id,
entitlement: event.entitlement,
units: event.overage,
credit: event.credit.description,
});
}
});
// ── Upload: gate before accepting the file ────────────────────────────────────
async function handleUpload(customerId: string, file: File) {
// Gate before accepting the multipart upload — don't buffer a file you'll reject.
// check() is read-only: tests whether allow() would succeed without consuming quota.
// Limitr converts bytes → MiB automatically via stof_units.
if (!await policy.check(customerId, 'storage', `${file.size}bytes`)) {
const remaining = await policy.remaining(customerId, 'storage');
return {
success: false,
error: `Storage quota exceeded. ${(remaining ?? 0).toFixed(1)} MiB remaining.`,
};
}
// Accept and store the file
const { key } = await objectStorage.put(file);
// Meter actual bytes stored — allow() enforces and meters in one operation
await policy.allow(customerId, 'storage', `${file.size}bytes`);
return { success: true, key };
}
// ── Delete: release quota on file removal ─────────────────────────────────────
async function handleDelete(customerId: string, fileKey: string) {
const file = await db.getFile(fileKey);
if (!file) return { error: 'File not found' };
await objectStorage.delete(fileKey);
await db.deleteFile(fileKey);
// Re-query actual storage and set() the meter to the real value.
// set() computes the delta and calls allow(newValue - currentMeter) internally.
const actualBytes = await db.getTotalStorageBytes(customerId);
await policy.set(customerId, 'storage', actualBytes / (1024 * 1024));
}
// ── Sync: reconcile meter with actual storage backend ─────────────────────────
// Call on a schedule or after bulk operations to prevent meter drift.
async function syncStorageUsage(customerId: string) {
const actualBytes = await objectStorage.getTotalBytes(customerId);
await policy.set(customerId, 'storage', actualBytes / (1024 * 1024));
}
// ── Bandwidth metering ────────────────────────────────────────────────────────
async function meterDownload(customerId: string, responseBytes: number) {
// Meter egress after the response is sent — byte count isn't known before
await policy.allow(customerId, 'bandwidth', responseBytes / (1024 ** 3));
}
// ── Usage display ─────────────────────────────────────────────────────────────
async function getStorageDisplay(customerId: string) {
const usedMiB = await policy.value(customerId, 'storage');
const limitMiB = await policy.limit(customerId, 'storage');
const remainingMiB = await policy.remaining(customerId, 'storage');
const usedPct = await policy.value(customerId, 'storage', true);
return {
used: { mib: usedMiB, gib: (usedMiB ?? 0) / 1024 },
limit: { mib: limitMiB, gib: (limitMiB ?? 0) / 1024 },
remaining: { mib: remainingMiB, gib: (remainingMiB ?? 0) / 1024 },
usedPct,
};
}
Notes
Unit string resolution — setting stof_units: MiB on the credit means Limitr treats all meter values as MiB. When you pass a unit string to allow() — '500MB', '2GiB', '1048576bytes' — Limitr converts it to MiB automatically before metering. You can use whatever unit is natural at the call site; the meter is always stored in the credit's canonical units. The limit value: '1GiB' in the policy resolves to 1024 MiB at policy load time.
Why set() instead of allow() for delete? — storage is not a delta-accumulating resource, it's a current state. set() computes allow(newValue - currentMeter) which handles both growth and shrinkage correctly. For maximum accuracy, re-query your storage backend's actual byte count and pass that to set() rather than computing the delta yourself.
observe mode for Enterprise — Enterprise storage is typically metered for reporting and invoicing rather than enforced against a hard cap. observe mode means the meter accumulates correctly for margin tracking and Cloud dashboards without ever blocking the customer. Combine with createCustomerOverride() to add a hard cap for any enterprise customer who requests one.
Bandwidth resets; storage doesn't — bandwidth is a flow metric that resets monthly because customers pay for what they transferred this period. Storage is a state metric that doesn't reset because what was stored last month is still stored this month. resets: false on storage_mib, resets: true on bandwidth_gib.
Pre-upload gating with check() — the pattern of check() before accepting the upload, then allow() after storing, is important for storage. If you called allow() upfront and the upload failed, you'd need to call set() to release the quota. Using check() to gate acceptance and allow() after successful storage is simpler and more correct.
Meter drift — with any delete-capable resource, the meter can drift from reality if deletes aren't always correctly reflected. The syncStorageUsage() function reconciles by reading the actual storage backend and calling set(). Run it on a schedule (daily cron, or after bulk operations) to prevent compounding drift.