Photo by Harvey Power on Unsplash
T3 unlimited mode charges appear when a burstable instance’s average CPU stays above its baseline and it spends surplus credits it can’t pay back. EC2 bills them at a flat rate per vCPU-hour: $0.05 for Linux T2, T3 and T3a and $0.04 for T4g in us-east-1 (September 2026). Sum the CloudWatch metric CPUSurplusCreditsCharged, divide by 60 and multiply by the rate.
T3, T3a, T4g and T8i instances launch in unlimited mode by default, so a build agent or a reporting worker that runs hot all day never gets throttled. It gets billed instead, on a line most people don’t look at. This example is for engineers and FinOps reviewers who want to find which burstable instances generate T3 unlimited mode charges, how much each one costs, and whether a larger size, a non-burstable type or standard mode is the cheaper fix.
The script is a report by default. It switches instances to standard mode only for IDs you pass with --apply --instance-ids.
How do unlimited mode charges work?
Every burstable instance earns CPU credits at a rate that matches its baseline, for example 30% per vCPU on a t3.large. One credit is one vCPU at 100% for one minute. In unlimited mode, an instance that runs out of earned credits keeps bursting by spending surplus credits, and pays them back later when CPU drops below the baseline.
Surplus credits become a charge in three cases, per the EC2 User Guide page on unlimited mode concepts:
- The spent surplus credits exceed what the instance can earn in 24 hours. The excess is charged at the end of the hour.
- The instance is stopped or terminated.
- The instance is switched from
unlimitedtostandard.
CloudWatch tracks the balance still owed as CPUSurplusCreditBalance and what was billed as CPUSurplusCreditsCharged, both in credits (vCPU-minutes) at five-minute intervals. That makes the bill easy to reconstruct: charged credits ÷ 60 = vCPU-hours.
What do T3 unlimited mode charges cost?
As of September 2026, the AWS Price List for Amazon EC2 in US East (N. Virginia), published 25 September 2026, lists these surplus credit rates:
| Family | Linux per vCPU-hour | Windows per vCPU-hour |
|---|---|---|
| T2 | $0.05 | $0.096 |
| T3 | $0.05 | $0.096 |
| T3a | $0.05 | not listed separately |
| T4g | $0.04 | – |
| T8i | $0.06 | $0.115 |
Worked example: a t3.large (2 vCPUs, 30% baseline) that averages 90% CPU all month. It uses 1.8 vCPUs but earns only 0.6, so it’s charged for 1.2 vCPUs × 720 hours = 864 vCPU-hours, or 51,840 credits. That’s 864 × $0.05 = $43.20 on top of the instance price of 720 × $0.0832 = $59.90, for $103.10 a month. An m7i.large, also 2 vCPUs with no credit system, costs 720 × $0.1008 = $72.58. Moving saves $30.52 a month and removes the variable line.
The same page gives the breakeven for T3 against M5: 42.5% average CPU for a t3.large and 52.5% for t3.xlarge and t3.2xlarge. Below that, unlimited T3 is the cheaper choice even with the charges.
What does the script do?
- Lists burstable instances
paginateDescribeInstanceswith aninstance-typefilter fort2.*,t3.*,t3a.*,t4g.*andt8i.*, running or stopped. - Reads the credit option
DescribeInstanceCreditSpecificationswith up to 1,000 instance IDs per call returnsunlimitedorstandard. - Reads three metricsDaily
CPUSurplusCreditsChargedsums, the newestCPUSurplusCreditBalanceand averageCPUUtilization, 500 queries perGetMetricDatacall. - Prices the chargesCredits ÷ 60 × the family’s rate, using the Windows rate when
PlatformDetailssays Windows. - Suggests a directionNo charges: leave it. Busy all day: price a bigger or fixed-performance type. Short bursts: compare with one size up before switching to standard.
Prerequisites
- Node.js 18 or later with
tsx,@aws-sdk/client-ec2and@aws-sdk/client-cloudwatch. - A read-only profile for the report. The guide to connecting ChatWithCloud and SDK scripts to AWS profiles and SSO shows how to set one up.
- At least a week of history; 30 days catches month-end batch jobs.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadInstancesCreditsAndMetrics",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeInstanceCreditSpecifications",
"cloudwatch:GetMetricData"
],
"Resource": "*"
},
{
"Sid": "SwitchToStandardOnlyWithApply",
"Effect": "Allow",
"Action": "ec2:ModifyInstanceCreditSpecification",
"Resource": "arn:aws:ec2:*:123456789012:instance/*"
}
]
}
Leave out the second statement for report-only runs. You can draft or check a policy for your own version of the script with the free IAM policy generator for TypeScript.
The script to find T3 unlimited mode charges
// find-unlimited-mode-charges.ts
// Lists burstable EC2 instances (T2, T3, T3a, T4g, T8i) and their credit option, then prices the surplus
// CPU credits each unlimited instance was charged for over the last 30 days (CloudWatch CPUSurplusCreditsCharged).
// Report only by default. With --apply --instance-ids it switches the listed instances to standard mode.
// Usage: npx tsx find-unlimited-mode-charges.ts [--regions us-east-1,eu-west-1] [--days 30]
// npx tsx find-unlimited-mode-charges.ts --regions us-east-1 --apply --instance-ids i-0abc,i-0def
import {
EC2Client,
paginateDescribeInstances,
DescribeInstanceCreditSpecificationsCommand,
ModifyInstanceCreditSpecificationCommand,
type Instance,
} from "@aws-sdk/client-ec2";
import { CloudWatchClient, paginateGetMetricData, type MetricDataQuery } from "@aws-sdk/client-cloudwatch";
const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
};
const regions = (flag("--regions") ?? process.env.AWS_REGION ?? "us-east-1").split(",").map((r) => r.trim()).filter(Boolean);
const days = Math.min(Math.max(Number(flag("--days") ?? 30), 7), 90);
const apply = args.includes("--apply");
const applyIds = new Set((flag("--instance-ids") ?? "").split(",").map((v) => v.trim()).filter(Boolean));
// Surplus credit price per vCPU-hour, us-east-1, AWS Price List (published 25 September 2026).
// One CPU credit is one vCPU at 100% for one minute, so vCPU-hours = credits / 60.
const LINUX: Record<string, number> = { t2: 0.05, t3: 0.05, t3a: 0.05, t4g: 0.04, t8i: 0.06 };
const WINDOWS: Record<string, number> = { t2: 0.096, t3: 0.096, t8i: 0.115 };
const BURSTABLE = Object.keys(LINUX);
interface Row {
Instance: string;
Name: string;
Type: string;
Credits: string;
AvgCpu: string;
CreditsCharged: number;
Cost: string;
SurplusNow: number;
Advice: string;
cost: number;
}
const errorText = (err: unknown): string => (err instanceof Error ? `${err.name}: ${err.message}` : String(err));
const family = (type: string): string => type.split(".")[0];
async function creditModes(ec2: EC2Client, ids: string[]): Promise<Map<string, string>> {
const out = new Map<string, string>();
for (let i = 0; i < ids.length; i += 1000) {
const res = await ec2.send(new DescribeInstanceCreditSpecificationsCommand({ InstanceIds: ids.slice(i, i + 1000) }));
for (const spec of res.InstanceCreditSpecifications ?? []) {
if (spec.InstanceId) out.set(spec.InstanceId, spec.CpuCredits ?? "unknown");
}
}
return out;
}
/** Total credits charged, latest surplus balance and average CPU per instance. */
async function readMetrics(cw: CloudWatchClient, ids: string[]) {
const end = new Date();
const start = new Date(end.getTime() - days * 86_400_000);
const specs: [string, string][] = [
["CPUSurplusCreditsCharged", "Sum"],
["CPUSurplusCreditBalance", "Maximum"],
["CPUUtilization", "Average"],
];
const queries: MetricDataQuery[] = ids.flatMap((id, n) =>
specs.map(([name, stat], m) => ({
Id: `q${n}_${m}`,
MetricStat: {
Metric: { Namespace: "AWS/EC2", MetricName: name, Dimensions: [{ Name: "InstanceId", Value: id }] },
Period: 86_400,
Stat: stat,
},
})),
);
const out = new Map(ids.map((id) => [id, { charged: 0, surplusNow: 0, cpu: [] as number[] }]));
for (let i = 0; i < queries.length; i += 500) {
const pages = paginateGetMetricData(
{ client: cw },
{ MetricDataQueries: queries.slice(i, i + 500), StartTime: start, EndTime: end, ScanBy: "TimestampDescending" },
);
for await (const page of pages) {
for (const r of page.MetricDataResults ?? []) {
const match = /^q(\d+)_(\d)$/.exec(r.Id ?? "");
const m = match ? out.get(ids[Number(match[1])]) : undefined;
const values = r.Values ?? [];
if (!match || !m || values.length === 0) continue;
if (match[2] === "0") m.charged += values.reduce((a, b) => a + b, 0);
if (match[2] === "1") m.surplusNow = Math.max(m.surplusNow, values[0]); // newest day first
if (match[2] === "2") m.cpu.push(...values);
}
}
}
return out;
}
function advise(mode: string, charged: number, avgCpu: number | undefined): string {
if (mode !== "unlimited") return "standard: throttled to baseline when credits run out";
if (charged === 0) return "no surplus charges: unlimited costs nothing extra here";
if (avgCpu !== undefined && avgCpu >= 40) return "busy all day: price a larger size or a non-burstable type";
return "short bursts: compare the charge with one size up before switching to standard";
}
async function scanRegion(region: string): Promise<void> {
const ec2 = new EC2Client({ region });
const instances: Instance[] = [];
const filters = [
{ Name: "instance-state-name", Values: ["running", "stopped"] },
{ Name: "instance-type", Values: BURSTABLE.map((f) => `${f}.*`) },
];
for await (const page of paginateDescribeInstances({ client: ec2 }, { Filters: filters })) {
for (const r of page.Reservations ?? []) instances.push(...(r.Instances ?? []).filter((i) => i.InstanceId));
}
if (instances.length === 0) {
console.log(`${region}: no burstable instances`);
return;
}
const ids = instances.map((i) => i.InstanceId ?? "");
const modes = await creditModes(ec2, ids);
const metrics = await readMetrics(new CloudWatchClient({ region }), ids);
const rows: Row[] = instances.map((inst) => {
const id = inst.InstanceId ?? "";
const type = inst.InstanceType ?? "";
const windows = (inst.PlatformDetails ?? "").toLowerCase().includes("windows");
const rate = (windows ? WINDOWS : LINUX)[family(type)] ?? LINUX[family(type)] ?? 0.05;
const m = metrics.get(id) ?? { charged: 0, surplusNow: 0, cpu: [] };
const avgCpu = m.cpu.length ? m.cpu.reduce((a, b) => a + b, 0) / m.cpu.length : undefined;
const cost = (m.charged / 60) * rate;
const mode = modes.get(id) ?? "unknown";
return {
Instance: id,
Name: inst.Tags?.find((t) => t.Key === "Name")?.Value ?? "",
Type: type,
Credits: mode,
AvgCpu: avgCpu === undefined ? "-" : `${avgCpu.toFixed(1)}%`,
CreditsCharged: Math.round(m.charged),
Cost: `$${cost.toFixed(2)}`,
SurplusNow: Math.round(m.surplusNow),
Advice: advise(mode, m.charged, avgCpu),
cost,
};
});
rows.sort((a, b) => b.cost - a.cost);
console.log(`\n${region}: burstable instances, surplus credits charged over the last ${days} days`);
console.table(rows.map(({ cost: _cost, ...visible }) => visible));
const total = rows.reduce((sum, r) => sum + r.cost, 0);
const unlimited = rows.filter((r) => r.Credits === "unlimited").length;
console.log(`${unlimited} of ${rows.length} in unlimited mode. Surplus credit charges: $${total.toFixed(2)} in ${days} days.`);
if (!apply) return;
const chosen = rows.filter((r) => applyIds.has(r.Instance) && r.Credits === "unlimited");
if (chosen.length === 0) {
console.log("Nothing to apply: none of --instance-ids is an unlimited instance in this region.");
return;
}
for (const r of chosen) {
if (r.SurplusNow > 0) console.log(`${r.Instance}: ${r.SurplusNow} spent surplus credits will be charged when it switches`);
}
const res = await ec2.send(
new ModifyInstanceCreditSpecificationCommand({
InstanceCreditSpecifications: chosen.map((r) => ({ InstanceId: r.Instance, CpuCredits: "standard" })),
}),
);
for (const ok of res.SuccessfulInstanceCreditSpecifications ?? []) console.log(`${ok.InstanceId}: now standard`);
for (const bad of res.UnsuccessfulInstanceCreditSpecifications ?? []) {
console.error(`${bad.InstanceId}: ${bad.Error?.Code} ${bad.Error?.Message ?? ""}`);
}
}
async function main(): Promise<void> {
if (apply && applyIds.size === 0) {
console.error("--apply needs --instance-ids i-...,i-... (the script never changes instances you didn't name)");
process.exit(2);
}
for (const region of regions) {
try {
await scanRegion(region);
} catch (err) {
console.error(`${region}: ${errorText(err)}`);
}
}
}
main().catch((err) => {
console.error(errorText(err));
process.exit(1);
});
The 40% threshold in advise() is a rough cut near the documented t3.large breakeven. Baselines differ by size (a t3.micro earns far fewer credits than a t3.2xlarge), so treat the advice as a starting point and check the credit table for your sizes.
How do you run it?
npm install @aws-sdk/client-ec2 @aws-sdk/client-cloudwatch
npm install --save-dev tsx typescript @types/node
# Report only
AWS_PROFILE=readonly npx tsx find-unlimited-mode-charges.ts --regions us-east-1,eu-west-1 --days 30
# Switch one instance to standard after its owner agrees
AWS_PROFILE=ec2-admin npx tsx find-unlimited-mode-charges.ts --regions us-east-1 --apply --instance-ids i-0b2
Sample output
us-east-1: burstable instances, surplus credits charged over the last 30 days
┌─────────┬──────────┬───────────────────┬─────────────┬─────────────┬─────────┬────────────────┬──────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────┐
│ (index) │ Instance │ Name │ Type │ Credits │ AvgCpu │ CreditsCharged │ Cost │ SurplusNow │ Advice │
├─────────┼──────────┼───────────────────┼─────────────┼─────────────┼─────────┼────────────────┼──────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ 0 │ 'i-0a1' │ 'reports-worker' │ 't3.large' │ 'unlimited' │ '90.0%' │ 51840 │ '$43.20' │ 96 │ 'busy all day: price a larger size or a non-burstable type' │
│ 1 │ 'i-0c3' │ 'win-rdp-gateway' │ 't3.xlarge' │ 'unlimited' │ '52.0%' │ 3000 │ '$4.80' │ 12 │ 'busy all day: price a larger size or a non-burstable type' │
│ 2 │ 'i-0b2' │ 'jenkins-agent' │ 't3.medium' │ 'unlimited' │ '26.0%' │ 720 │ '$0.60' │ 50 │ 'short bursts: compare the charge with one size up before switching to standard' │
│ 3 │ 'i-0d4' │ 'bastion' │ 't4g.small' │ 'unlimited' │ '4.0%' │ 0 │ '$0.00' │ 0 │ 'no surplus charges: unlimited costs nothing extra here' │
│ 4 │ 'i-0e5' │ 'legacy-cron' │ 't2.micro' │ 'standard' │ '7.0%' │ 0 │ '$0.00' │ 0 │ 'standard: throttled to baseline when credits run out' │
└─────────┴──────────┴───────────────────┴─────────────┴─────────────┴─────────┴────────────────┴──────────┴────────────┴──────────────────────────────────────────────────────────────────────────────────┘
4 of 5 in unlimited mode. Surplus credit charges: $48.60 in 30 days.
This run used mocked EC2 and CloudWatch responses. reports-worker is the worked example: $43.20 in surplus credits at 90% CPU, a clear case for a fixed-performance type. win-rdp-gateway is small in credits but pays the Windows rate. jenkins-agent bursts for builds and was charged $0.60, which is cheaper than any resize. bastion is unlimited but never goes over its baseline, so unlimited mode costs nothing there.
Resize, switch to standard or leave it?
- Leave it when the monthly charge is a few dollars. Unlimited mode is doing its job: absorbing bursts without throttling.
- Resize when average CPU sits above the baseline every day. A bigger T size raises the baseline; an M or C type removes credits altogether. Check memory too, since T and M sizes with the same vCPUs don’t always match. The Compute Optimizer rightsizing recommendations example gives a second opinion from AWS’s own analysis.
- Switch to standard only for workloads that can tolerate being throttled to the baseline, such as low-priority batch jobs. The switch charges any
CPUSurplusCreditBalanceimmediately, which the script prints before it applies. - Fix the default. T3, T3a, T4g and T8i launch as unlimited, and T2 as standard, unless you change the account’s default credit specification per family in each Region.
Instances that barely use their CPU are a different problem; the script to detect and stop underutilized EC2 instances handles those, and finding previous generation EC2 instances catches old T2 fleets worth moving to T3 or T4g. For the EBS side of the same instances, the script to find gp3 volumes with unused provisioned IOPS and throughput finds performance you pay for but don’t use. Monitoring can be the same: the script to find EC2 instances with detailed monitoring enabled shows which instances pay for one-minute CloudWatch metrics.
Troubleshooting
- Charges on the bill but zero in the report. The instance was terminated; the surplus balance was charged at termination and the instance no longer appears in
DescribeInstances. In Cost Explorer, group by usage type and look for types such asCPUCredits:t3. unknowncredit option. The instance ID wasn’t in theDescribeInstanceCreditSpecificationsresponse, usually because it was terminated between the two calls. Run the script again.- Empty metrics. The instance was stopped for the whole window, or CloudWatch hasn’t published data yet for a new one.
- Access denied. Compare your role with the policy above and see how to troubleshoot IAM access denied errors.
Ask ChatWithCloud instead
For a quick answer, ask ChatWithCloud “Which T3 instances were charged for surplus CPU credits last month?” It writes AWS SDK for JavaScript v2 code, runs it locally with your profile and summarizes the result, as described in how ChatWithCloud answers AWS questions. It can be wrong, and it runs changes without asking for confirmation, so keep it on a read-only profile and change credit modes yourself. For the wider bill, getting AWS billing details by service for last month shows where EC2 sits, and the AWS practical examples library has more cost reports.
Frequently asked questions
Why am I charged for T3 unlimited mode?
Your instance’s average CPU over 24 hours was above its baseline, so it spent surplus credits that earned credits couldn’t pay back. EC2 bills those at a flat rate per vCPU-hour.
Is T3 unlimited mode on by default?
Yes. T3, T3a, T4g and T8i launch as unlimited unless you set standard at launch or change the account default. T2 launches as standard.
How do I turn off T3 unlimited?
Call ModifyInstanceCreditSpecification with CpuCredits: "standard", or use the console. Any surplus credit balance is charged at the moment you switch.
How do I calculate the unlimited charge from CloudWatch?
Sum CPUSurplusCreditsCharged for the period, divide by 60 to get vCPU-hours, and multiply by the surplus credit rate for the family and operating system.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud