Diagnosing Epsilon Budget Exhaustion Across Releases

Each differentially private release over the same source spends part of a finite privacy budget, and a pipeline that accounts for epsilon per release instead of cumulatively will quietly exceed the total it promised — voiding the guarantee for every release, not just the last.

Part of Privacy-Preserving Generation Frameworks: the parent page defines how a single generation run allocates a differential privacy budget across its mechanisms; this page addresses what happens across many runs over time, where composition adds those per-run costs together and an unaccounted-for total silently exhausts the budget the whole program was designed around.

Root Cause: Composition Accumulates Epsilon Across Releases

Differential privacy is a property of a mechanism, but privacy loss is a property of everything an adversary sees. A mechanism M\mathcal{M} is (ε,δ)(\varepsilon, \delta)-differentially private when, for adjacent datasets D,DD, D' differing in one record and any output set SS,

Pr[M(D)S]eεPr[M(D)S]+δ.\Pr[\mathcal{M}(D) \in S] \le e^{\varepsilon}\,\Pr[\mathcal{M}(D') \in S] + \delta.

Composed epsilon over 52 weekly releases under three accounting rules, with the crossover marked The horizontal axis is the number of releases from one to fifty-two, a year of weekly publication; the vertical axis is the composed epsilon guarantee. Three curves are drawn from the standard closed forms, each release spending epsilon 0.1 with delta prime of one in a million. Basic composition is a straight line through the origin: epsilons simply add, reaching 5.2 after fifty-two releases. Advanced composition and the zero-concentrated conversion both start well above the origin, because their square-root terms carry a fixed cost paid on the very first release, and both grow slowly. The three curves cross in the mid-thirties: before the crossover, plain addition is the tightest available accounting, and a ledger hard-coded to advanced composition over-charges every release in the first two thirds of the year. After the crossover the square-root rules win and keep winning. A dashed line marks an annual ceiling of epsilon 3, and markers show where each curve crosses it — within three releases of each other, which is the point: no accounting rule rescues an over-ambitious release cadence, it only shifts the wall by a few weeks. The operational conclusion is that a ledger should evaluate every applicable rule and record the minimum, rather than committing to one. Basic composition is the tightest rule you have — until the crossover 1 13 26 39 52 0 1 2 3 4 5 releases against the same population (weekly) composed ε annual ceiling ε = 3 crossover at release 35 before it, Σ ε is the tightest rule you have 5.2 4.34 basic composition (Σ ε) advanced composition zCDP conversion Each release spends ε₀ = 0.1 with δ′ = 1e-6; standard closed forms, no simulation. The ceiling is reached — Σ ε at 31, advanced at 27, zCDP at 30 — so no rule rescues the cadence, it only moves the wall by a few weeks. A ledger should evaluate every applicable rule and record the minimum; parallel composition over provably disjoint regions is a separate rule again and costs max(ε).
Computed from the standard closed forms: plain addition is the tightest rule available for the first 34 releases, and no rule rescues an over-ambitious cadence.

The trap is that when you release kk times over the same source, an adversary sees all kk outputs, and the guarantee that governs them is the composition of the mechanisms, not any single one. Under basic sequential composition, epsilons and deltas simply add:

εtotal=i=1kεi,δtotal=i=1kδi.\varepsilon_{\text{total}} = \sum_{i=1}^{k} \varepsilon_i, \qquad \delta_{\text{total}} = \sum_{i=1}^{k} \delta_i.

Ten monthly releases at ε=0.5\varepsilon = 0.5 each do not cost ε=0.5\varepsilon = 0.5; they cost ε=5.0\varepsilon = 5.0. A program that set a lifetime ceiling of εmax=2.0\varepsilon_{\max} = 2.0 blew through it after the fourth release, and every release from the fifth onward carries a guarantee weaker than the one promised to data subjects. Two refinements change the arithmetic but not the principle. Advanced composition tightens the growth to roughly εtotal2kln(1/δ)ε+kε(eε1)\varepsilon_{\text{total}} \approx \sqrt{2k \ln(1/\delta')}\,\varepsilon + k\varepsilon(e^{\varepsilon}-1), sublinear in kk at the cost of a slack term δ\delta'. Parallel composition is the escape hatch: mechanisms applied to disjoint partitions of the source cost only the maximum of their epsilons, not the sum, because no record appears in more than one partition. The failure is not the math — it is not tracking the running total at all. A per-release check that each individual εiεmax\varepsilon_i \le \varepsilon_{\max} passes forever while the composed total climbs without limit.

The diagnosis, therefore, is an accounting problem. You need a persistent ledger that records the epsilon and delta of every release against a source, knows whether each new release is sequential (same or overlapping data) or parallel (disjoint partition), and computes the composed total the way an adversary would. The per-coordinate mechanics of spending that budget live in implementing differential privacy for coordinate generation, and the choice between Laplace, Gaussian, and their composition behaviour is compared in differential privacy mechanisms compared for spatial data; this page assumes those choices are made and focuses on adding them up correctly over time.

Composed epsilon accumulating across releases against a fixed budget ceiling, with the over-spending release blocked A horizontal budget track runs across the figure with a dashed threshold line marking epsilon-max equals 2.0. Five release segments accumulate from left to right. Release one spends 0.5 and release two another 0.5, both sequential, bringing the cumulative epsilon to 1.0. Release three adds 0.5 sequentially, reaching 1.5. Release four adds 0.7 sequentially and crosses the epsilon-max threshold, marking the point of budget exhaustion where the composed guarantee exceeds the promised ceiling. Release five is drawn past the ceiling in the accent colour and stamped BLOCKED, showing the CI gate refusing the over-spending release. A small inset at the lower right contrasts two composition rules: sequential composition over overlapping data adds epsilons, while parallel composition over disjoint spatial partitions costs only the maximum epsilon. Composed ε across releases over one source R1+0.5 R2+0.5 R3+0.5 R4+0.5 fits +0.2over εₛₓₓ = 2.0 Σ=0.5Σ=1.0 Σ=1.5Σ=2.2 R5 BLOCKED CI gate budget exhausted at R4: composed ε > εₛₓₓ Composition rule decides the cost Sequential · overlapping source releases touch the same records cost = ε₁ + ε₂ + … + εₖ (adds up) Parallel · disjoint partitions no record in two partitions cost = max(ε₁, ε₂, …, εₖ) (the escape hatch)
Sequential releases over the same source add their epsilons until the composed total crosses the ceiling; only disjoint partitions escape the sum. The ledger's job is to know which rule applies to each release and stop the one that overspends.

Prerequisite Check: See the Total the Per-Release Check Misses

Reproduce the blind spot. A guard that only checks each release’s individual epsilon against the ceiling passes indefinitely while the composed total marches past it.

python
# requirements.txt — pin majors; let patch releases float
numpy==1.26.*
python
releases = [0.5, 0.5, 0.5, 0.7, 0.6]   # epsilon spent by five sequential releases
EPS_MAX = 2.0

# The naive per-release guard — passes every time:
for i, eps in enumerate(releases, 1):
    assert eps <= EPS_MAX, f"release {i} over budget"   # 0.7 <= 2.0, always fine
print("per-release guard: all releases OK")

# What the adversary actually sees (basic sequential composition):
cumulative = 0.0
for i, eps in enumerate(releases, 1):
    cumulative += eps
    flag = "  <-- BUDGET EXHAUSTED" if cumulative > EPS_MAX else ""
    print(f"after release {i}: composed eps = {cumulative:.1f}{flag}")
# after release 4: composed eps = 2.2   <-- BUDGET EXHAUSTED

The per-release guard is the exhaustion bug in miniature: it never sees the sum, so it never fails, while the real guarantee was voided at release four.

Fix: A Persistent Epsilon Ledger with Composition-Aware Accounting

The fix is a ledger that persists across releases, records each spend with the partition it touched, and composes the total using the correct rule. Sequential spends over overlapping data add; parallel spends over disjoint partitions take the maximum within each partition group.

Anatomy of a privacy-ledger row and the accounting question each field answers A single wide record is drawn as nine adjacent fields, with callouts above and below. The fields are: release identifier, source population identifier, mechanism, epsilon, delta, composition mode, region key, timestamp, and artifact hash. The source population identifier answers which budget this spend draws against; without it, two releases over the same people are accounted as two independent budgets and the guarantee is silently doubled. The mechanism and its epsilon and delta answer what was actually spent; without the mechanism the composition rule cannot be chosen. The composition mode answers whether this release is sequential over the same records — in which case epsilons add — or parallel over a disjoint partition, in which case the cost is the maximum rather than the sum. The region key is what makes parallel composition checkable: two releases may only be accounted in parallel if their region keys are provably disjoint. The timestamp anchors the accounting window, and the artifact hash makes the row auditable against the thing that was actually published. A note records that a ledger without a composition mode and a region key can only ever compute the pessimistic sum, which is why teams that omit them appear to exhaust their budget far faster than they truly do. Nine fields, and what each one is the only field able to answer release_id which release source_pop_id whose budget mechanism which rule applies epsilon what was spent delta failure probability composition add or max? region_key disjoint from what? issued_at which window artifact_sha against what artifact one row per release, appended, never updated Without source_pop_id two releases over the same people become two independent budgets — the guarantee silently doubles and nothing in the pipeline notices. Without composition + region_key the ledger can only compute the pessimistic sum; disjoint regional releases are charged as if they overlapped, and the budget appears to vanish. Without artifact_sha the row is an assertion about a release nobody can identify — the audit reduces to trusting the pipeline that wrote it. The ledger is append-only: a corrected spend is a new row that references the old one, never an edit.
Nine fields, each the only one able to answer its question — drop the composition mode and the region key and the ledger can only ever compute the pessimistic sum.

Step 1 — Record every spend. Each entry carries the epsilon, delta, a timestamp, and a partition key identifying which slice of the source it touched. Persist the ledger (a JSON file, a table) so it survives across CI runs and releases.

python
import json
from dataclasses import dataclass, asdict
from pathlib import Path

@dataclass(frozen=True)
class Spend:
    release_id: str
    epsilon: float
    delta: float
    partition: str        # disjoint partition key; "GLOBAL" means whole source
    ts: str

class BudgetLedger:
    def __init__(self, path: Path):
        self.path = Path(path)
        self.entries: list[Spend] = [
            Spend(**e) for e in json.loads(self.path.read_text())
        ] if self.path.exists() else []

    def record(self, spend: Spend) -> None:
        self.entries.append(spend)
        self.path.write_text(json.dumps([asdict(e) for e in self.entries], indent=2))

Step 2 — Compose within and across partitions. Group spends by partition; within a group they are sequential and add; across disjoint groups they are parallel and the total is the maximum group cost. This is the accounting the privacy-preserving generation frameworks contract requires every release to honour.

python
from collections import defaultdict

def composed_epsilon(entries: list[Spend]) -> float:
    """Sequential within a partition (sum); parallel across partitions (max)."""
    per_partition: dict[str, float] = defaultdict(float)
    for e in entries:
        per_partition[e.partition] += e.epsilon      # same records -> add
    return max(per_partition.values(), default=0.0)  # disjoint slices -> take the max

def composed_delta(entries: list[Spend]) -> float:
    per_partition: dict[str, float] = defaultdict(float)
    for e in entries:
        per_partition[e.partition] += e.delta
    return max(per_partition.values(), default=0.0)

Step 3 — Admit a release only if it fits. Before publishing, simulate the spend against the ledger and refuse it if the projected composed total would breach either ceiling. Refusing before the write is what keeps the guarantee intact rather than discovering the breach afterward.

python
def can_release(ledger: BudgetLedger, spend: Spend, *,
                eps_max: float, delta_max: float) -> tuple[bool, float]:
    projected = ledger.entries + [spend]              # what the total WOULD be
    eps = composed_epsilon(projected)
    ok = eps <= eps_max and composed_delta(projected) <= delta_max
    return ok, eps

Verification Step: Block Over-Spending Releases in CI

Turn admission into a build gate. The CI job loads the persisted ledger, projects the pending release, and fails the build if it would exhaust the budget — so an over-spending release cannot merge, let alone publish. This is the privacy counterpart to the black-box check in running membership inference attacks on synthetic geodata: the ledger proves the budget was respected, the attack confirms the data behaves as if it was.

python
import pytest
from datetime import datetime, timezone

EPS_MAX, DELTA_MAX = 2.0, 1e-5

def test_release_blocked_when_budget_exhausted(tmp_path):
    ledger = BudgetLedger(tmp_path / "budget.json")
    now = lambda: datetime.now(timezone.utc).isoformat()

    # Three sequential releases over the GLOBAL source: composed eps = 1.5, still fine.
    for i, eps in enumerate([0.5, 0.5, 0.5], 1):
        spend = Spend(f"r{i}", eps, 2e-6, "GLOBAL", now())
        ok, total = can_release(ledger, spend, eps_max=EPS_MAX, delta_max=DELTA_MAX)
        assert ok, f"release {i} wrongly blocked at composed eps {total}"
        ledger.record(spend)

    # Fourth sequential release pushes composed eps to 2.2 > 2.0 -> must be blocked.
    over = Spend("r4", 0.7, 2e-6, "GLOBAL", now())
    ok, total = can_release(ledger, over, eps_max=EPS_MAX, delta_max=DELTA_MAX)
    assert not ok and total == pytest.approx(2.2), "over-spending release was not blocked"

    # The SAME epsilon on a DISJOINT partition is admissible: parallel composition.
    parallel = Spend("r4b", 0.7, 2e-6, "REGION_NORTH", now())
    ok, _ = can_release(ledger, parallel, eps_max=EPS_MAX, delta_max=DELTA_MAX)
    assert ok, "disjoint-partition release wrongly blocked"

The disjoint-partition assertion is the load-bearing subtlety: the gate must block 0.7 more on the global source yet admit the same 0.7 on a region that shares no records, because parallel composition costs the maximum, not the sum. A gate that treats every release as sequential is safe but needlessly starves the budget.

Edge Cases & Gotchas

Partitions that are not actually disjoint. Parallel composition only applies when no record appears in two partitions. If a spatial partition is defined by a bounding box and a moving object crosses the boundary, its record lands in both partitions and the two spends are no longer parallel — they must be summed. Antimeridian-spanning partitions are the classic trap: a box “east of 170°E” and one “west of 170°W” both claim the strip across the Pacific. Define partitions by a total, gap-free, overlap-free key (an H3 cell id, an administrative code) and assert the partition function is a true surjection before trusting the max rule.

Delta accumulates too, and advanced composition adds its own. It is easy to watch epsilon and forget delta, but delta sums under sequential composition and advanced composition introduces an additional slack term δ\delta' beyond the sum of per-release deltas. A program that gates only on epsilon can satisfy the epsilon ceiling while the composed delta drifts above its own limit, weakening the failure-probability side of the guarantee. Gate on both, and if you use advanced composition account for the extra δ\delta' explicitly.

Re-releasing a corrected dataset. Publishing a fixed version of a release over the same source is a new release, not a replacement — the adversary saw both. Deleting the buggy release from the ledger does not un-observe it. Mark superseded releases as still-spent rather than removing them, so the composed total reflects everything an adversary could have downloaded, including the version you wish you had never shipped.

Frequently Asked Questions

Why does releasing over the same source repeatedly cost more privacy?
Because an adversary sees every release, and the guarantee that governs them is the composition of all the mechanisms, not any single one. Under basic sequential composition the epsilons add, so ten releases at epsilon 0.5 each compose to epsilon 5.0. Any single release can look well within budget while the composed total has already exceeded the lifetime ceiling.
When can I use parallel composition to avoid summing epsilons?
Only when the releases touch strictly disjoint partitions of the source, so no record appears in more than one partition. Then the composed cost is the maximum epsilon across partitions rather than the sum. Verify the partitions are genuinely non-overlapping first: bounding boxes that share a boundary, or antimeridian-spanning regions, quietly double-count boundary-crossing records and break the max rule.
Does deleting a buggy release from the ledger recover its budget?
No. If the release was ever published, an adversary may have observed it, so its privacy cost is permanent regardless of what your ledger says afterward. Publishing a corrected version is an additional release whose cost composes with the original, not a replacement. Mark superseded releases as still-spent so the composed total reflects everything that was ever downloadable.
Should the CI gate track delta as well as epsilon?
Yes. Delta sums under sequential composition just as epsilon does, and advanced composition adds a further slack term beyond the sum of per-release deltas. A gate that watches only epsilon can pass while the composed delta drifts above its own ceiling, weakening the failure-probability side of the guarantee. Gate on both budgets and account for the extra delta term whenever you rely on advanced composition.