Photo by Eric Rothermel on Unsplash
To find EC2 Reserved Instances expiring soon, call DescribeReservedInstances in every enabled region with the filter state=active, then keep the reservations whose End date falls within your window, such as the next 30 days. Sort by end date and you have a renewal list. Reserved Instances don’t renew on their own.
When a Reserved Instance (RI) ends, the instances it covered keep running and are billed at On-Demand rates from that moment. Nobody gets paged; the bill just rises next month. This example is for engineers and FinOps owners who want to see EC2 reserved instances expiring before that happens. You get a read-only TypeScript script for the AWS SDK for JavaScript v3 that checks every enabled region in parallel and prints the reservations ending within 30 days, or any window you pass.
It’s one of our AWS SDK v3 cost and inventory examples. If an RI already lapsed and costs went up, the walkthrough to ask AI why your AWS bill went up shows how to confirm it from Cost Explorer.
What does this script do?
- List enabled regions
DescribeRegionsreturns the regions enabled for your account. Reservations can exist in any of them, and the EC2 API only answers for one region per call. - Query each region in parallelIt calls
DescribeReservedInstanceswithFilters: [{ Name: "state", Values: ["active"] }]in every region at once, usingPromise.allSettledso a region you can’t read is reported instead of stopping the run. The API isn’t paginated: one call returns every reservation in the region. - Keep the ones ending soonReservations whose
Endis on or before today plus the window (30 days by default,--daysto change it) are kept, with the days left rounded up. - Print a renewal listEach row shows region, reservation ID, instance type, count, scope (
Regionor the Availability Zone), offering class and payment option, platform and end date, soonest first.--jsonprints the same rows as JSON for a ticket or a chat alert.
Prerequisites
- Node.js 18 or later, npm and
tsx. - The
@aws-sdk/client-ec2package. - A profile in the account that bought the reservations. The script only reads.
Which IAM permissions does it need?
Two read-only actions, neither of which supports resource-level permissions. AWS’s ReadOnlyAccess managed policy already includes both.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadReservedInstances",
"Effect": "Allow",
"Action": [
"ec2:DescribeRegions",
"ec2:DescribeReservedInstances"
],
"Resource": "*"
}
]
}
If you extend the script, for example to look up matching offerings, paste it into the TypeScript to IAM policy generator for a first draft of the new policy.
The full script: list EC2 reserved instances expiring in 30 days
// expiring-reserved-instances.ts
// Lists active EC2 Reserved Instances in every enabled region that expire within
// the next N days (default 30), soonest first. Read-only.
import {
EC2Client,
DescribeRegionsCommand,
DescribeReservedInstancesCommand,
} from "@aws-sdk/client-ec2";
const args = process.argv.slice(2);
const daysArg = args.indexOf("--days");
const WINDOW_DAYS = daysArg !== -1 ? Number(args[daysArg + 1]) : 30;
if (!Number.isFinite(WINDOW_DAYS) || WINDOW_DAYS < 1) throw new Error("--days must be a positive number");
const AS_JSON = args.includes("--json");
const HOME_REGION = process.env.AWS_REGION ?? "us-east-1";
const DAY_MS = 24 * 60 * 60 * 1000;
type Row = {
region: string;
id: string;
type: string;
count: number;
scope: string;
offering: string;
platform: string;
ends: string;
daysLeft: number;
};
async function enabledRegions(): Promise<string[]> {
const ec2 = new EC2Client({ region: HOME_REGION });
const { Regions = [] } = await ec2.send(new DescribeRegionsCommand({}));
return Regions.map((r) => r.RegionName).filter((r): r is string => !!r);
}
async function expiringIn(region: string, now: Date, limit: Date): Promise<Row[]> {
const ec2 = new EC2Client({ region });
// DescribeReservedInstances isn't paginated: one call returns every RI in the region.
const { ReservedInstances = [] } = await ec2.send(
new DescribeReservedInstancesCommand({ Filters: [{ Name: "state", Values: ["active"] }] }),
);
return ReservedInstances.filter((ri) => ri.End && ri.End <= limit).map((ri) => ({
region,
id: ri.ReservedInstancesId ?? "-",
type: ri.InstanceType ?? "-",
count: ri.InstanceCount ?? 0,
scope: ri.Scope === "Availability Zone" ? (ri.AvailabilityZone ?? "zonal") : "Region",
offering: `${ri.OfferingClass ?? "?"} / ${ri.OfferingType ?? "?"}`,
platform: ri.ProductDescription ?? "-",
ends: ri.End!.toISOString().slice(0, 10),
daysLeft: Math.ceil((ri.End!.getTime() - now.getTime()) / DAY_MS),
}));
}
async function main(): Promise<void> {
const now = new Date();
const limit = new Date(now.getTime() + WINDOW_DAYS * DAY_MS);
const regions = await enabledRegions();
const results = await Promise.allSettled(regions.map((r) => expiringIn(r, now, limit)));
const rows: Row[] = [];
results.forEach((result, i) => {
if (result.status === "fulfilled") rows.push(...result.value);
else console.error(`${regions[i]}: ${(result.reason as Error).name ?? result.reason}`);
});
rows.sort((a, b) => a.daysLeft - b.daysLeft);
if (AS_JSON) {
console.log(JSON.stringify(rows, null, 2));
return;
}
if (rows.length === 0) {
console.log(`No active Reserved Instances expire in the next ${WINDOW_DAYS} days (${regions.length} regions checked).`);
return;
}
console.table(rows);
const instances = rows.reduce((sum, r) => sum + r.count, 0);
console.log(`${rows.length} reservation(s) covering ${instances} instance(s) expire within ${WINDOW_DAYS} days.`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
The region fan-out uses the same Promise.allSettled pattern as the multi-region EC2 instance report by type and region, which is worth running next to see which instances an expiring RI currently covers.
How do you run it?
npm install @aws-sdk/client-ec2
npm install --save-dev tsx typescript
# Reservations ending in the next 30 days, all enabled regions
AWS_PROFILE=readonly npx tsx expiring-reserved-instances.ts
# A 90-day window, as JSON
AWS_PROFILE=readonly npx tsx expiring-reserved-instances.ts --days 90 --json
Run it on a schedule, for example weekly from CI, and post the JSON to your team channel. A 60- or 90-day window leaves time to decide between renewing, changing instance family or moving to another commitment.
Sample output
┌─────────┬─────────────┬────────────────────────────────────────┬───────────────┬───────┬──────────────┬─────────────────────────────────┬──────────────┬──────────────┬──────────┐
│ (index) │ region │ id │ type │ count │ scope │ offering │ platform │ ends │ daysLeft │
├─────────┼─────────────┼────────────────────────────────────────┼───────────────┼───────┼──────────────┼─────────────────────────────────┼──────────────┼──────────────┼──────────┤
│ 0 │ 'us-east-1' │ '4b2f1c9e-8a7d-4e21-b3c5-0d9f6a1e2b7c' │ 'm6i.large' │ 4 │ 'Region' │ 'standard / No Upfront' │ 'Linux/UNIX' │ '2026-10-04' │ 7 │
│ 1 │ 'eu-west-1' │ 'a91c3e57-2f4b-4d8a-9e1c-6b7d5f3a2c10' │ 'r6g.xlarge' │ 2 │ 'Region' │ 'convertible / Partial Upfront' │ 'Linux/UNIX' │ '2026-10-15' │ 18 │
│ 2 │ 'us-west-2' │ '0e7d6c5b-4a39-4821-9f0e-1d2c3b4a5f6e' │ 'c6i.2xlarge' │ 1 │ 'us-west-2a' │ 'standard / All Upfront' │ 'Windows' │ '2026-10-24' │ 27 │
└─────────┴─────────────┴────────────────────────────────────────┴───────────────┴───────┴──────────────┴─────────────────────────────────┴──────────────┴──────────────┴──────────┘
3 reservation(s) covering 7 instance(s) expire within 30 days.
Reservation IDs, types and dates are illustrative. A zonal reservation shows its Availability Zone in the scope column; a regional one shows Region. On-Demand Capacity Reservations also hold capacity in a zone, but with no term discount and billing whether or not an instance uses them; the script to find unused EC2 Capacity Reservations reports the empty ones.
What should you do before a Reserved Instance expires?
Treat each row as a decision, not an automatic renewal. Check whether the instances it covers still run at the same size: the example to detect underutilized EC2 instances by CPU flags ones that could shrink first. Then compare what the covered usage costs today; the example to find your most expensive AWS service with Cost Explorer puts EC2 in context with the rest of the bill.
For regional RIs, EC2 lets you queue a purchase to start at a future date, so a replacement can begin as the old one ends. Commitment decisions are a team practice as much as a script; the FinOps Foundation’s guidance on the rate optimization capability covers how organizations plan reservations and other commitments.
Troubleshooting: why is an expiring RI missing?
- The reservation was bought in another account.
DescribeReservedInstanceslists the reservations of the account you’re signed in to. In AWS Organizations, run the script in each account that buys RIs. - A region shows an error line. An SCP may deny that region, or your profile lacks access there. The other regions still report; follow the steps to debug AWS IAM access denied errors if it should work.
- It’s an RDS, ElastiCache or OpenSearch reservation. Those are separate services with their own describe calls. This script covers EC2 only. Before renewing an ElastiCache reservation, confirm the cluster is still used with the script to find idle ElastiCache clusters.
- You use Savings Plans. Savings Plans aren’t Reserved Instances and don’t appear here; they’re read through the Savings Plans API, and their coverage through Cost Explorer, as in the script to check Savings Plans coverage and utilization.
- The RI already ended. Expired reservations move to the
retiredstate and are excluded by theactivefilter.
Ask ChatWithCloud instead
You can ask ChatWithCloud “Which EC2 Reserved Instances expire in the next 30 days?” and it writes AWS SDK for JavaScript v2 code, runs it locally with your profile and summarizes the answer. One thing to know: a session uses one profile and one region, so ask about each region you buy RIs in, or use the script for the all-region view. How the ChatWithCloud CLI answers AWS questions covers the loop. For cost questions, a read-only profile with ce: permissions added is a good fit; see how to connect ChatWithCloud to your AWS profiles.
Frequently asked questions
Do EC2 Reserved Instances renew automatically?
No. When the term ends the reservation retires, and matching instances are billed at On-Demand rates. For regional RIs you can queue a new purchase ahead of time so coverage continues.
How do I see when my Reserved Instances expire?
Each reservation returned by DescribeReservedInstances has an End timestamp. The script above reads it in every enabled region and sorts by days left.
What happens to my instances when a Reserved Instance expires?
Nothing changes for the instances themselves. They keep running; only the billing changes, because the discount no longer applies.
Does this script show Savings Plans or RDS reservations?
No. It reads EC2 Reserved Instances only. Savings Plans and reservations for other services use their own APIs.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud
