TL;DR: Agree on partial-hour billing and rounding before calculating a charge. Use decimal rates, validate each usage interval, and apply credits after summing the charges according to that policy.
How to approach it
Ask whether partial hours are proportional, where rounding occurs, and how billing periods and rate changes affect an interval. Establish whether the input contains trusted allocation intervals or raw events that still need pairing and deduplication. Then implement the calculation and test the boundaries that could change an invoice.
A strong answer
Suppose a tenant uses four GPUs for seven and a half minutes. At an illustrative rate of $1.10 per GPU-hour, proportional billing charges 4 × 1.10 × 0.125 = $0.55. Rounding the duration up to a whole hour charges $4.40. Both calculations can implement a policy; the programmer must not choose between them accidentally.
For this example, rates are decimal strings, durations are proportional, and each record is rounded to cents using half-up rounding. The invoice sums these rounded line items, subtracts a nonnegative credit, and floors the amount due at zero. Other contracts may round only the invoice total or retain finer precision in line items. Follow the agreed contract consistently.
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_UP
RATES = {"h100": Decimal("2.50"), "a100": Decimal("1.10"), "l4": Decimal("0.35")}
CENT = Decimal("0.01")
@dataclass(frozen=True)
class Usage:
tenant: str
gpu_type: str
count: int
start: datetime
end: datetime
def hours(u):
if any(t.tzinfo is None or t.utcoffset() is None for t in (u.start, u.end)):
raise ValueError("timestamps must be timezone-aware")
delta = u.end.astimezone(timezone.utc) - u.start.astimezone(timezone.utc)
micros = (delta.days * 86400 + delta.seconds) * 1_000_000 + delta.microseconds
if micros <= 0:
raise ValueError("end must be after start")
return Decimal(micros) / Decimal(3_600_000_000)
def cost(u):
if type(u.count) is not int or u.count <= 0:
raise ValueError("count must be a positive integer")
if u.gpu_type not in RATES:
raise ValueError("unknown GPU type")
raw = RATES[u.gpu_type] * u.count * hours(u)
return raw.quantize(CENT, rounding=ROUND_HALF_UP)
def bill(records, free_credit=Decimal("0")):
if not isinstance(free_credit, Decimal) or not free_credit.is_finite() or free_credit < 0:
raise ValueError("credit must be a finite nonnegative Decimal")
totals = {}
for u in records:
totals[u.tenant] = totals.get(u.tenant, Decimal("0")) + cost(u)
return {t: max(Decimal("0"), total - free_credit).quantize(CENT, rounding=ROUND_HALF_UP)
for t, total in totals.items()}
def dt(s):
return datetime.fromisoformat(s).replace(tzinfo=timezone.utc)
u = Usage("acme", "a100", 4, dt("2026-09-01T10:00:00"), dt("2026-09-01T10:07:30"))
assert cost(u) == Decimal("0.55")
assert bill([u], Decimal("50")) == {"acme": Decimal("0.00")}
month = Usage("acme", "h100", 1, dt("2026-08-31T23:00:00"), dt("2026-09-01T01:00:00"))
assert cost(month) == Decimal("5.00")
assert bill([u, month]) == {"acme": Decimal("5.55")}
for bad in [Usage("t", "unknown", 1, u.start, u.end),
Usage("t", "a100", 0, u.start, u.end),
Usage("t", "a100", 1, u.end, u.start)]:
try:
cost(bad)
except ValueError:
pass
else:
raise AssertionError("invalid usage accepted")
The duration calculation uses integer microseconds before converting to Decimal. Converting the float returned by total_seconds() into Decimal would preserve that float's approximation, not recover the original exact duration. Decimal division can still round to its configured precision; for a production calculator, choose and test the precision against the supported amount and duration ranges.
Binary floats do not represent every decimal fraction exactly. The size of an invoice error depends on the operations and rounding boundaries, so there is no universal record count at which floats lose a cent. Decimal makes the intended decimal arithmetic and rounding explicit. Integer subunits are another option when the chosen scale represents all intermediate charges; integer cents alone cannot exactly represent every prorated rate.
| Test | What it establishes |
|---|---|
| Seven-and-a-half-minute interval | Partial hours survive the calculation |
| Interval across midnight | Duration remains correct across a date boundary |
| Zero, negative or boolean count | Invalid quantities are rejected |
| Credit larger than charges | Amount due stops at zero |
| Half-cent boundary | The agreed rounding rule is applied |
The midnight test does not implement monthly allocation. This calculator prices an entire interval at one rate. If the contract splits usage across billing periods or rate changes, split the interval first and define how rounding applies to those pieces. Likewise, duplicated records must be handled before this loop using a stable usage identifier; matching timestamps alone do not prove two allocations are duplicates.
The GPU Credit Scheduler Pattern explains how reservation and usage events feed billing. If the input is an event log, first reconstruct valid intervals and resolve missing or repeated events. A short arithmetic function cannot repair ambiguous source data.
What interviewers probe next
- "Why round per record?" It is this example's policy, so the displayed line items sum to the charge before credits.
- "What happens when a rate changes?" Split the interval at the effective boundary and apply the versioned rates.
- "Does rejecting a record mean crashing the entire service?" No. Surface an explicit error or quarantine it for reconciliation; never silently turn it into zero usage.
- "What about duplicate input?" Require an allocation or usage identity and enforce deduplication before charging.
Common mistakes
- Treating a rounding policy as an implementation detail.
- Converting a float to Decimal after precision has already been lost.
- Accepting negative GPU counts or mixing naive and aware timestamps.
- Claiming a duration test also proves billing-period allocation.
Key takeaways
- Four GPUs × $1.10/hour × 0.125 hours = $0.55 under proportional billing.
- Decimal arithmetic still needs an explicit precision and rounding policy.
- Validate the interval and quantity before calculating money.
