Cost Explorer's charts answer "which service". The CSV answers the better question, "which line item, and when did it change". You can read it with a script short enough to paste into a terminal, and no AWS credentials are involved: you only read a file you already downloaded.
Get the file
In Cost Explorer, set the date range to the last 30 to 90 days, granularity Daily, Group by: Usage type, then Download CSV. The file has one row per usage type, one column per day, and a final column with the total.
Here is a tiny made-up file (synthetic, not from a real account), saved as sample.csv:
"Usage type","2026-10-01","2026-10-02","2026-10-03","Total costs($)" "USE1-BoxUsage:m5.large",36.86,36.86,36.86,110.58 "USE1-NatGateway-Bytes",8.00,8.00,31.00,47.00 "USE1-NatGateway-Hours",2.16,2.16,2.16,6.48 "USE1-DataTransfer-Regional-Bytes",12.00,12.00,12.00,36.00 "USE1-PublicIPv4:InUseAddress",0.48,0.48,0.48,1.44 "Total costs",59.50,59.50,82.50,201.50
The script
Save as top.mjs. It needs Node 18 or newer and nothing from npm:
import { readFileSync } from 'node:fs';
// Split one CSV line; commas inside "quotes" stay in the field.
const cells = (line) => line.match(/("([^"]|"")*"|[^,]*)(,|$)/g)
.map((c) => c.replace(/,$/, '').replace(/^"|"$/g, '').replace(/""/g, '"'))
.slice(0, -1);
const [head, ...rows] = readFileSync(process.argv[2], 'utf8').replace(/^\uFEFF/, '').trim().split(/\r?\n/);
const days = cells(head).slice(1, -1); // the date columns
const items = rows.map(cells)
.filter((r) => !/^Total costs/.test(r[0]))
.map((r) => ({ type: r[0], first: +r[1], last: +r[r.length - 2], total: +r[r.length - 1] }))
.sort((a, b) => b.total - a.total);
console.log(`${days[0]} to ${days.at(-1)}`);
for (const i of items.slice(0, 10)) {
const jump = i.first > 0 && i.last / i.first >= 2 ? ` <- ${(i.last / i.first).toFixed(1)}x since day 1` : '';
console.log(`${i.type.padEnd(34)} $${i.total.toFixed(2).padStart(8)}${jump}`);
}
Run node top.mjs sample.csv and you get:
2026-10-01 to 2026-10-03 USE1-BoxUsage:m5.large $ 110.58 USE1-NatGateway-Bytes $ 47.00 <- 3.9x since day 1 USE1-DataTransfer-Regional-Bytes $ 36.00 USE1-NatGateway-Hours $ 6.48 USE1-PublicIPv4:InUseAddress $ 1.44
That output was produced by running the script on the file above. It handles a byte-order mark, Windows line endings and quoted names that contain commas.
What to look at in the output
- The biggest line is rarely the interesting one. Compute is large and expected. The lines worth a question are the ones you cannot explain from your architecture:
NatGateway-Bytes,DataTransfer-Regional-Bytes,PublicIPv4:*,DataProcessing-Bytes(CloudWatch Logs). - The arrow is the whole point. A line that ran at $8 a day and now runs at $31 a day did not drift. Something shipped, scaled or started looping on that date. Open the daily columns and find the first day it moved, then match it against your deploy history.
- Check the price against the arithmetic. In US East (N. Virginia) a NAT gateway processes data at $0.045 per GB, so $47.00 of
NatGateway-Bytesis about 1,044 GB. If that number surprises you, that is the thing to investigate. Prices differ by region; check yours on the AWS pricing page for the service.
Make it yours
- Change
slice(0, 10)to show more rows. - Change the
>= 2ratio to catch smaller jumps. - Lines that start at $0 are not flagged (the arrow needs a day-1 amount to divide by). To catch something that appeared mid-period, add a check for
first === 0 && last > 0. - Compare two files by building a map from usage type to total and printing the difference.
If you would rather not maintain a script, the same kind of file can be dropped into a browser-based reader that runs the line items against a table of known cost causes and shows the dollars for each, with the console steps for the fix. The file is read in your browser; only a compact summary of totals is sent.