Enrichment modules: AWS and Azure service coverage¶
SPRUCE generates its estimates by chaining EnrichmentModules.
An EnrichmentModule is the unit of extension in SPRUCE. Each module reads columns from
the CUR input row and/or from values set by earlier modules, then writes its results into a
shared map. The pipeline materialises one output row per CUR row at the end, avoiding
per-module row copies.
The enrichment modules are listed and configured in a configuration file, one per cloud provider and report format. If no configuration is specified, SPRUCE uses the bundled default for the active provider (e.g. default-config-aws.json for a native AWS CUR, or default-config-aws-focus.json / default-config-azure-focus.json for an AWS or Azure FOCUS report).
See Configure the modules for instructions on how to modify the enrichment modules, and Write a module to add your own.
The list of columns generated by the modules can be found in the SpruceColumn class.
Native and FOCUS report formats
A FOCUS export carries mostly the same values as the provider's native export under different
column names (e.g. for Azure x_SkuMeterCategory instead of MeterCategory, for AWS x_Operation
instead of line_item_operation and SkuMeter instead of line_item_usage_type). The provider
modules therefore work for both formats: when the report format is FOCUS, they automatically
rebind their input columns to the FOCUS names. Two AWS specificities: x_ServiceCode carries the
CUR product code (line_item_product_code), and the columns with no FOCUS equivalent are derived
from SkuMeter — the instance type from meters like BoxUsage:m5.large and the data transfer
category from the meter suffixes (DataTransfer-Regional-Bytes, AWS-Out-Bytes, ...).
The pipeline at a glance¶
The modules run in sequence and fall into five stages. Later stages consume the columns written by earlier ones, which is why the order in the configuration file matters.
flowchart LR
A[Billing row] --> B("<b>Metadata</b><br>RegionExtraction")
B --> C("<b>Energy & embodied</b><br>Storage, Networking,<br>Serverless, Accelerators,<br>Boavizta, EcoLogits")
C --> D("<b>Factors</b><br>PWUE,<br>AverageCarbonIntensity")
D --> E("<b>Impacts</b><br>OperationalEmissions,<br>Water")
E --> F("<b>Normalisation</b><br>FOCUSColumns<br>(native formats only)")
F --> G[Enriched row]
| Module | Providers | Writes | Based on |
|---|---|---|---|
| RegionExtraction | AWS, Azure, FOCUS | region |
— |
| Storage | AWS, Azure | operational_energy_kwh, embodied_emissions_co2eq_g |
Cloud Carbon Footprint for energy, Boavizta and vendor LCAs for embodied |
| Networking | AWS, Azure | operational_energy_kwh |
Boavizta coefficients |
| Serverless | AWS | operational_energy_kwh |
Tailpipe |
| Accelerators | AWS | operational_energy_kwh |
Cloud Carbon Footprint |
| Compute — Boavizta | AWS, Azure | operational_energy_kwh, embodied_emissions_co2eq_g, embodied_adp_sbeq_g |
BoaviztAPI |
| LLM inference — EcoLogits | AWS, Azure | operational_energy_kwh, embodied_emissions_co2eq_g |
EcoLogits |
| PWUE | AWS, Azure | power_usage_effectiveness, water_usage_effectiveness |
Provider-published data |
| AverageCarbonIntensity | AWS, Azure | carbon_intensity |
Ember |
| OperationalEmissions | AWS, Azure | operational_emissions_co2eq_g |
— |
| Water | AWS, Azure | water_cooling_l, water_electricity_production_l, water_consumption_stress_area_l |
WRI |
| FOCUSColumns | AWS, Azure (native formats) | FOCUS columns | — |
Stage 1 — Metadata extraction¶
RegionExtraction¶
Extracts the region information from the provider-specific input columns and stores it in
a standard location, where all the following modules can find it. The FOCUS variant is
provider-neutral: it reads the standard RegionId column.
| Classes | com.digitalpebble.spruce.modules.aws.RegionExtractioncom.digitalpebble.spruce.modules.azure.RegionExtractioncom.digitalpebble.spruce.modules.focus.RegionExtraction |
| Writes | region |
Stage 2 — Energy and embodied estimates¶
The modules in this stage estimate the energy used by a row of usage and, for Storage, Boavizta and EcoLogits, the related embodied emissions. Each module handles a different kind of usage (storage, networking, compute, …), so they complement each other rather than overlap.
Storage¶
Estimates the energy used for storage by applying a flat coefficient per GB, following the approach of the Cloud Carbon Footprint project, and the embodied emissions of the drives holding the data, amortised over their service life. Service-specific replication factors are applied to both, since the same bytes occupy that many times more physical drives. On Azure, managed disks are estimated from their provisioned capacity.
| Classes | com.digitalpebble.spruce.modules.ccf.aws.Storagecom.digitalpebble.spruce.modules.ccf.azure.Storage |
| Writes | operational_energy_kwhembodied_emissions_co2eq_g |
Energy configuration (in Wh per TB-hour):
| Key | Default | Description |
|---|---|---|
hdd_coefficient_tb_h |
0.65 | Energy per TB-hour for HDD storage |
ssd_coefficient_tb_h |
1.2 | Energy per TB-hour for SSD storage |
Embodied emissions configuration:
| Key | Default | Description |
|---|---|---|
hdd_embodied_kg_per_drive |
30.0 | Embodied emissions of one hard drive, in kg CO2eq |
hdd_capacity_gb |
15000.0 | Capacity assumed for one hard drive |
ssd_embodied_kg_per_gb |
0.055 | Embodied emissions of an SSD, per GB of capacity |
storage_lifetime_hours |
43800.0 | Service life the embodied emissions are amortised over (5 years) |
The two media are modelled on different bases. A hard drive costs roughly the same to manufacture whatever its capacity, since the platters, motor, actuator, casing and PCB are near-fixed for a 3.5" unit and areal density does the work, so its embodied emissions are a constant per drive divided by an assumed capacity. An SSD's die area scales with capacity, so its figure is a rate per GB. With the defaults that works out at 0.40 kg CO2eq per TB-year for HDD and 11 kg for SSD.
Note that hdd_capacity_gb describes the physical drive rather than a provisioned volume, so it
is not the same thing as an Azure Managed Disk size or an EBS volume size.
Data sources for the embodied figures:
| Figure | Value | Source |
|---|---|---|
| HDD, per drive | 31.11 kg CO2eq | BoaviztAPI HDD component, from Umweltbundesamt, Green Cloud Computing 2021 |
| HDD, Seagate Exos X22 LCA | 28.7 kg CO2eq for a 22 TB drive | Tailpipe manufacture methodology |
| HDD, per TB-year | 0.27 kg CO2eq | Seagate, hard drives and data centre sustainability |
| HDD, 24 vendor LCAs | 0.02 kg CO2eq/GB over a 512 GB to 6 TB sample | Tannu & Nair, The Dirty Secret of SSDs: Embodied Carbon |
| SSD, die-area formula | 0.052 kg CO2eq/GB | BoaviztAPI SSD component |
| SSD, 3D NAND study | 0.056 kg CO2eq/GB | Tailpipe manufacture methodology, from Embodied Carbon Footprint of 3D NAND Memories |
| Drive capacity mix | 15 TB installed-fleet average | Backblaze Drive Stats 2025 |
The first four rows converge on roughly 30 kg per drive across a 40x range of capacities, which is itself the evidence for treating HDD embodied carbon as capacity independent. The Tannu & Nair rate cannot be applied per GB to current hardware: it encodes the drive sizes of a pre-2023 corpus and overstates per-byte embodied carbon by about an order of magnitude. See issue #102 for the full derivation.
Networking¶
Estimates the energy used for networking in and out of data centres. The module distinguishes between three transfer types with separate coefficients, taken from the Boavizta Cloud Emissions Working Group.
In a CUR, the transfer type is read from the transfer_type product attribute of the
AWSDataTransfer lines. A FOCUS report carries neither, so the category is derived from the
SkuMeter suffixes instead (DataTransfer-Regional-Bytes and DataTransfer-xAZ-*-Bytes →
intra, AWS-*-Bytes and CloudFront-*-Bytes → inter, DataTransfer-In/Out-Bytes → external),
which map one-to-one to the CUR transfer types.
| Classes | com.digitalpebble.spruce.modules.aws.Networkingcom.digitalpebble.spruce.modules.azure.Networking |
| Writes | operational_energy_kwh |
Configuration — the network_coefficients_kwh_gb map (in kWh/GB):
| Transfer type | Key | Default | Description |
|---|---|---|---|
| Intra-region | intra |
0.001 | Traffic within the same region |
| Inter-region | inter |
0.0015 | Traffic between regions |
| External | extra |
0.059 | Traffic to/from the internet (Inbound / Outbound) |
Attributing networking emissions
The relevance and usefulness of attributing emissions for networking based on usage is subject for debate, as the energy use of networking is pretty constant independently of traffic. The consequences of reducing networking are probably negligible but since the approach in SPRUCE is attributional, we do the same for networking in order to be consistent.
Serverless¶
Estimates the energy for the memory and vCPU usage of serverless services like Fargate, EMR Serverless or DocumentDB Elastic, which bill compute in vCPU-hours and GB-hours rather than by instance type. The default coefficients are taken from the Tailpipe methodology.
DocumentDB Elastic clusters bill their compute as ElasticCPUUsage in vCPU-hours; the storage and
backup lines of the same clusters are covered by Storage instead. AWS does not document
the underlying hardware, so the x86 coefficient is applied.
| Class | com.digitalpebble.spruce.modules.aws.Serverless |
| Writes | operational_energy_kwh |
Configuration:
| Key | Default | Description |
|---|---|---|
memory_coefficient_kwh |
0.0000598 | kWh per GB of memory |
arm_cpu_coefficient_kwh |
0.00191015625 | kWh per vCPU (ARM) |
x86_cpu_coefficient_kwh |
0.0088121875 | kWh per vCPU (x86) |
Accelerators¶
Estimates the energy used by accelerators (GPUs), following the approach of the Cloud Carbon Footprint project: the power draw is interpolated between the minimum and maximum wattage of the accelerator at an assumed utilisation rate.
Instance types which expose only a fraction of a physical GPU (e.g. g6f.large, which gets an
eighth of an NVIDIA L4) are supported: their share of the GPU is a decimal number and the power
draw is scaled accordingly.
| Class | com.digitalpebble.spruce.modules.ccf.aws.Accelerators |
| Writes | operational_energy_kwh |
Configuration:
| Key | Default | Description |
|---|---|---|
gpu_utilisation_percent |
50 | Assumed GPU utilisation rate; decimals are allowed (e.g. 12.5) |
Compute — Boavizta¶
Estimates the final energy used for computation (e.g. EC2, OpenSearch, RDS, Amazon MQ, ElastiCache on AWS; virtual machines on Azure), as well as the related embodied emissions and abiotic resource depletion, using the BoaviztAPI.
In a CUR, the instance type is read from product_instance_type; a FOCUS report does not carry
it, so it is parsed from the SkuMeter column instead (e.g. EUW2-BoxUsage:t3.xlarge).
Several managed services report their instance shape behind a service-specific prefix, which is
removed before the lookup: db. for RDS, mq. for Amazon MQ, cache. for ElastiCache, and the
.search suffix for OpenSearch. The two formats do not always agree on this, so both are
normalised: product_instance_type already reads m5.large for an Amazon MQ broker, while the
matching SkuMeter still reads mq.m5.large.
Amazon MQ is also the one service here that does not bill per instance. A clustered deployment is
charged one cluster-hour whatever its size, so a three-node RabbitMQ cluster running for an hour
appears as a single unit of USE1-RabbitMQ-3-InstanceUsage:mq.m5.large. The node count is read
back from the usage type and applied to both the energy and the embodied impacts; usage types
carrying no such marker are treated as a single instance, and Multi-AZ as two.
Each provider has two variants:
- BoaviztAPI queries a running instance of the API (
docker run -p 5000:5000 ghcr.io/boavizta/boaviztapi:latest). The address can be set with theaddressconfiguration key (defaulthttp://localhost:5000). - BoaviztAPIstatic reads the same information from a static file generated from the API and bundled with SPRUCE. No API instance is needed, which makes it simpler to use — this is the variant enabled in the default configurations.
| Classes | com.digitalpebble.spruce.modules.boavizta.aws.BoaviztAPIcom.digitalpebble.spruce.modules.boavizta.aws.BoaviztAPIstaticcom.digitalpebble.spruce.modules.boavizta.azure.BoaviztAPIcom.digitalpebble.spruce.modules.boavizta.azure.BoaviztAPIstatic |
| Writes | operational_energy_kwh, embodied_emissions_co2eq_g, embodied_adp_sbeq_g |
What is Abiotic Depletion Potential (ADP)?
From the BoaviztAPI documentation:
Abiotic Depletion Potential (ADP) is an environmental impact indicator. This category corresponds to mineral and resources used and is, in this sense, mainly influenced by the rate of resources extracted. The effect of this consumption on their depletion is estimated according to their availability stock at a global scale. This impact category is divided into two components: a material component and a fossil fuels component (we use a version of ADP which includes both). This impact is expressed in grams of antimony equivalent (gSbeq).
Source: sciencedirect
LLM inference — EcoLogits¶
Estimates the energy consumption and embodied emissions of LLM inference on AWS Bedrock
and Azure AI Foundry (initially the Azure OpenAI models), based on static per-model
coefficients derived from the EcoLogits project. Like BoaviztAPIstatic, a static data file
bundled in the JAR is loaded at initialisation time; the modules then match billing rows to
per-model coefficients.
BedrockEcoLogits parses the line_item_usage_type field (format:
{REGION}-{ModelKey}-{input|output}-tokens[-batch]) to extract both the model key and the
token type, then normalises the token count from pricing_unit (handling real-world values
such as 1K tokens or 1M tokens). Only output-token rows are scored — the EcoLogits
methodology attributes ~all generation cost to the autoregressive output phase, so
input-token rows are skipped.
AzureFoundryTokenEcoLogits does the same for token meters billed under the Azure OpenAI (or
newer Foundry Models) category: the model label and token direction are extracted from
MeterName (e.g. GPT 5 outpt Glbl 1M Tokens) and the token count is read from Quantity
(ConsumedQuantity in FOCUS reports). Both hold the number of tokens consumed: the 1K/1M
unit in UnitOfMeasure only describes the pricing block and does not scale the quantity —
Microsoft's FOCUS conversion defines ConsumedQuantity = Quantity and
ContractedCost = UnitPrice × Quantity / x_PricingBlockSize. Provisioned throughput (PTU),
hourly hosting and fine-tuning meters are not token-based and are not covered. The other
Foundry model families (Mistral, Cohere, Llama, ...) bill through the same kind of token
meters and only need mapping.csv entries verified against real exports — the module
initially ships with Azure OpenAI mappings.
| Classes | com.digitalpebble.spruce.modules.ecologits.BedrockEcoLogitscom.digitalpebble.spruce.modules.ecologits.AzureFoundryTokenEcoLogits |
| Writes | operational_energy_kwh, embodied_emissions_co2eq_g |
Batch size assumption
EcoLogits hardcodes a batch size of B=64 concurrent requests. The resulting
coefficients are a mid-batch estimate — they underestimate energy for low-traffic
scenarios and overestimate it for high-throughput batch inference (e.g. Bedrock Batch
mode). Making B dynamic requires provider telemetry not available in billing data.
Stage 3 — Efficiency and intensity factors¶
The modules in this stage do not estimate energy themselves; they attach the per-region factors that the impact modules in the next stage multiply the energy estimates by.
PWUE¶
Loads both Power Usage Effectiveness (PUE) and Water Usage Effectiveness (WUE)
factors from a CSV resource file bundled per provider: aws-pue-wue.csv carries the 2022–2025 figures
published by AWS, and
azure-pue-wue.csv is sourced from Microsoft's data centre sustainability pages.
These factors are published per year and move noticeably from one year to the next, so they
are keyed by region and year rather than applied as a blanket value. The year is that of
the line item's usage date, read from whichever of ChargePeriodStart,
line_item_usage_start_date, Date or BILLING_PERIOD the report carries.
The lookup logic follows this priority:
- Exact region match (e.g.
us-east-1) - Regex pattern match (e.g.
eu-.+), i.e. the geography-level average - The provider-wide
GLOBALaverage, where the CSV has one - Default configured value (fallback to 1.15 for PUE, null for WUE)
Within a tier the entry for the usage year is used; when that year is not covered — a region AWS started reporting on recently, a WUE only published from 2024 onwards, or a row with no usable date — the closest year available for that region is used instead.
Azure figures are not dated
Microsoft does not break its PUE and WUE down by year, so the rows in
azure-pue-wue.csv leave the year empty and apply to every year.
| Class | com.digitalpebble.spruce.modules.PWUE |
| Reads | region, the usage date of the line item |
| Writes | power_usage_effectiveness, water_usage_effectiveness |
Configuration:
| Key | Default | Description |
|---|---|---|
default |
1.15 | PUE used when a region matches no entry at any tier, global average included |
AverageCarbonIntensity¶
Adds average carbon intensity factors derived from Ember's
electricity data, distributed under the Creative Commons Attribution Licence (CC-BY-4.0).
Values are keyed directly by cloud provider and region (e.g. aws:us-east-1). For regions
in countries with sub-national data (currently the US and India), the carbon intensity is
taken from the Ember value for the state hosting the data centre; otherwise the
country-level Ember value is used.
The data is loaded from ember/ember_co2_intensity.csv, which is generated from
cloud_regions.json — see the scripts under
scripts/ and the dedicated
README for how to
refresh it.
| Class | com.digitalpebble.spruce.modules.ember.AverageCarbonIntensity |
| Reads | region |
| Writes | carbon_intensity |
Stage 4 — Impacts¶
The modules in this stage combine the energy estimates from stage 2 with the factors from stage 3 to produce the final impact columns.
OperationalEmissions¶
Computes operational emissions from the energy usage, carbon intensity and PUE estimated by the preceding modules:
operational_emissions_co2eq_g =
operational_energy_kwh × carbon_intensity × power_usage_effectiveness
× powerSupplyEfficiency × powerTransmissionLosses
It accounts for two additional overheads:
- Power Supply Efficiency: the power lost between the data centre mains electricity and the server (default
1.04). - Power Transmission Losses: the power lost between the power station and the data centre mains electricity (default
1.08).
| Class | com.digitalpebble.spruce.modules.OperationalEmissions |
| Reads | operational_energy_kwh, carbon_intensity, power_usage_effectiveness |
| Writes | operational_emissions_co2eq_g |
Configuration:
| Key | Default | Description |
|---|---|---|
powerSupplyEfficiency |
1.04 | Losses between mains and server |
powerTransmissionLosses |
1.08 | Losses between power station and data centre |
Water¶
Estimates the water consumption associated with cloud usage, producing three columns:
-
water_cooling_l— the volume of water (in litres) used for data centre cooling. Computed asoperational_energy_kwh×power_usage_effectiveness× WUE, where WUE is the ratio of litres of water consumed for cooling per kWh of IT energy, loaded per region by the PWUE module. -
water_electricity_production_l— the volume of water (in litres) consumed during electricity generation to power the data centre. Computed asoperational_energy_kwh×power_usage_effectiveness× WCF, where WCF (Water Consumption Factor) represents the litres of water consumed per kWh of electricity generated. The WCF values per electricity grid zone are sourced from the WRI methodology for calculating water use embedded in purchased electricity. -
water_consumption_stress_area_l— the total water consumption (water_cooling_l+water_electricity_production_l) attributed to regions under high or extremely high water stress. This field is only populated when the electricity grid zone for the region has a baseline water stress category of 3 (High) or 4 (Extremely High) in the WRI Aqueduct 4.0 dataset; it is absent otherwise. The Aqueduct data is licensed through Creative Commons and has been extracted and mapped to cloud provider region codes.
| Class | com.digitalpebble.spruce.modules.Water |
| Reads | operational_energy_kwh, power_usage_effectiveness, water_usage_effectiveness, region |
| Writes | water_cooling_l, water_electricity_production_l, water_consumption_stress_area_l |
Stage 5 — Output normalisation¶
FOCUSColumns¶
Bridges provider-native billing columns to provider-neutral FOCUS
(FinOps Open Cost & Usage Specification) columns, so the reporting scripts and dashboard can
consume enriched data with the same column names whatever the input format. Only the columns
the reporting scripts and dashboard rely on are covered — this is not a full native-to-FOCUS
conversion, and values are copied as-is (e.g. ChargeCategory carries the raw
line_item_line_item_type / ChargeType vocabulary). Runs last in the pipeline, after
region has been set by RegionExtraction.
These modules are only needed for the native formats: a FOCUS report already carries these columns on input, so the FOCUS pipelines do not include them.
For AWS CURs:
| FOCUS column | AWS source |
|---|---|
BilledCost |
line_item_unblended_cost |
ListCost |
pricing_public_on_demand_cost |
RegionId |
region (normalised by RegionExtraction) |
ServiceName |
product_servicecode |
ChargeCategory |
line_item_line_item_type |
SubAccountId |
line_item_usage_account_id |
ChargePeriodStart |
line_item_usage_start_date |
ChargePeriodEnd |
line_item_usage_end_date |
Tags |
resource_tags, copied as a map of tag key/values as in AWS FOCUS exports |
SkuMeter |
line_item_usage_type (e.g. EUW2-BoxUsage:t3.xlarge, from which reporting derives instance types) |
x_ServiceCode |
line_item_product_code |
x_Operation |
line_item_operation |
For Azure cost details exports (columns that already carry a FOCUS-compatible name in the
input, such as BillingCurrency and Tags, pass through unchanged — except that Tags,
which EA exports truncate to a JSON fragment without the enclosing braces, is wrapped back
into a valid JSON object when the report is loaded):
| FOCUS column | Azure source |
|---|---|
BilledCost |
CostInBillingCurrency |
RegionId |
region (normalised by RegionExtraction) |
ServiceName |
MeterCategory |
ChargeCategory |
ChargeType |
SubAccountId |
SubscriptionId |
ChargePeriodStart |
Date |
ChargePeriodEnd |
Date + 1 day |
| Classes | com.digitalpebble.spruce.modules.aws.FOCUSColumnscom.digitalpebble.spruce.modules.azure.FOCUSColumns |
| Reads | region and the provider-native columns above |
| Writes | BilledCost, ListCost, RegionId, ServiceName, ChargeCategory, SubAccountId, ChargePeriodStart, ChargePeriodEnd, Tags, SkuMeter, x_ServiceCode, x_Operation |
Supporting data¶
Cloud region metadata¶
SPRUCE ships with cloud_regions.json, a single JSON file listing the AWS, GCP, and Azure
cloud regions together with their location (country, metro area, latitude/longitude),
service status, and number of availability zones. It is the canonical source for the
region-to-location mapping used by the modules and resource files (e.g.
AverageCarbonIntensity).
The file is produced in two steps by scripts under
scripts/, see
scripts/README.md
for the full usage details.