
Photo by Dominik Gryzbon on Pexels
ECS Fargate Spot savings were about 68% per task in US East (N. Virginia) in September 2026: $0.01288871 per vCPU-hour and $0.00141527 per GB-hour on Fargate Spot, against $0.04048 and $0.004445 on Fargate. To find candidates, list services with DescribeServices, skip those whose capacityProviderStrategy already includes FARGATE_SPOT, and price each task definition’s CPU and memory at both rates.
Fargate Spot runs the same containers on spare capacity at a lower price, in exchange for a two-minute warning when AWS needs the capacity back. For queue workers, CI runners and stateless web tiers with several tasks, that trade is usually fine; for a single-task service it isn’t. This example is for platform and FinOps engineers who want to see ECS Fargate Spot savings per service before changing anything.
The script walks every cluster in the Regions you choose, finds services on Fargate, reads how they place tasks and what size each task is, and prints what they cost now and what they’d cost on Fargate Spot. It’s report only.
How big are ECS Fargate Spot savings?
As of September 2026, these are the Linux rates in US East (N. Virginia). Fargate rates come from the AWS Price List for Amazon ECS (published 11 September 2026); Fargate Spot rates are the spot prices shown on the AWS Fargate pricing page on 28 September 2026. Spot prices change over time, and the page describes the discount as up to 70%.
| Architecture | Fargate vCPU-hour | Fargate GB-hour | Spot vCPU-hour | Spot GB-hour |
|---|---|---|---|---|
| x86_64 | $0.04048 | $0.004445 | $0.01288871 | $0.00141527 |
| ARM64 | $0.03238 | $0.00356 | $0.01030969 | $0.00113349 |
Worked example: a web service runs 6 tasks of 1 vCPU and 2 GB around the clock. One task costs (1 × $0.04048 + 2 × $0.004445) × 730 hours = $36.04 a month on Fargate, and (1 × $0.01288871 + 2 × $0.00141527) × 730 = $11.48 on Fargate Spot. All six on Fargate cost $216.24; all six on Spot, $68.85, a saving of $147.39. A safer split with 3 tasks on each, which the strategy further down produces for 6 tasks, costs $108.12 + $34.43 = $142.55, still $73.69 a month less.
Which ECS services suit Fargate Spot?
The Amazon ECS Developer Guide says Fargate Spot tasks get a two-minute warning, sent as an EventBridge task state change event and as a SIGTERM to the task. The stopped task has the stop code SpotInterruption. Everything else follows from that:
- Good fits: queue consumers that can pick a message up again, batch and CI jobs that can rerun, and stateless services with several tasks behind a load balancer.
- Handle
SIGTERM. Containers must catch it and finish or hand off work; afterstopTimeout(30 seconds by default, up to 120) they getSIGKILL. - Poor fits: single-task services (the guide says a service with one task stays interrupted until capacity returns), long jobs that can’t checkpoint, and anything stateful on local storage.
- No fallback. When Spot capacity is short, ECS keeps retrying Spot; Fargate doesn’t replace it with regular capacity. A
FARGATEbase in the strategy keeps a minimum running. - Not eligible: Windows containers on Fargate don’t support Fargate Spot. The ECS API reference says
FARGATE_SPOTsupports Linux tasks on X86_64 from platform version 1.3.0 and ARM64 from 1.4.0.
What does the script do?
- Lists clusters
paginateListClusters, thenDescribeClustersin batches of 100 for each cluster’s default capacity provider strategy. - Lists services
paginateListServicesper cluster andDescribeServicesin batches of 10, the API’s limit. - Works out placementA
launchTypeofFARGATE, or acapacityProviderStrategy(or the cluster default) that usesFARGATEorFARGATE_SPOT. EC2 and external services are skipped. - Reads task size
DescribeTaskDefinitionfor task-levelcpu,memoryandruntimePlatform, cached per revision. - Prices it
desiredCount× task size × 730 hours at Fargate and Fargate Spot rates, plus a note on how well the service fits Spot.
Prerequisites
- Node.js 18 or later with
tsx, and@aws-sdk/client-ecs. - A read-only AWS profile. The script calls only list and describe operations.
- Prices for your Region if it isn’t US East (N. Virginia): edit
PRICES. The script to compare EC2 Spot price history covers the EC2 side of Spot.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadEcsServices",
"Effect": "Allow",
"Action": [
"ecs:ListClusters",
"ecs:DescribeClusters",
"ecs:ListServices",
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition"
],
"Resource": "*"
}
]
}
The IAM policy generator for TypeScript AWS SDK code produces the same list from the script. While you have task definitions open, the scripts to find privileged ECS task definitions and find secrets in ECS task definitions review the same data for security.
The script to find ECS services that could use Fargate Spot
// find-ecs-services-not-using-fargate-spot.ts
// Lists ECS services that run on Fargate, shows whether they already use the FARGATE_SPOT capacity provider,
// and estimates the monthly saving of moving their tasks to Fargate Spot from the task definition's CPU and
// memory. Report only: it never changes a service.
// Usage: npx tsx find-ecs-services-not-using-fargate-spot.ts [--regions us-east-1] [--csv spot.csv]
import { writeFileSync } from "node:fs";
import {
ECSClient,
DescribeClustersCommand,
DescribeServicesCommand,
DescribeTaskDefinitionCommand,
paginateListClusters,
paginateListServices,
type CapacityProviderStrategyItem,
type Service,
type TaskDefinition,
} from "@aws-sdk/client-ecs";
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 csvPath = flag("--csv");
// us-east-1 USD per hour. Fargate: AWS Price List (AmazonECS, published 11 September 2026).
// Fargate Spot: the spot price shown on the AWS Fargate pricing page on 28 September 2026 (it changes over time).
const PRICES = {
X86_64: { cpu: 0.04048, gb: 0.004445, spotCpu: 0.01288871, spotGb: 0.00141527 },
ARM64: { cpu: 0.03238, gb: 0.00356, spotCpu: 0.01030969, spotGb: 0.00113349 },
} as const;
const HOURS_PER_MONTH = 730;
interface Row {
Region: string;
Cluster: string;
Service: string;
Tasks: number;
TaskSize: string;
Arch: string;
Placement: string;
OnDemandPerMonth: number;
AllSpotPerMonth: number;
SavingPerMonth: number;
Note: string;
}
const errorText = (err: unknown): string => (err instanceof Error ? `${err.name}: ${err.message}` : String(err));
const chunk = <T,>(items: T[], size: number): T[][] =>
Array.from({ length: Math.ceil(items.length / size) }, (_, i) => items.slice(i * size, i * size + size));
const round = (n: number): number => Math.round(n * 100) / 100;
/** Describe how the service places tasks: a launch type or its capacity provider strategy. */
function placement(service: Service, clusterDefault: CapacityProviderStrategyItem[]): { label: string; fargate: boolean; spot: boolean } {
if (service.launchType) {
return { label: service.launchType, fargate: service.launchType === "FARGATE", spot: false };
}
const strategy = service.capacityProviderStrategy?.length ? service.capacityProviderStrategy : clusterDefault;
const label = strategy.map((s) => `${s.capacityProvider}:${s.base ?? 0}/${s.weight ?? 0}`).join(" ") || "unknown";
const names = strategy.map((s) => s.capacityProvider ?? "");
const spot = strategy.some((s) => s.capacityProvider === "FARGATE_SPOT" && ((s.weight ?? 0) > 0 || (s.base ?? 0) > 0));
return { label, fargate: names.some((n) => n === "FARGATE" || n === "FARGATE_SPOT"), spot };
}
function price(td: TaskDefinition, tasks: number): { onDemand: number; spot: number; size: string; arch: "X86_64" | "ARM64" } {
const vcpu = Number(td.cpu ?? 0) / 1024; // task-level CPU units
const gb = Number(td.memory ?? 0) / 1024; // task-level memory in MiB
const arch = td.runtimePlatform?.cpuArchitecture === "ARM64" ? "ARM64" : "X86_64";
const p = PRICES[arch];
const hours = tasks * HOURS_PER_MONTH;
return {
onDemand: (vcpu * p.cpu + gb * p.gb) * hours,
spot: (vcpu * p.spotCpu + gb * p.spotGb) * hours,
size: `${vcpu} vCPU / ${gb} GB`,
arch,
};
}
function note(service: Service, td: TaskDefinition): string {
const os = td.runtimePlatform?.operatingSystemFamily ?? "LINUX";
if (os.startsWith("WINDOWS")) return "not eligible: Windows tasks can't use Fargate Spot";
const tasks = service.desiredCount ?? 0;
if (tasks <= 1) return "single task: an interruption is an outage";
if ((service.loadBalancers ?? []).length > 0) return "behind a load balancer: keep a FARGATE base";
return "candidate: check it tolerates SIGTERM + 2 min";
}
async function scanRegion(region: string): Promise<Row[]> {
const ecs = new ECSClient({ region });
const clusterArns: string[] = [];
for await (const page of paginateListClusters({ client: ecs }, {})) clusterArns.push(...(page.clusterArns ?? []));
const rows: Row[] = [];
const taskDefs = new Map<string, TaskDefinition>();
for (const batch of chunk(clusterArns, 100)) {
const { clusters = [] } = await ecs.send(new DescribeClustersCommand({ clusters: batch }));
for (const cluster of clusters) {
const clusterArn = cluster.clusterArn ?? "";
const serviceArns: string[] = [];
for await (const page of paginateListServices({ client: ecs }, { cluster: clusterArn })) {
serviceArns.push(...(page.serviceArns ?? []));
}
for (const ten of chunk(serviceArns, 10)) {
const { services = [] } = await ecs.send(new DescribeServicesCommand({ cluster: clusterArn, services: ten }));
for (const service of services) {
const where = placement(service, cluster.defaultCapacityProviderStrategy ?? []);
if (!where.fargate || !service.taskDefinition) continue; // EC2 and EXTERNAL services aren't Fargate
let td = taskDefs.get(service.taskDefinition);
if (!td) {
const out = await ecs.send(new DescribeTaskDefinitionCommand({ taskDefinition: service.taskDefinition }));
td = out.taskDefinition ?? {};
taskDefs.set(service.taskDefinition, td);
}
const tasks = service.desiredCount ?? 0;
const cost = price(td, tasks);
const why = where.spot ? "already uses FARGATE_SPOT" : note(service, td);
rows.push({
Region: region,
Cluster: cluster.clusterName ?? "",
Service: service.serviceName ?? "",
Tasks: tasks,
TaskSize: cost.size,
Arch: cost.arch,
Placement: where.label,
OnDemandPerMonth: round(cost.onDemand),
AllSpotPerMonth: round(cost.spot),
SavingPerMonth: where.spot || why.startsWith("not eligible") ? 0 : round(cost.onDemand - cost.spot),
Note: why,
});
}
}
}
}
return rows;
}
function toCsv(rows: Row[]): string {
const cols = Object.keys(rows[0] ?? {}) as (keyof Row)[];
const cell = (v: string | number) => `"${String(v).replace(/"/g, '""')}"`;
return [cols.join(","), ...rows.map((r) => cols.map((c) => cell(r[c])).join(","))].join("\n") + "\n";
}
async function main(): Promise<void> {
const rows: Row[] = [];
for (const region of regions) {
try {
rows.push(...(await scanRegion(region)));
} catch (err) {
console.error(`${region}: ${errorText(err)}`);
}
}
if (rows.length === 0) {
console.log(`No Fargate services in ${regions.join(", ")}.`);
return;
}
rows.sort((a, b) => b.SavingPerMonth - a.SavingPerMonth);
console.table(rows);
const candidates = rows.filter((r) => r.Note.startsWith("candidate") || r.Note.startsWith("behind"));
const total = candidates.reduce((sum, r) => sum + r.SavingPerMonth, 0);
console.log(`${candidates.length} of ${rows.length} Fargate services are Spot candidates: up to $${total.toFixed(2)} a month ` +
"if every task moved to Fargate Spot (us-east-1 prices; a FARGATE base lowers this).");
if (csvPath) {
writeFileSync(csvPath, toCsv(rows));
console.log(`Wrote ${rows.length} rows to ${csvPath}`);
}
}
main().catch((err) => {
console.error(errorText(err));
process.exit(1);
});
How do you run it?
npm install @aws-sdk/client-ecs
npm install --save-dev tsx typescript @types/node
AWS_PROFILE=readonly npx tsx find-ecs-services-not-using-fargate-spot.ts --regions us-east-1 --csv fargate-spot.csv
Sample output
┌─────────┬─────────────┬─────────┬────────────────┬───────┬──────────────────────┬──────────┬────────────────────┬──────────────────┬─────────────────┬────────────────┬──────────────────────────────────────────────────────┐
│ (index) │ Region │ Cluster │ Service │ Tasks │ TaskSize │ Arch │ Placement │ OnDemandPerMonth │ AllSpotPerMonth │ SavingPerMonth │ Note │
├─────────┼─────────────┼─────────┼────────────────┼───────┼──────────────────────┼──────────┼────────────────────┼──────────────────┼─────────────────┼────────────────┼──────────────────────────────────────────────────────┤
│ 0 │ 'us-east-1' │ 'prod' │ 'web' │ 6 │ '1 vCPU / 2 GB' │ 'X86_64' │ 'FARGATE' │ 216.24 │ 68.85 │ 147.39 │ 'behind a load balancer: keep a FARGATE base' │
│ 1 │ 'us-east-1' │ 'jobs' │ 'thumbnailer' │ 3 │ '1 vCPU / 4 GB' │ 'ARM64' │ 'FARGATE:0/1' │ 102.1 │ 32.51 │ 69.59 │ 'candidate: check it tolerates SIGTERM + 2 min' │
│ 2 │ 'us-east-1' │ 'prod' │ 'api' │ 4 │ '0.5 vCPU / 1 GB' │ 'X86_64' │ 'FARGATE' │ 72.08 │ 22.95 │ 49.13 │ 'behind a load balancer: keep a FARGATE base' │
│ 3 │ 'us-east-1' │ 'prod' │ 'admin' │ 1 │ '0.25 vCPU / 0.5 GB' │ 'X86_64' │ 'FARGATE' │ 9.01 │ 2.87 │ 6.14 │ 'single task: an interruption is an outage' │
│ 4 │ 'us-east-1' │ 'prod' │ 'legacy-win' │ 2 │ '1 vCPU / 2 GB' │ 'X86_64' │ 'FARGATE' │ 72.08 │ 22.95 │ 0 │ "not eligible: Windows tasks can't use Fargate Spot" │
│ 5 │ 'us-east-1' │ 'jobs' │ 'queue-worker' │ 8 │ '2 vCPU / 4 GB' │ 'X86_64' │ 'FARGATE_SPOT:0/1' │ 576.64 │ 183.6 │ 0 │ 'already uses FARGATE_SPOT' │
└─────────┴─────────────┴─────────┴────────────────┴───────┴──────────────────────┴──────────┴────────────────────┴──────────────────┴─────────────────┴────────────────┴──────────────────────────────────────────────────────┘
3 of 6 Fargate services are Spot candidates: up to $266.11 a month if every task moved to Fargate Spot (us-east-1 prices; a FARGATE base lowers this).
The run used mocked ECS responses, so names are illustrative. web and api are the best candidates, as long as a FARGATE base keeps some tasks off Spot. thumbnailer runs on ARM64 and uses the lower ARM rates. admin has one task, so an interruption takes it offline. legacy-win is Windows and shows no saving, and queue-worker inherits the jobs cluster’s default strategy, which already uses Fargate Spot. The metrics-agent service runs on EC2 and doesn’t appear.
How do you move a service to Fargate Spot?
The ECS Developer Guide says a service can switch from a launch type to a capacity provider strategy, but you must force a new deployment when you do. The capacity providers must be associated with the cluster first:
# 1. Make FARGATE and FARGATE_SPOT available on the cluster (pass the full list and a default strategy)
aws ecs put-cluster-capacity-providers --cluster prod \
--capacity-providers FARGATE FARGATE_SPOT \
--default-capacity-provider-strategy capacityProvider=FARGATE,weight=1
# 2. Keep 2 tasks on FARGATE, then split the rest 1:3 between FARGATE and FARGATE_SPOT
aws ecs update-service --cluster prod --service web \
--capacity-provider-strategy capacityProvider=FARGATE,base=2,weight=1 capacityProvider=FARGATE_SPOT,weight=3 \
--force-new-deployment
Only one provider in a strategy can have a base, and through the API a provider without a weight gets 0 and places no tasks beyond its base. After the change, watch EventBridge for SpotInterruption stop codes and check that tasks drain cleanly. Keep the same change in Terraform or CDK so the next deploy doesn’t revert it.
Troubleshooting
- No Fargate services found. Check the Region list, and that the profile can call
ecs:ListServices; see how to troubleshoot AWS IAM access denied errors. - TaskSize shows 0 vCPU. The task definition sets CPU and memory only per container. Fargate requires task-level values, so check you’re reading the revision the service actually runs.
- The saving looks too high. It assumes
desiredCounttasks for 730 hours. Services that scale out and in cost less; compare with the ECS line in your Cost Explorer usage by service. - The update is rejected. The capacity provider isn’t associated with the cluster yet, or every provider in the strategy has weight 0.
Spot isn’t the only lever. The script to find Lambda functions not on arm64 shows the Graviton saving on the serverless side, and setting an ECR lifecycle policy trims the image storage behind these services.
Ask ChatWithCloud instead
For a quick look, ask ChatWithCloud “Which ECS services in this Region run on Fargate without Fargate Spot, and how many tasks does each run?” It writes AWS SDK for JavaScript v2 code, runs it locally with your profile and summarizes the answer; how ChatWithCloud answers AWS questions locally has the details. It uses one profile and Region per session, can be wrong, and doesn’t ask before making changes, so connect ChatWithCloud with a read-only AWS profile. More cost scripts live in the AWS practical examples hub.
Frequently asked questions
How much cheaper is Fargate Spot than Fargate?
In US East (N. Virginia) on 28 September 2026, about 68% for x86_64 Linux tasks. AWS’s pricing page says up to 70%, and Spot prices change over time.
How much warning do Fargate Spot tasks get?
Two minutes, as an EventBridge task state change event and a SIGTERM to the task. Set stopTimeout up to 120 seconds to use it for a clean shutdown.
Can I mix Fargate and Fargate Spot in one ECS service?
Yes. A capacity provider strategy can put a base number of tasks on FARGATE and split the rest by weight between FARGATE and FARGATE_SPOT.
Does Fargate Spot support Windows containers?
No. Windows containers on Fargate don’t support the Fargate Spot capacity provider.
What happens when there’s no Fargate Spot capacity?
ECS keeps trying to launch the Spot tasks until capacity is available; it doesn’t fall back to regular Fargate. Tasks placed by a FARGATE base keep running.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud