AWS instance attributes and prices (EC2, RDS, ElastiCache and more), with their full
history, from the bundles Infrawrench publishes at instances-bundles.infrawrench.com.
composer require infrawrench/bulk-instancesRequires PHP 8.2+ with ext-curl and ext-json, plus either the xz PECL extension
or the xz command (see Caching).
use Infrawrench\BulkInstances\BulkInstances;
$data = BulkInstances::downloadAwsEc2PricingData(token: getenv('INFRAWRENCH_API_KEY') ?: null);
$data->keys(); // ['m5.large', 't2.micro', ...]
$m5 = $data->get('m5.large');
$m5->attributes()['vcpu']; // '2'
$m5->attributesIn('US East (N. Virginia)'); // with that region's overrides
foreach ($m5->pricing()->onDemand as $p) {
if ($p->regionCode === 'us-east-1' && $p->operatingSystem === 'Linux' && $p->tenancy === 'Shared'
&& $p->preInstalledSw === 'NA' && $p->capacitystatus === 'Used') {
echo $p->hourly; // 0.096
}
}
$m5->attributesHistory(); // every attribute change, oldest first
$m5->pricingHistory()->reserved; // every reserved price change, oldest firstAny Infrawrench bearer credential works as token (an iwk_ API key, an agent
credential or a WorkOS access token). It defaults to INFRAWRENCH_API_KEY. When
the data cannot be downloaded, AWSPricingDataException is thrown; its status
is the HTTP status, if there was a response.
BulkInstances::downloadAwsPricingData($platform, ...) loads any platform's bundle;
BulkInstances::downloadAwsEc2PricingData(...) is the same as downloadAwsPricingData('ec2', ...).
BulkInstances::AWS_PLATFORMS lists the platforms:
| Platform | AWS service |
|---|---|
ec2 |
EC2 |
rds |
RDS |
elasticache |
ElastiCache |
opensearch |
OpenSearch Service |
redshift |
Redshift |
docdb |
DocumentDB |
neptune |
Neptune |
memorydb |
MemoryDB |
dax |
DynamoDB Accelerator |
sagemaker |
SageMaker |
emr |
EMR |
gamelift |
GameLift |
spot |
EC2 Spot (see below) |
The fields that pick a price differ by platform. $data->variantKeys() names them, and every
price has a $variant array keyed by them:
use Infrawrench\BulkInstances\BulkInstances;
$rds = BulkInstances::downloadAwsPricingData('rds');
$rds->variantKeys(); // ['databaseEngine', 'databaseEdition', 'licenseModel', 'deploymentOption', ...]
foreach ($rds->get('db.m5.large')->pricing()->onDemand as $p) {
if ($p->regionCode === 'us-east-1' && $p->variant['databaseEngine'] === 'MySQL'
&& $p->variant['deploymentOption'] === 'Multi-AZ') {
echo $p->hourly;
}
}operatingSystem, tenancy, licenseModel, preInstalledSw and capacitystatus are EC2's
variant fields. On other platforms they are '', unless the platform has a field with that name
(RDS has licenseModel).
spot is EC2's spot prices. They are in pricing()->onDemand, one per region and operatingSystem (Linux or
Windows): the lowest price across the region's availability zones, as AWS's Spot pricing page shows it.
Its locations are region codes, so region and regionCode are both us-east-1. Spot instances
have no attributes; take their specs from ec2. The history goes back to August 2026, hourly at
first and every 6 hours since November 2026, and prices change far more often than on-demand prices.
BulkInstances::downloadRunpodPricingData(...) loads RunPod's GPU and CPU pod prices. It takes the same
named parameters as the AWS methods (minus $platform), caches the same way, and throws a
RunpodPricingDataException, a subclass of AWSPricingDataException. It returns a RunpodPricingData, whose
get() returns a RunpodInstance; they have the same members as AWSPricingData and AWSInstance.
$runpod = BulkInstances::downloadRunpodPricingData();
foreach ($runpod->get('NVIDIA H100 80GB HBM3')?->pricing()->onDemand ?? [] as $p) {
if ($p->variant['cloudType'] === 'Secure Cloud') {
echo $p->hourly;
}
}It has GPU types by RunPod's id (NVIDIA H100 80GB HBM3), priced per GPU per hour, and CPU pods by pod id
(cpu3c-2-4: flavor, vCPUs, GB of memory), priced per pod per hour; the productFamily attribute is GPU or
CPU. Prices are the same in every data center, so every price is in the location global. The only variant
key is cloudType, Secure Cloud or Community Cloud. Savings plans are in pricing()->reserved with the term
3mo or 6mo, Savings Plan, All Upfront, and hourly is the plan's effective hourly rate. Stock levels
and spot prices are not included. The history starts in November 2026.
BulkInstances::fetchRunpodInstance($id, ...) fetches one GPU type or CPU pod from the Infrawrench API instead
(see One instance). It returns a RunpodInstance, or null if RunPod has no instance with that
id, and throws a RunpodPricingDataException:
$h100 = BulkInstances::fetchRunpodInstance('NVIDIA H100 80GB HBM3');BulkInstances::downloadAlibabaPricingData(...) loads Alibaba Cloud ECS prices. It takes the same named
parameters as the AWS methods (minus $platform), caches the same way, and throws an
AlibabaPricingDataException, a subclass of AWSPricingDataException. It returns an AlibabaPricingData, whose
get() returns an AlibabaInstance; they have the same members as AWSPricingData and AWSInstance.
$alibaba = BulkInstances::downloadAlibabaPricingData();
foreach ($alibaba->get('ecs.g6.large')?->pricing()->onDemand ?? [] as $p) {
if ($p->regionCode === 'cn-hangzhou' && $p->variant['operatingSystem'] === 'Linux'
&& $p->variant['networkType'] === 'VPC') {
echo $p->hourly;
}
}Its locations are region ids (cn-hangzhou), so the location and its region code are the same, and regions()
gives each an English name. Its variant keys are operatingSystem (Linux or Windows) and networkType
(VPC, or Classic in older history). pricing()->onDemand has pay-as-you-go prices. pricing()->reserved has
subscriptions, with the term 1mo or 1yr to 5yr, Subscription, All Upfront: they are paid in full
when bought, so hourly is 0 and upfront is the price. It also has reserved instances, with the term 1yr or
3yr, Reserved Instance, and a purchase option. Attributes are Alibaba's instance type definitions
(CpuCoreCount, MemorySize, CpuOptions.ThreadsPerCore, ...), from May 2023 on. The history starts in
May 2018.
BulkInstances::fetchAlibabaInstance($instanceType, ...) fetches one instance type from the Infrawrench API
instead (see One instance). It returns an AlibabaInstance, or null if Alibaba has no instance
type with that name, and throws an AlibabaPricingDataException:
$g6 = BulkInstances::fetchAlibabaInstance('ecs.g6.large');BulkInstances::downloadAzurePricingData(...) loads Azure virtual machine prices. It takes the same named
parameters as the AWS methods (minus $platform), caches the same way, and throws an
AzurePricingDataException, a subclass of AWSPricingDataException. It returns an AzurePricingData, whose
get() returns an AzureInstance; they have the same members as AWSPricingData and AWSInstance.
$azure = BulkInstances::downloadAzurePricingData();
foreach ($azure->get('Standard_D2s_v3')?->pricing()->onDemand ?? [] as $p) {
if ($p->regionCode === 'eastus' && $p->variant['operatingSystem'] === 'Linux'
&& $p->variant['priority'] === 'Regular') {
echo $p->hourly;
}
}It has VM sizes by ARM name (Standard_D2s_v3), priced per VM per hour. Its locations are ARM region names
(eastus), so the location and its region code are the same, and regions() gives each Azure's display
name (US East). Its variant keys are operatingSystem (Linux or Windows) and priority (Regular,
Spot or Low Priority). pricing()->onDemand has pay-as-you-go prices, dated by when Azure says they took
effect. pricing()->reserved has reservations, with the term 1yr, 3yr or 5yr, Reserved Instance,
All Upfront: hourly is 0 and upfront is the whole term's price. Azure lists reservations under Linux
only; on Windows the license is billed on top. It also has savings plans, with the term 1yr or 3yr,
Savings Plan, No Upfront, and hourly is the plan's rate. Attributes are skuName, productName and,
from Azure's pricing calculator, cores, ram (GB), diskSize (temporary disk, GB), gpu and series.
About a third of sizes, mostly the newest and retired series, have no calculator specs. Dedicated hosts are
not included. The history starts in November 2026.
BulkInstances::downloadGcpPricingData(...) loads Google Cloud Compute Engine prices. It takes the same named
parameters as the AWS methods (minus $platform), caches the same way, and throws a
GCPPricingDataException, a subclass of AWSPricingDataException. It returns a GCPPricingData, whose
get() returns a GCPInstance; they have the same members as AWSPricingData and AWSInstance.
$gcp = BulkInstances::downloadGcpPricingData();
foreach ($gcp->get('n2-standard-2')?->pricing()->onDemand ?? [] as $p) {
if ($p->regionCode === 'us-central1' && $p->variant['provisioningModel'] === 'Standard') {
echo $p->hourly;
}
}It has machine types by name (n2-standard-2), priced per VM per hour. Google bills most machine types per
vCPU and per GiB of memory at their family's rates, plus their GPUs, so each price is worked out from those
rates and the machine type's shape. Prices leave out bundled Local SSD, OS licenses and sustained use
discounts, and TPU, X4, X5 and A4X machine types aren't included. Its locations are regions (us-central1),
so the location and its region code are the same. The only variant key is provisioningModel, Standard or
Spot. pricing()->reserved has committed use discounts, with the term 1yr or 3yr, Committed Use,
No Upfront, and hourly is the commitment's rate. Attributes are the machine type's fields from the Compute
Engine API (guestCpus, memoryMb, description, isSharedCpu, ...), acceleratorType and
acceleratorCount for its GPUs, and zones, the zones in each region that have it, which differ by region
(see attributesIn($region)). The history starts in November 2026.
These load the same way as Google Cloud's, with the same options, caching and failures. Each returns its own data and instance types, which have the same members as the AWS ones.
Method of BulkInstances |
Data | Instance | Error |
|---|---|---|---|
downloadHetznerPricingData |
HetznerPricingData |
HetznerInstance |
HetznerPricingDataException |
downloadHetznerAuctionPricingData |
HetznerAuctionPricingData |
HetznerAuctionInstance |
HetznerAuctionPricingDataException |
downloadOvhPricingData |
OVHPricingData |
OVHInstance |
OVHPricingDataException |
downloadOvhEcoPricingData |
OVHEcoPricingData |
OVHEcoInstance |
OVHEcoPricingDataException |
downloadOraclePricingData |
OraclePricingData |
OracleInstance |
OraclePricingDataException |
downloadIbmPricingData |
IBMPricingData |
IBMInstance |
IBMPricingDataException |
$hetzner = BulkInstances::downloadHetznerPricingData();
foreach ($hetzner->get('cx23')?->pricing()->onDemand ?? [] as $p) {
if ($p->regionCode === 'fsn1' && $p->variant['ipv4'] === 'Primary IPv4') {
echo $p->hourly;
}
}hetzner, Hetzner cloud servers, by Cloud API name (cx23), per hour. Locations are Cloud API location names (fsn1,ash). The only variant key isipv4,IPv6 onlyorPrimary IPv4, since a primary IPv4 is optional and billed apart.pricing()->reservedhas the monthly cap, with the term1mo,Monthly Cap,Postpaid,hourly0 andupfrontthe cap. Attributes includecores,cpu,memoryInGb,diskInGbandarchitecture.hetznerauction, Hetzner Server Auction listings, by listing id (3107671). Each is one physical server, so its history runs from when it is listed until it is sold, and it has the same variant key and term ashetzner. Attributes are the listing's hardware and details (CPU.Name,RAM.Size,datacenter,fixedPrice,setupFee, ...). The bundle is read hourly, so a price drop is dated by the read that saw it.ovh, OVHcloud Public Cloud flavors (b3-8), per hour, from the World and US catalogs, in USD. Locations are pricing zones, not regions:world,3az,lz-eu,lz-eurozone,lz-af,usandus-east-lz-dal-a. The only variant key isoperatingSystem.pricing()->reservedhas monthly plans where a flavor has them, with the term1mo,Monthly,PostpaidorAll Upfront,hourly0 andupfrontthe month's price.ovheco, OVHcloud's Kimsufi, So you Start, Rise and Advance servers (24sys01), by data center (gra,sgp); therangeattribute says which. These have no on-demand prices, because they are only rented by the month. The rent is inpricing()->reserved, with the term1mo,Rental,All Upfront,hourly0 andupfrontthe rent, beside prepaying (1yror2yr,Prepaid). The variant keys arememory,storageandsystemStorage, which is often empty.oracle, Oracle Cloud VM and bare metal shapes, per hour, the same in every commercial region, so every price is inglobal. The only variant key isoperatingSystem. Flexible shapes have no one price, so they are priced at a set of sizes named<shape>-<ocpus>-<GB>(VM.Standard.E5.Flex-2-16), which aren't names Oracle uses: theshapeattribute is, andocpuPriceandmemoryGbPricegive any other size.ibm, IBM Cloud Virtual Server for VPC profiles (bx3d-2x10), per hour, by region. The only variant key iscapacityType,On-Demand,SpotorBurstable 50%,25%or10%.pricing()->reservedhas reservations, with the term1yror3yr,Reserved,No Upfront, andhourlythe reservation's rate. About 90 older profiles (bx2,cx2,mx2,gx2, ...) have no public on-demand price of their own, so theirs is IBM's rate for an hourly reservation, which is an approximation.
All of their histories start in November 2026.
There is no single-instance fetch for Azure, Google Cloud, Hetzner, OVHcloud, Oracle Cloud or IBM Cloud yet; download their bundles instead.
BulkInstances::fetchAwsInstance($platform, $instanceType, ...) fetches a single instance from the
Infrawrench API instead of downloading the platform's whole bundle. It returns the same
AWSInstance that get() does, or null if the platform has no instance with that name:
$m5 = BulkInstances::fetchAwsInstance('ec2', 'm5.large');
count($m5?->pricing()->onDemand ?? []);It takes token like the bundle download, and baseUrl for where the API is served from
(https://app.infrawrench.com by default). Nothing is cached. The API syncs each new bundle within
about half an hour of it being published, so it can briefly trail downloadAwsPricingData().
BulkInstances::fetchRunpodInstance($id, ...) and BulkInstances::fetchAlibabaInstance($instanceType, ...) do
the same for RunPod and Alibaba Cloud, with the same named parameters (minus $platform) and the same half-hour
sync. They return a RunpodInstance or AlibabaInstance (or null) and throw the provider's exception. RunPod's
ids have spaces; pass them as they are, they are escaped for you.
The bundle is downloaded xz-compressed (about 20 MB) and kept decompressed (about
210 MB) in ~/.infrawrench/aws_pricing_data (cachePath), so a cached load skips
decompression. Each load asks the server which bundle is latest and downloads it
only if it is not cached, deleting the older one. If the server cannot be
reached, the newest cached bundle is used instead. Each platform's bundle is cached
under its own name (rds-bundle-…json), so loading one never evicts another. Pass
cachePricingData: false to always download and never touch the disk. The cache is shared with the other
Infrawrench SDKs.
Decompression uses the xz PECL extension when it
is loaded, and otherwise pipes the download through the xz command (xz-utils:
apt install xz-utils, brew install xz). If neither is available, downloading
throws AWSPricingDataException; a cached load needs neither.
Loading decompresses the whole bundle and keeps its JSON in memory. Instances are
decoded lazily: get() only parses that instance's rows, and attributes(),
attributesHistory(), pricing() and pricingHistory() each decode on first
access and keep the result. The returned objects are shared between calls and
are immutable (readonly classes; arrays are returned by value).
A load peaks at about 260 MB and a decoded instance takes a few MB, so raise PHP's
default memory_limit of 128M: 512M is enough to load the bundle and look at a
few dozen instance types. Decoding every instance takes about 5 GB.
regionis the AWS location name (US East (N. Virginia)),regionCodeits code (us-east-1) where the price list gives one,nullotherwise.pricing()holds only what is still offered. InpricingHistory(), anullhourlymeans the price stopped being offered atat.- Instances AWS no longer lists are still in
keys(), withdeletedAt()set and emptypricing(). - Dates are
DateTimeImmutablein UTC.