The cost was larger than the purge

Campaign activity generated repeated CloudFront invalidations. The direct invalidation spend was only part of the problem: failed or repeated operations could also lead to more CodeBuild and CodePipeline runs.

I identified the important campaign invalidations and opportunities to combine repeated work, then designed the service end to end: API, queues, state, scheduling, retries, metrics, and delivery.

One service owns the constraint

A serverless API accepts requests and returns a tracking ID. Exact paths accumulate in DynamoDB by distribution and priority. Pattern requests enter priority-aware FIFO queues.

A Node.js worker on ECS Fargate owns scheduling and CloudFront capacity. It batches queued paths, prefers urgent work, retries when throttled, and tracks each request through completion. DynamoDB status and EventBridge notifications give downstream systems a clear lifecycle.

Choose the runtime for the job

Ingestion is short-lived and bursty; Lambda is a useful fit. Scheduling and completion polling are ongoing processes, so the worker runs in a container. Separating those responsibilities makes their operating constraints explicit.

Conditional updates protect shared path state. Failed work has a dead-letter route rather than disappearing after the last retry.

What changed

Teams gained a shared service instead of independently racing the CDN. Combining repeated work reduced invalidation activity and helped avoid unnecessary reruns of dependent builds and pipelines. The outcome is reduced spend and more predictable campaign operations.

Batching helps manage API capacity. It does not, by itself, make different paths free: CloudFront’s pay-as-you-go invalidation billing counts submitted paths, with the first 1,000 paths per month free. Avoiding repeated and unnecessary paths is a separate part of the cost story.