How a 95th Percentile Bandwidth Commit Becomes a Bill
Last edited on August 11, 2026

Start with the billing window: a 95th-percentile bandwidth bill is built from sampled rates, not from total transferred bytes and not from the single highest spike. A common contract records five-minute inbound and outbound rates, discards a defined highest 5% tail, keeps the largest surviving value under its direction rule, and compares that result with the committed Mbps.

The arithmetic is short. Contracts create the divergence: providers may treat directions separately, take the larger direction within every interval, aggregate ports, round the discarded count differently, or define missing data and attack traffic in their own terms. Reproduce the provider’s exact rule before choosing a commit.

Use this article if you buy dedicated-server or colocation capacity and can export CSV and run Python 3. No switch configuration or production traffic change is required. The worked lab uses synthetic, secret-free data so every row and failure control can be inspected; replace it with an authorized export only after the calculator matches the provider’s written method.

A Rate Contract, Not a Transfer Total

Three numbers that often appear beside one another answer different questions:

  • Port speed is a capacity ceiling such as 1 Gbps. It does not automatically state the committed rate or monthly transfer allowance.
  • Transfer volume counts bytes over time, usually in GB or TB. A 20 TB quota can be exhausted by many different traffic shapes.
  • A 95th-percentile rate describes the highest surviving interval after a contract-defined tail is removed. It is expressed in Mbps or Gbps.

Stackscale’s billing-method explanation describes the familiar 30-day example: 8,640 five-minute samples and roughly 432 discarded values. HostDime’s provider-specific calculation adds an important detail: its source is the switch port serving the equipment, and its billing window follows the actual calendar month. Those facts are contract inputs, not universal defaults.

Short bursts can disappear from the billed tail. Sustained busy periods cannot. If a campaign, backup, replication job, or stream stays above the commit for longer than the discarded fraction, some high samples survive and raise the billed rate. Averages hide that boundary because they spread a busy period across every quiet interval.

Contract reading follows the discipline in hosting uptime SLA calculations: identify the measurement owner, window, inclusion rules, exclusions, and remedy before treating a percentage as comparable.

Freeze the Provider Rules Before Calculating

Ask for a raw or exportable sample history and write one rule sheet beside it. A dashboard graph is helpful for orientation, but a screenshot cannot reveal every row used for the invoice.

Contract field What to record Why it changes the result
Meter and interval Port/interface, UTC timestamps, average or peak, exact seconds Different sources and windows create different rates
Billing cycle Start/end timestamps and expected row count February, a 30-day period, and a partial first month are not equivalent
Direction rule Separate percentiles then larger; per-interval maximum; sum; billed direction only Non-overlapping inbound and outbound bursts can produce very different bills
Tail and rounding Exactly how many rows are removed and how fractional counts round A boundary sample may become the highest survivor or be discarded
Commit and overage Included Mbps, rounding unit, currency, per-Mbps rate or tier The percentile alone is not the invoice
Exceptions Missing samples, maintenance, scrubbing, attacks, pooled ports, canceled service Silent substitution can move the ranking and contract liability

Run repeatable route-and-direction speed tests to answer whether a path can carry a workload. Do not substitute a few active tests for the provider’s billing ledger: a speed test measures selected moments, whereas billing needs the complete declared window.

When counters originate from SNMP, interface identity and counter width matter. RFC 2863 defines high-capacity ifHCInOctets and ifHCOutOctets for interfaces where 32-bit counters can wrap too quickly. The collector still owns counter resets, timestamp alignment, conversion to bits, and division by the real interval. This lab begins after those rates have been exported.

Recreate the Invoice From a Five-Minute CSV

Keep the blocks in one shell so LAB remains available. The bootstrap refuses an existing path and writes a marker that later cleanup must verify.

set -Eeuo pipefail
LAB=/tmp/voxfor-bandwidth-p95-lab
MARKER="$LAB/.voxfor-bandwidth-p95-lab"

if [[ -e "$LAB" ]]; then
  printf 'Refusing existing lab path: %s\n' "$LAB" >&2
  exit 1
fi
mkdir -m 700 "$LAB"
printf '%s\n' 'voxfor-bandwidth-p95-lab-v1' > "$MARKER"
python3 --version

