Pricing sources
Where every number comes from, how long it is cached, and what happens when a lookup fails.
Sources by dimension
| Dimension | Source | Live? |
|---|---|---|
| EC2 on-demand | AWS Price List API (pricing:GetProducts) | Yes |
| EC2 spot | EC2 Spot Price History (ec2:DescribeSpotPriceHistory) | Yes |
| EBS per-GB-month | AWS Price List API | Yes |
| EBS IOPS / throughput | static constants | No |
| Data transfer | static table, us-east-1 rates for all regions | No |
| Load balancer | static table | No |
The Price List API client is pinned to us-east-1 because that is the global endpoint for the service, regardless of which region you are pricing.
The two IAM permissions above are the only ones the server needs. Both return public list prices — no customer-specific or billing data.
Cache TTLs
| Entry | TTL | Why |
|---|---|---|
| On-demand | 24 hours | list prices change rarely |
| Spot | 15 minutes | spot moves constantly |
| EBS | 24 hours | as on-demand |
| Transfer | never expires | static table, seeded at boot |
| Load balancer | never expires | static table, seeded at boot |
The cache is in-memory and miss-driven — there is no catalogue warm-up. The first query for an instance type fetches it; subsequent queries within the TTL do not. A stale entry counts as a miss.
Spot cache keys include the availability zone, since spot prices differ per AZ.
Retry behaviour
Transient AWS errors are retried a small number of times with backoff.
Permanent errors are not retried — there is an allowlist of transient error codes, and anything outside it fails immediately. Retrying an InvalidParameterValue three times just delays the error.
Failure modes
No AWS credentials at boot. The server still starts. It serves whatever prices are already cached and reports the rest as unavailable, and every cost response carries pricingAvailable: false so the console can label it. This is what keeps local development and CI working without credentials.
Cache miss with no fetcher. Not found. The node group lands in coverage.unpriced.
Unsupported region. 16 regions are mapped from region code to Price List "location" string. A region outside that map returns an unsupported-region error rather than a fallback price.
Spot history empty. Not found. No fallback to on-demand — silently substituting the on-demand price would overstate cost on a spot fleet, which is worse than admitting the gap.
Invalid product data. Products with price <= 0, vcpu <= 0 or memGiB <= 0 are rejected rather than cached.
Spot and instance shape
Spot price history returns a price but not the instance's vCPU/memory shape. The shape is resolved from cached on-demand data, fetching it if necessary, so unit costs are correct. If the shape cannot be resolved the price is still returned but unit costs are zero — the whole-node price is right, the per-vCPU split is not available.
Discounts
Console figures are at list price; there is no org-level EDP setting yet. The pricing API accepts an EDP percentage per request, validated as 0 < d ≤ 100. Invalid values are ignored, not clamped, so a malformed discount never silently alters a price. Prices are cached undiscounted and a discount is applied on read, so it is never applied twice.
Reserved Instances and Savings Plans are not modelled.
EBS pricing detail
Only the per-GB-month storage price is fetched live. IOPS and throughput rates are static because the Price List API models them as separate product families with naming that varies by volume type and region, and getting that wrong would misprice provisioned-IOPS volumes.
| Type | Baseline IOPS | Baseline throughput | $/IOPS-mo | $/MBps-mo |
|---|---|---|---|---|
gp3 | 3000 | 125 MB/s | 0.005 | 0.04 |
gp2 | bundled | bundled | — | — |
io1 | — | — | 0.065 | — |
io2 | — | — | 0.065 | — |
st1 | 500 | 500 | — | — |
sc1 | 250 | 250 | — | — |
storage = sizeGiB × pricePerGBMonth
gp3: iops = max(0, iops − 3000) × 0.005
tput = max(0, mbps − 125) × 0.04
io1/io2: iops = iops × 0.065
total = storage + iops + tput, then EDP discountTransfer table
See AWS network costs for the rates. Responses report the source as a static table so the console can label them approximate.
There is no VPC peering rate in the table.
Freshness
Every price response carries an asOf timestamp from when it was fetched. For spot in particular, treat the figure as a point-in-time sample rather than a current quote.