Downloading your AWS cost history as a CSV
The cost chart shows a sparkline and the top 5 services by spend. That's useful for spotting anomalies at a glance. But the finance team doesn't want a sparkline — they want a spreadsheet.
CostWatch now has a CSV export endpoint.
The endpoint
GET /account/{account_id}/export?days=90
Authorization: session cookie
Returns a CSV file with Content-Disposition: attachment:
date,total,Amazon EC2,Amazon RDS,Amazon S3,AWS Lambda,...
2026-05-10,47.23,31.20,9.80,4.12,1.11,...
2026-05-11,52.18,35.40,9.80,4.15,1.83,...
...
Every service that appeared in the window gets its own column, sorted alphabetically. A service that was zero on a particular day gets 0.0000. The total column is the sum of all services for that day.
Why not top-5
The cost chart caps at top 5 services to keep the chart readable. The export has no cap. If you have 47 services you actually spent money on last month (yes, this happens), all 47 get columns.
The intent is different: the chart answers "what's unusual?", the CSV answers "give me everything so I can pivot it".
The implementation
Cost Explorer's DAILY granularity returns one result per day, with groups per service:
resp = ce.get_cost_and_usage(
TimePeriod={"Start": start, "End": end},
Granularity="DAILY",
Metrics=["UnblendedCost"],
GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
)
Building the CSV from that is straightforward:
days_data: list[tuple[str, dict[str, float]]] = []
all_services: set[str] = set()
for day_data in resp.get("ResultsByTime", []):
day_str = day_data["TimePeriod"]["Start"]
day_services: dict[str, float] = {}
for group in day_data.get("Groups", []):
svc = group["Keys"][0]
amt = float(group["Metrics"]["UnblendedCost"]["Amount"])
day_services[svc] = round(amt, 4)
all_services.add(svc)
days_data.append((day_str, day_services))
services_sorted = sorted(all_services)
import io, csv
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["date", "total"] + services_sorted)
for day_str, svc_map in days_data:
total = round(sum(svc_map.values()), 4)
row = [day_str, total] + [round(svc_map.get(s, 0.0), 4) for s in services_sorted]
writer.writerow(row)
The response uses Cache-Control: no-store since it contains billing data. Same-account cross-account role assumption as the cost chart — the Lambda assumes a reader role in the user's account to call Cost Explorer.
Caveats
- Solo and Team plans only. Free users get the dashboard but not the raw data export.
- 90-day cap on the window. Cost Explorer's
GetCostAndUsageworks fine past 90 days, but billing data older than 90 days is less actionable for most uses and the query cost isn't trivial. Cache-Control: no-storeon the response. Don't proxy-cache billing data.
CostWatch is at osspulse.io — wait, wrong product. CostWatch doesn't have a live URL yet. Working on it.