Counting how much storage a tenant uses

Keep one honest number — bytes used — and nudge it on every write and delete, so you always know whether the next write still fits.

The idea

A storage quota caps how much a tenant, account, or bucket may hold. The system keeps a running used counter: every write adds the object's size, every delete subtracts it, and an overwrite adjusts by the delta between old and new size.

A write is allowed only when used + size <= limit. The whole scheme depends on that counter staying honest, so the update has to be atomic — otherwise two concurrent writes can step on each other and the number drifts away from reality. A soft limit warns as you approach; a hard limit rejects the write outright.

See it work

A tenant with a 10 GB limit. Step through a stream of operations.

How it works

The counter update is the whole game. On a write you reserve space and the counter; on a delete you release it. Both must be a single atomic operation against shared state so concurrent callers can't lose each other's updates.

# write: reserve size only if it still fits, atomically
def try_write(used, size, limit):
    # compare-and-add in one indivisible step (DB transaction,
    # Redis Lua, or atomic compare_exchange) — never read-then-write
    if atomic_add_if(used, size, max=limit):   # used += size iff used+size <= limit
        store_object(size)
        return "ok"
    return "rejected: would exceed limit"

# delete: always release what we accounted for
def on_delete(used, size):
    atomic_sub(used, size)                     # used -= size

# overwrite is just the delta
def on_overwrite(used, old_size, new_size):
    atomic_add(used, new_size - old_size)      # may be + or -

# EXACT accounting: the counter is updated synchronously inside the write
#   path — always correct, but the counter is a hot, contended row.
# APPROXIMATE accounting: writes update a fast local/sharded tally and a
#   background job rolls it up — cheap, but can briefly over/under-count.
# RECONCILIATION: a periodic scan re-sums true object sizes and overwrites
#   the counter, healing any drift from crashes or non-atomic paths.

Cost

What it costsWhy
Counter contentionExact accounting touches one hot counter on every write — a serialization point that caps write throughput per tenant.
Exact vs approximateExact is always correct but contended; approximate (sharded or async tallies) is cheap but can briefly over- or under-count near the limit.
Reconciliation scanRe-summing true usage means listing every object — O(objects) work, so it runs periodically (nightly), not per request.
DriftAny gap between the counter and true on-disk usage — from crashes, races, or missed decrements — costs either over-charging tenants or letting them exceed the cap.
Signals to watchused / limit ratio (warn band), reconciliation drift = counter − scanned, and the rejected-write rate (tenants hitting the wall).

Watch out for

Worked example

A tenant has a 10 GB limit. A run of writes brings used to 7 GB — past a 7.5 GB soft line we'd warn on soon. The tenant deletes a 2 GB object, so used drops to 5 GB and there's room to grow again.

But a buggy concurrent delete path skips its decrement, so the counter still reads high while real usage is lower — that gap is drift. A nightly reconciliation re-scans the actual objects, finds 5 GB of real data, and overwrites the counter back to 5 GB. The accounting is honest again, and the next write is judged against the true number.

Check yourself

1. Two writes of 1 GB each arrive at the same instant for a tenant at used = 8 GB, limit = 10 GB. The code does used = used + size (a plain read-then-write). What's the danger?

2. After crashes left the counter reading higher than what's actually stored, what fixes it?