Build a fixture with two different burst directions

Build 300 consecutive UTC rows with exactly 15 inbound burst intervals and a different 15 outbound burst intervals. That separation is deliberate: it exposes direction-policy differences that a single shared spike cannot show.

python3 - "$LAB/traffic.csv" <<'PY'
import csv
import datetime as dt
import sys

target = sys.argv[1]
start = dt.datetime(2026, 8, 1, tzinfo=dt.timezone.utc)
with open(target, "w", newline="", encoding="utf-8") as handle:
    writer = csv.writer(handle)
    writer.writerow(["timestamp_utc", "in_mbps", "out_mbps"])
    for index in range(300):
        inbound = 18.0 + (index % 20) * 0.3
        outbound = 30.0 + (index % 30) * 0.4
        if 40 <= index < 55:
            inbound = 180.0 + (index - 40)
        if 140 <= index < 155:
            outbound = 140.0 + (index - 140)
        stamp = start + dt.timedelta(minutes=5 * index)
        writer.writerow([
            stamp.isoformat().replace("+00:00", "Z"),
            f"{inbound:.2f}",
            f"{outbound:.2f}",
        ])
print(f"fixture={target} samples=300 interval_seconds=300")
PY

Create a fail-closed highest-survivor calculator

Next, implement one declared provider-style rule: remove ceil(N × 0.05) highest values and choose the next survivor. That is not interchangeable with every statistical software default. Confirm the provider’s tail and rounding method, then change the named rule rather than silently relying on interpolation.

cat > "$LAB/calculate_p95.py" <<'PY'
#!/usr/bin/env python3
import argparse
import csv
import datetime as dt
import math
import pathlib

def parse_utc(value):
    parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
    if parsed.utcoffset() != dt.timedelta(0):
        raise ValueError(f"timestamp is not UTC: {value}")
    return parsed

def highest_survivor(values, discard_count):
    ordered = sorted(values)
    if discard_count <= 0 or discard_count >= len(ordered):
        raise ValueError("discard count leaves no auditable survivor set")
    return ordered[-discard_count - 1]

parser = argparse.ArgumentParser()
parser.add_argument("csv_path", type=pathlib.Path)
parser.add_argument("--expected-samples", type=int, required=True)
parser.add_argument("--interval-seconds", type=int, default=300)
parser.add_argument("--commit-mbps", type=float, required=True)
parser.add_argument("--overage-per-mbps", type=float, default=0.0)
parser.add_argument("--show-discarded", action="store_true")
args = parser.parse_args()

with args.csv_path.open(newline="", encoding="utf-8") as handle:
    rows = list(csv.DictReader(handle))
required = {"timestamp_utc", "in_mbps", "out_mbps"}
if not rows or not required.issubset(rows[0]):
    raise SystemExit("ERROR schema must contain timestamp_utc,in_mbps,out_mbps")
if len(rows) != args.expected_samples:
    raise SystemExit(
        f"ERROR expected {args.expected_samples} samples, found {len(rows)}"
    )

timestamps = [parse_utc(row["timestamp_utc"]) for row in rows]
if len(set(timestamps)) != len(timestamps):
    raise SystemExit("ERROR duplicate timestamp")
steps = [
    (right - left).total_seconds()
    for left, right in zip(timestamps, timestamps[1:])
]
if any(step != args.interval_seconds for step in steps):
    raise SystemExit(
        f"ERROR expected a constant {args.interval_seconds}-second interval"
    )

inbound = [float(row["in_mbps"]) for row in rows]
outbound = [float(row["out_mbps"]) for row in rows]
if any(value < 0 or not math.isfinite(value) for value in inbound + outbound):
    raise SystemExit("ERROR rates must be finite non-negative Mbps")

