AWS Fargate Spot: Running Serverless Containers at 70 Percent Less

Fargate Spot runs your containers on spare AWS capacity at up to 70 percent off the standard Fargate price. When AWS needs the capacity back, your task gets a SIGTERM signal with a two-minute window to clean up before termination. For the right workloads, this is the most impactful cost optimization available on ECS.

The key is understanding which workloads tolerate interruption and which do not. In this article I will walk through configuring Fargate Spot with capacity providers, mixing Spot and on-demand for fault tolerance, and writing tasks that handle the interruption signal gracefully.

Capacity Provider Strategy

ECS capacity providers define how tasks are placed across Fargate and Fargate Spot. You configure a strategy that says: use Spot for most tasks, fall back to on-demand when Spot capacity is unavailable.

resource "aws_ecs_cluster" "main" {
  name = "production"

  configuration {
    execute_command_configuration {
      logging = "OVERRIDE"
      log_configuration {
        cloud_watch_log_group_name = "/aws/ecs/execute-command"
      }
    }
  }
}

resource "aws_ecs_cluster_capacity_providers" "main" {
  cluster_name = aws_ecs_cluster.main.name

  capacity_providers = ["FARGATE", "FARGATE_SPOT"]

  default_capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    weight            = 4
    base              = 0
  }

  default_capacity_provider_strategy {
    capacity_provider = "FARGATE"
    weight            = 1
    base              = 1
  }
}

resource "aws_ecs_service" "batch_processor" {
  name            = "batch-processor"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.batch_processor.arn
  desired_count   = 10

  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    weight            = 4
    base              = 0
  }

  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    weight            = 1
    base              = 1
  }

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.batch.id]
    assign_public_ip = false
  }
}

resource "aws_ecs_service" "api" {
  name            = "api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.api.arn
  desired_count   = 3

  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    weight            = 1
    base              = 3
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.api.arn
    container_name   = "api"
    container_port   = 8080
  }

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.api.id]
    assign_public_ip = false
  }
}

The weight values define the proportion of tasks placed on each capacity provider. With Spot weight 4 and on-demand weight 1, approximately 80 percent of tasks go to Spot. The base = 1 on on-demand means at least one task always runs on on-demand, ensuring the service never drops to zero tasks if Spot capacity is unavailable.

The api service uses FARGATE only, no Spot. User-facing services should not be interrupted without warning. A two-minute termination window is acceptable for batch work and is not acceptable for an HTTP service where the load balancer is actively routing requests to the task. Keep Spot for background processing.

Handling Interruption Signals

When Fargate Spot reclaims a task, it sends SIGTERM to your container. Your application has two minutes to finish current work and exit cleanly. Design your workload so each unit of work is small enough to complete within that window, or implement checkpointing so interrupted work can resume from the last checkpoint rather than starting over.

import signal
import sys
import boto3
import json
import logging

logger    = logging.getLogger()
sqs       = boto3.client("sqs")
shutdown  = False

def handle_sigterm(signum, frame):
    global shutdown
    logger.info("SIGTERM received, finishing current item and shutting down")
    shutdown = True

signal.signal(signal.SIGTERM, handle_sigterm)
signal.signal(signal.SIGINT,  handle_sigterm)

def process_queue(queue_url: str):
    while not shutdown:
        response = sqs.receive_message(
            QueueUrl            = queue_url,
            MaxNumberOfMessages = 1,
            WaitTimeSeconds     = 5,
            VisibilityTimeout   = 90
        )

        messages = response.get("Messages", [])
        if not messages:
            continue

        message = messages[0]
        try:
            body = json.loads(message["Body"])
            process_item(body)
            sqs.delete_message(
                QueueUrl      = queue_url,
                ReceiptHandle = message["ReceiptHandle"]
            )
            logger.info(f"Processed item {body.get('id')}")
        except Exception as e:
            logger.error(f"Failed to process item: {e}")

    logger.info("Graceful shutdown complete")
    sys.exit(0)

Setting VisibilityTimeout to 90 seconds gives the current item enough time to complete before other consumers can pick it up. When SIGTERM fires, the loop exits after the current item finishes. If the task is terminated before the item completes, the visibility timeout expires and SQS makes the message visible again for another consumer. The item is reprocessed, not lost. This pattern requires idempotent processing, which is a requirement for any reliable queue consumer regardless of whether you use Spot or not.

Workloads That Suit Fargate Spot

Good candidates are batch data processing pipelines where each unit of work is small and independently restartable, image or video processing jobs where progress can be checkpointed, report generation that runs on a schedule rather than in response to user requests, test runners in CI/CD pipelines where a failed task triggers a retry automatically, and ML inference workloads that are stateless and can be restarted without losing work.

Poor candidates are anything user-facing with latency requirements, stateful services that hold in-memory state across requests, and long-running jobs where two minutes is not enough time to save progress before termination.

Monitoring Interruptions

resource "aws_cloudwatch_metric_alarm" "spot_interruptions" {
  alarm_name          = "fargate-spot-interruptions"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "InterruptedTaskCount"
  namespace           = "ECS/ContainerInsights"
  period              = 300
  statistic           = "Sum"
  threshold           = 5
  alarm_description   = "Fargate Spot interruptions exceeding threshold"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    ClusterName = aws_ecs_cluster.main.name
    ServiceName = aws_ecs_service.batch_processor.name
  }
}

Monitoring interruption rate helps you understand Spot availability patterns for your regions and time zones. A high interruption rate might mean your workload needs to redistribute across regions, or that current Spot capacity pressure warrants increasing the on-demand fallback weight temporarily.

Closing Thoughts

Fargate Spot is one of the simplest cost reductions available on ECS. For batch processing workloads it is effectively free money: you write an interruption handler, configure a capacity provider strategy, and your infrastructure costs drop by 60 to 70 percent for those workloads. The engineering investment is an afternoon.

Audit your ECS services and identify which ones run background processing rather than serving user requests. Move those to Spot. Keep user-facing services on on-demand. The cost savings compound every month without any ongoing maintenance.

Enjoy the cloud.

Osama


#AWS #FargateSpot #ECS #CostOptimization #CloudArchitecture #ServerlessContainers #Terraform #InfrastructureAsCode #AmazonWebServices #SolutionsArchitect #CloudComputing #FinOps #BatchProcessing #CloudNative #TechBlog #CloudInfrastructure #Containers #Docker #SRE #DevOps

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.