Guide · ECS Capacity Providers · Fargate Spot
ECS Capacity Providers for MCP Server Monitoring: Fargate and Fargate Spot Blend
ECS capacity providers let you specify how compute is provisioned for your tasks — FARGATE (On-Demand), FARGATE_SPOT, or EC2 Auto Scaling groups — and mix them in a single service using a base + weight strategy. For MCP server monitoring fleets running probe workers, capacity providers are the control plane that determines which fraction of tasks run on discounted Spot capacity and which tasks stay on reliable On-Demand. This guide covers the base + weight formula, CDK setup for both cluster-level and service-level configuration, managed Auto Scaling for probe worker fleets, and the AliveMCP probe fleet production configuration. For Fargate Spot pricing and task-level SIGTERM handling see AWS Fargate Spot for MCP Server Probe Workers; for the SIGTERM checkpoint pattern see Handling Fargate Spot Interruptions for MCP Workers.
TL;DR
Enable FARGATE and FARGATE_SPOT capacity providers on the ECS cluster with cluster.enableFargateCapacityProviders() in CDK. On the ECS service, set capacityProviderStrategies to [{ capacityProvider: 'FARGATE', base: 1, weight: 1 }, { capacityProvider: 'FARGATE_SPOT', base: 0, weight: 3 }]. This gives you 1 On-Demand task always running (the base), with the remaining tasks distributed 25% On-Demand / 75% Spot by weight. For 10 probe workers: 1 base On-Demand + 2–3 On-Demand (weight) + 6–7 Spot = ~70% Spot cost structure. Add Application Auto Scaling with an SQS queue depth metric to scale probe workers from 2 to 50 tasks as the monitored endpoint count grows.
Capacity provider types for Fargate
ECS offers three built-in Fargate capacity providers — no EC2 instances to manage:
| Capacity provider | Pricing | Interruption risk | Best for |
|---|---|---|---|
FARGATE |
Standard On-Demand | None (SLA-backed) | Probe coordinator, API server, any task that must not be interrupted |
FARGATE_SPOT |
Up to 70% off On-Demand | 2-minute SIGTERM warning before eviction | Stateless probe workers, registry crawlers, async batch tasks |
| EC2 Auto Scaling group | EC2 On-Demand or Spot pricing | EC2 Spot: 2-minute warning; On-Demand: none | High-throughput probe workers, GPU tasks, tasks requiring specific instance types |
For Fargate-based MCP monitoring stacks, you only need FARGATE and FARGATE_SPOT. The EC2 capacity provider is relevant if your probe workers need more vCPU/memory than Fargate supports (max 16 vCPU / 120 GB per task), or if you want to run spot EC2 instance types with specific network performance characteristics.
The key difference between using a launch type vs a capacity provider: when you specify launchType: FARGATE in the console or CDK without a capacity provider strategy, 100% of tasks run On-Demand with no Spot option. Capacity providers are the only way to mix On-Demand and Spot in the same ECS service.
Base + weight formula explained
The capacity provider strategy has two parameters per provider: base and weight.
- base — a fixed number of tasks that always run on this provider, regardless of total
desiredCount. Only one provider in a strategy can have a non-zero base. - weight — a relative proportion controlling how tasks beyond the base are distributed across providers.
The formula for task allocation given desiredCount = N:
base_tasks = base (for the provider with base set)
remaining = N - base_tasks
for each provider:
provider_share = remaining * weight[provider] / sum(all weights)
Example: desiredCount=10, FARGATE base=1 weight=1, FARGATE_SPOT base=0 weight=3
base_tasks = 1 (FARGATE)
remaining = 10 - 1 = 9
FARGATE remaining share = 9 * 1/(1+3) = 2.25 → 2 tasks
FARGATE_SPOT remaining share = 9 * 3/(1+3) = 6.75 → 7 tasks
Total: 3 On-Demand (1 base + 2 weight) + 7 Spot = 10 tasks
ECS rounds fractional task counts — rounding behavior is not deterministic; you may get 2 or 3 On-Demand tasks for the remaining share. The important property is that the base count is guaranteed: at minimum 1 On-Demand task always runs, even if Spot capacity is fully unavailable and ECS cannot provision the weight-based Spot tasks.
CDK: cluster-level capacity provider setup
Before a service can use FARGATE_SPOT, the ECS cluster must have both capacity providers enabled. In CDK, enableFargateCapacityProviders() does this in one call:
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class McpMonitoringStack extends Stack {
public readonly cluster: ecs.Cluster;
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: false });
this.cluster = new ecs.Cluster(this, 'McpProbeCluster', {
vpc,
clusterName: 'mcp-probe-cluster',
containerInsights: true, // CloudWatch Container Insights for CPU/memory metrics
});
// Enables both FARGATE and FARGATE_SPOT capacity providers on the cluster.
// Without this call, services in the cluster cannot use FARGATE_SPOT —
// the API rejects CapacityProviderStrategy references to FARGATE_SPOT.
this.cluster.enableFargateCapacityProviders();
}
}
You can also set a default capacity provider strategy on the cluster (applied to services that don't specify their own strategy), but it's better to be explicit at the service level so each service makes an intentional cost/reliability choice rather than inheriting a cluster default.
CDK: service-level capacity provider strategy
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as applicationautoscaling from 'aws-cdk-lib/aws-applicationautoscaling';
import { Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';
interface ProbeWorkerServiceProps {
cluster: ecs.Cluster;
taskDef: ecs.FargateTaskDefinition;
sqsQueueArn: string;
}
export function createProbeFleet(
scope: Construct,
props: ProbeWorkerServiceProps,
): ecs.FargateService {
const service = new ecs.FargateService(scope, 'ProbeWorkerService', {
cluster: props.cluster,
taskDefinition: props.taskDef,
desiredCount: 4, // starting point; Auto Scaling adjusts this
capacityProviderStrategies: [
{
// Guarantee at least 1 On-Demand task even during Spot droughts
capacityProvider: 'FARGATE',
base: 1,
weight: 1,
},
{
// 75% of remaining tasks run on Spot (at 66% discount)
capacityProvider: 'FARGATE_SPOT',
base: 0,
weight: 3,
},
],
// Spread across AZs for Spot resilience: if one AZ loses Spot capacity,
// tasks in other AZs continue running.
vpcSubnets: {
// Use private subnets in at least 3 AZs
availabilityZones: ['us-east-1a', 'us-east-1b', 'us-east-1c'],
onePerAz: true,
},
assignPublicIp: false,
minHealthyPercent: 50, // allow ECS to terminate old tasks during updates
maxHealthyPercent: 200,
enableExecuteCommand: true, // for prod debugging via ecs execute-command
});
return service;
}
Managed Auto Scaling for probe worker fleets
Probe workers pull batches from an SQS queue. The natural Auto Scaling metric is SQS approximate visible messages per task: when the queue depth per worker rises above a threshold, scale out; when it falls near zero, scale in.
import * as applicationautoscaling from 'aws-cdk-lib/aws-applicationautoscaling';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import { Duration } from 'aws-cdk-lib';
export function attachProbeWorkerAutoScaling(
scope: Construct,
service: ecs.FargateService,
probeQueue: sqs.IQueue,
): void {
const scalableTarget = service.autoScaleTaskCount({
minCapacity: 2, // never fewer than 2 workers (1 On-Demand base + 1 Spot)
maxCapacity: 50, // max 50 workers for 50,000 endpoints at 20 probes/worker
});
// Custom metric: visible messages per running task
// ApproximateNumberOfMessagesVisible / RunningTaskCount
const messagesPerTask = new cloudwatch.MathExpression({
expression: 'queueDepth / MAX([taskCount, 1])',
usingMetrics: {
queueDepth: probeQueue.metricApproximateNumberOfMessagesVisible({
period: Duration.minutes(1),
statistic: 'Average',
}),
taskCount: new cloudwatch.Metric({
namespace: 'ECS/ContainerInsights',
metricName: 'RunningTaskCount',
dimensionsMap: {
ServiceName: service.serviceName,
ClusterName: service.cluster.clusterName,
},
period: Duration.minutes(1),
statistic: 'Average',
}),
},
period: Duration.minutes(1),
});
scalableTarget.scaleOnMetric('ScaleOnQueueDepthPerTask', {
metric: messagesPerTask,
scalingSteps: [
{ upper: 5, change: -2 }, // queue near-empty: scale in
{ lower: 5, upper: 20, change: 0 }, // steady state
{ lower: 20, upper: 50, change: +2 }, // growing backlog: add 2 workers
{ lower: 50, change: +5 }, // big spike: add 5 workers fast
],
adjustmentType: applicationautoscaling.AdjustmentType.CHANGE_IN_CAPACITY,
cooldown: Duration.minutes(2),
});
}
The cooldown of 2 minutes matches the Fargate task startup time: new Spot tasks take 30–60 seconds to start (image pull + container init), so scaling decisions within 2 minutes of the previous one are likely acting on stale metrics. A 2-minute cooldown prevents thrashing.
Scale-in for Spot tasks is handled gracefully: ECS sends a SIGTERM to the tasks being terminated (same signal as a Spot interruption), and your SIGTERM handler drains them cleanly. The only difference from a real Spot interruption is that ECS scale-in does not have a hard 2-minute deadline — ECS waits for the task to stop on its own up to the stopTimeout value (default 30s, configurable up to 120s in the task definition's container-level stopTimeout).
AliveMCP probe fleet production configuration
AliveMCP's probe fleet for 1,000 monitored endpoints uses the following capacity provider configuration:
| Service | Task size | desiredCount | Capacity strategy | Cost/mo (approx) |
|---|---|---|---|---|
| probe-coordinator | 0.5 vCPU, 1 GB | 1 | FARGATE 100% (no Spot) | ~$9 On-Demand |
| probe-worker fleet | 0.25 vCPU, 0.5 GB | 8 (auto-scales 2–20) | FARGATE base=1 weight=1 + FARGATE_SPOT weight=3 | ~$12 blended |
| registry-crawler | 0.5 vCPU, 1 GB | 1 | FARGATE_SPOT 100% | ~$3 Spot |
Total ECS compute: ~$24/mo. Without Spot, the same fleet costs ~$70/mo — a 66% reduction. The probe-coordinator runs On-Demand because it maintains the epoch clock and coordinates SQS batch distribution; interrupting it causes a 30–90 second pause in probe scheduling which, while tolerable, adds operational complexity. Running it On-Demand eliminates that concern for $9/month.
Cluster default capacity provider strategy
You can set a default strategy on the cluster to apply to services that don't specify their own capacityProviderStrategies. This is useful for development clusters where you want everything on Spot by default:
// CDK: set cluster default to 100% Spot for dev, explicit On-Demand for prod services
this.cluster.addDefaultCapacityProviderStrategy([
{ capacityProvider: 'FARGATE_SPOT', weight: 1 },
]);
// A service with no capacityProviderStrategies will use 100% FARGATE_SPOT by default.
// Override in prod by specifying capacityProviderStrategies explicitly on the service.
Be careful with cluster defaults: if a new service is deployed without an explicit capacity provider strategy and the cluster default is 100% Spot, that service may be interrupted. Explicit per-service strategies are safer than relying on the cluster default in production environments.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
CapacityProviderStrategy provider 'FARGATE_SPOT' is not associated with the cluster |
Cluster was created without enableFargateCapacityProviders(), or the cluster was created before FARGATE_SPOT was available and has never been updated |
Call cluster.enableFargateCapacityProviders() in CDK and deploy; or run aws ecs put-cluster-capacity-providers to add FARGATE and FARGATE_SPOT to an existing cluster |
| Service stuck at 0 running tasks; events show "capacity unavailable" | FARGATE_SPOT-only strategy with no FARGATE fallback; Spot capacity is unavailable in all configured AZs | Add a FARGATE base ≥ 1 so at least one On-Demand task can run when Spot is constrained; spread subnets across 3+ AZs |
| desiredCount suddenly all On-Demand after a Spot shortage | ECS replaced interrupted Spot tasks with On-Demand fallback from the FARGATE weight allocation; Spot capacity not yet restored | Normal behavior — ECS will shift back to Spot when capacity is available. CloudWatch alarm on Spot task fraction can alert when the blend drifts unexpectedly. |
| Auto Scaling not scaling in despite empty SQS queue | Scale-in cooldown is active; or the SQS metric has a 5-minute evaluation delay | Reduce SQS metric evaluation period to 1 minute; set cooldown: Duration.minutes(2) for scale-in; confirm that SQS messages are being deleted (not just read) by workers |
| High cost despite Spot strategy: majority of tasks running On-Demand | Weight-based allocation math means high base value eats into Spot share; or low desiredCount means base dominates | Verify: at desiredCount=4 with base=1, only 3 remaining tasks split by weight → 1 more On-Demand + 2 Spot = 50% Spot. To get 75% Spot at desiredCount=4: set base=0, FARGATE_SPOT weight=3, FARGATE weight=1. Tradeoff: no On-Demand guarantee. |