discard_count = math.ceil(len(rows) * 0.05)
p95_in = highest_survivor(inbound, discard_count)
p95_out = highest_survivor(outbound, discard_count)
separate_then_max = max(p95_in, p95_out)
interval_max = highest_survivor(
    [max(i, o) for i, o in zip(inbound, outbound)], discard_count
)
average_total = sum(i + o for i, o in zip(inbound, outbound)) / len(rows)
observed_gb = sum(
    (i + o) * 1_000_000 / 8 * args.interval_seconds
    for i, o in zip(inbound, outbound)
) / 1_000_000_000
sustained_30d_tb = (
    separate_then_max * 1_000_000 / 8 * 30 * 86400 / 1_000_000_000_000
)
overage = max(0.0, separate_then_max - args.commit_mbps)

print(
    f"samples={len(rows)} interval_seconds={args.interval_seconds} "
    f"discarded_each_direction={discard_count}"
)
print(f"p95_in_mbps={p95_in:.2f} p95_out_mbps={p95_out:.2f}")
print(f"billable_separate_then_max_mbps={separate_then_max:.2f}")
print(f"alternative_per_interval_max_mbps={interval_max:.2f}")
print(
    f"average_total_mbps={average_total:.2f} "
    f"observed_transfer_gb={observed_gb:.3f}"
)
print(f"p95_if_sustained_30d_tb={sustained_30d_tb:.3f}")
print(
    f"commit_mbps={args.commit_mbps:.2f} overage_mbps={overage:.2f} "
    f"overage_cost={overage * args.overage_per_mbps:.2f}"
)
if args.show_discarded:
    inbound_tail = sorted(inbound, reverse=True)[:discard_count]
    outbound_tail = sorted(outbound, reverse=True)[:discard_count]
    print(
        f"discarded_in_range={min(inbound_tail):.2f}..{max(inbound_tail):.2f}"
    )
    print(
        f"discarded_out_range={min(outbound_tail):.2f}..{max(outbound_tail):.2f}"
    )
PY

chmod 700 "$LAB/calculate_p95.py"
python3 -m py_compile "$LAB/calculate_p95.py"

Preserve the baseline and discarded tail

Run the baseline with a 50 Mbps commit and display the removed ranges. Saving the receipt makes the arithmetic reviewable without rerunning a future, possibly changed export.

python3 "$LAB/calculate_p95.py" "$LAB/traffic.csv" \
  --expected-samples 300 \
  --interval-seconds 300 \
  --commit-mbps 50 \
  --overage-per-mbps 7.50 \
  --show-discarded | tee "$LAB/receipt.txt"

Below is a representative receipt from the reproduced fixture, not a provider quote:

samples=300 interval_seconds=300 discarded_each_direction=15
p95_in_mbps=23.70 p95_out_mbps=41.60
billable_separate_then_max_mbps=41.60
alternative_per_interval_max_mbps=154.00
average_total_mbps=70.50 observed_transfer_gb=793.181
p95_if_sustained_30d_tb=13.478
commit_mbps=50.00 overage_mbps=0.00 overage_cost=0.00
discarded_in_range=180.00..194.00
discarded_out_range=140.00..154.00
Sorted inbound and outbound bandwidth samples with the highest five percent discardedThe exact 300-row synthetic fixture is sorted independently by direction. Ranks 286 through 300 are shaded as discarded. Rank 285 remains the highest survivor at 23.70 megabits per second inbound and 41.60 megabits per second outbound, both below a 50 megabit commit.2001501005001285300Sorted sample rank50 Mbps commitIN survivor 23.70OUT survivor 41.60Ranks 286–300 · 15 discardedInboundOutbound
Sorted values from the synthetic 300-row fixture. Each direction discards ranks 286–300; rank 285 remains at 23.70 Mbps inbound and 41.60 Mbps outbound, below the 50 Mbps commit.

One Dataset, Two Direction Rules

ConnetU’s worked directional billing example calculates inbound and outbound percentiles separately and then chooses the larger result. The lab labels that method separate_then_max. Some contracts may instead choose the larger direction inside each interval before ranking, represented by per_interval_max.

Rule applied to the same 300 rows Result What caused it
Separate inbound and outbound tails, then choose larger survivor 41.60 Mbps Each direction discards its own 15 burst intervals
Choose each interval’s larger direction, then discard one combined tail 154.00 Mbps Thirty non-overlapping burst intervals compete for only 15 discarded positions

Neither label should be guessed from a graph. Ask the provider to state the direction function and reproduce it on a short known dataset. A 112.40 Mbps difference here comes from policy, not measurement noise.

Port capacity remains separate from both results. Place the uplink beside CPU, memory, storage, redundancy, and management terms in dedicated server hardware inventory. A 1 Gbps port can carry a 154 Mbps interval; it does not say whether the bill includes 50 Mbps, 100 Mbps, 1 Gbps, or a transfer quota.

Turn the Survivor Into a Commit Decision

Run the same accepted ledger with a lower declared commit. Only the commercial inputs change; the traffic sample and percentile stay fixed.

python3 "$LAB/calculate_p95.py" "$LAB/traffic.csv" \
  --expected-samples 300 \
  --interval-seconds 300 \
  --commit-mbps 35 \
  --overage-per-mbps 7.50 | tail -n 1

The result is commit_mbps=35.00 overage_mbps=6.60 overage_cost=49.50. That amount is only illustrative because currency, tiers, rounding, minimum charges, pooled commits, and taxes are contract-specific. The useful decision is whether ordinary busy periods repeatedly survive above the commit, not whether the largest spike looks dramatic.

Average total traffic was 70.50 Mbps across both directions in this fixture, while the separate-direction billed result was 41.60 Mbps. They are not contradictory: the average sums two directions, while this billing rule chooses one directional survivor. The reported 13.478 TB is merely the decimal transfer that 41.60 Mbps would move if sustained continuously for 30 days. Actual observed transfer was 793.181 GB across the 25-hour fixture. A percentile alone cannot reconstruct a unique monthly TB total.

Carry recurring commit fees, overage scenarios, monitoring, management, and recovery ownership into 12-month hosting budget rather than comparing the port label alone. A lower commit is attractive only if the expected overage and operational response remain acceptable.

Data quality belongs in that decision. Missing rows can reduce the number of high samples, change the discarded count, or hide a billing outage. The audit calculator refuses an incomplete export instead of silently sorting what remains.

python3 - "$LAB/traffic.csv" "$LAB/missing-row.csv" <<'PY'
import pathlib
import sys

source = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").splitlines()
pathlib.Path(sys.argv[2]).write_text(
    "\n".join(source[:121] + source[122:]) + "\n",
    encoding="utf-8",
)
PY

if python3 "$LAB/calculate_p95.py" "$LAB/missing-row.csv" \
  --expected-samples 300 --commit-mbps 50; then
  printf 'Unexpectedly accepted missing row\n' >&2
  exit 1
else
  printf 'missing-row control rejected as intended\n'
fi

Duplicated timestamps are also rejected. A repeated row can overweight one interval and conceal a missing neighbor even when the total count still equals 300.

python3 - "$LAB/traffic.csv" "$LAB/duplicate-time.csv" <<'PY'
import csv
import sys

with open(sys.argv[1], newline="", encoding="utf-8") as source:
    rows = list(csv.DictReader(source))
rows[80]["timestamp_utc"] = rows[79]["timestamp_utc"]
with open(sys.argv[2], "w", newline="", encoding="utf-8") as target:
    writer = csv.DictWriter(
        target,
        fieldnames=["timestamp_utc", "in_mbps", "out_mbps"],
    )
    writer.writeheader()
    writer.writerows(rows)
PY

if python3 "$LAB/calculate_p95.py" "$LAB/duplicate-time.csv" \
  --expected-samples 300 --commit-mbps 50; then
  printf 'Unexpectedly accepted duplicate timestamp\n' >&2
  exit 1
else
  printf 'duplicate-time control rejected as intended\n'
fi

The audit is complete when the export has the declared row count, every adjacent timestamp is exactly one sampling interval apart, all rates are finite and non-negative, the discarded count and highest survivors can be reproduced, the direction rule matches the contract, and the commit calculation matches the retained receipt. In the worked fixture, the two negative controls must also fail with expected 300 samples, found 299 and duplicate timestamp.

Questions Worth Sending Before You Sign

Send the provider a compact request with the sample calculation attached:

  1. Which device and interface produce the bill, and are samples averages or peaks over each interval?
  2. What timezone, billing-window boundaries, and expected sample count apply to partial and full months?
  3. Are inbound and outbound ranked separately, combined per interval, summed, or billed on one direction?
  4. How does the system round the 5% discarded count, fractional Mbps, and currency overage?
  5. How are counter resets, missing rows, duplicate rows, maintenance, pooled ports, or interface changes handled?
  6. Does scrubbing remove attack traffic before the meter, and can the customer see the same post-scrubbing ledger?
  7. Is the commit a minimum charge, a reserved capacity promise, or both? What happens above it besides billing?
  8. Can the provider export the raw interval data and current running percentile before the invoice closes?

Attack treatment deserves written scope. Coordinate billing evidence with DDoS protection operations because the meter may sit before or after scrubbing. Do not assume malicious traffic is excluded merely because mitigation kept the application online.

Kentik’s network cost monitoring guide sets a useful operational goal: reproduce the provider’s bill continuously, alert before the running percentile crosses the commit, and reconcile the final invoice. A small team can start with the retained CSV and script; a larger network may need aggregation, contract inventory, flow attribution, and access controls around billing telemetry.

FAQ: Buyer Questions That Change the Answer

Is 95th-percentile bandwidth the same as average bandwidth?

No. Average bandwidth sums the sampled rates and divides by the sample count. A provider-style 95th-percentile method ranks samples, removes a defined high tail, and keeps the highest survivor. The two measures answer different capacity questions and can differ sharply on bursty traffic.

How many five-minute samples does a provider discard?

Multiply the accepted sample count by 5%, then apply the contract’s rounding rule. A 30-day window with 8,640 complete five-minute samples has exactly 432 rows in 5%. Months, partial cycles, missing data, and other intervals can change the count, so record the provider’s rule rather than assuming 432 every time.

Are inbound and outbound traffic added together?

Contracts differ. One may calculate both directions separately and bill the larger survivor; another may select the larger direction per interval, sum directions, or bill only outbound traffic. Ask for the exact function because non-overlapping directional bursts can produce materially different results.

Can a 95th-percentile Mbps value be converted to monthly TB?

No unique conversion exists. Mbps is a rate statistic and TB is transferred volume. Multiplying the percentile rate by every second in a month gives a sustained-rate equivalent, not the actual bytes moved, because the underlying intervals vary and the highest 5% were removed from the billing statistic.

Does a 1 Gbps port include a 1 Gbps commit?

Not necessarily. Port speed is the physical or configured ceiling, and that label alone does not prove the included commit. A contract may include a lower or full-rate commit, allow bursts, or use a transfer quota instead. Confirm port capacity, commit, transfer quota, overage, throttling, and fair-use terms as separate fields.

What should happen when samples are missing or duplicated?

The buyer-side audit should stop and identify the gap. The provider contract should explain whether missing intervals are estimated, ignored, carried forward, marked unavailable, or resolved through another rule. Duplicates need removal only under an agreed identity check; silently sorting either defect can change the bill.

Keep the Assumptions Beside the Number

A defensible bandwidth receipt contains the contract version, meter/interface, UTC window, interval, expected and accepted row counts, direction rule, discarded-count rule, inbound and outbound survivors, chosen billable rate, commit, overage terms, exceptions, script hash, and raw-export hash. Without those fields, 41.60 Mbps is a number without ownership.

Retain the calculator and receipt when they belong to an authorized invoice audit. Remove only the disposable fixture after checking its marker and exact path:

test "$(<"$MARKER")" = 'voxfor-bandwidth-p95-lab-v1'
case "$LAB" in
  /tmp/voxfor-bandwidth-p95-lab) ;;
  *) printf 'Refusing unexpected cleanup path\n' >&2; exit 1 ;;
esac
rm -rf -- "$LAB"
test ! -e "$LAB"
printf 'cleanup=lab_path_absent\n'

The cleanup command is appropriate only for the exact marker-bound lab path. It is not a production rollback. If a billing conclusion was already used for a purchase or commit change, preserve the original contract, export, script, receipt, invoice, and approval record; reverse the commercial change through the provider’s documented process rather than deleting evidence or rewriting historical samples.

Leave a Reply

Your email address will not be published. Required fields are marked *