CloudFront is cheap and fast when the cache key is tight and deploys use versioned URLs - and slow and expensive when the key is wide and every release fires a wall of invalidations. Most CloudFront SDK mistakes are not crashes; they are quietly low hit rates and quietly high invalidation bills.
Walk this list once now, then use it as a review checklist before shipping any code that creates, updates, or serves through a distribution.
Keep the cache key as narrow as correctness allows. Every query string, header, or cookie you add to a cache policy multiplies the number of cached copies and lowers the hit rate.
Use managed cache policies where they fit.CachingOptimized for static assets and CachingDisabled for dynamic APIs cover most cases without a custom policy.
Separate the cache key from what you forward. Forward a header to the origin via an origin request policy without adding it to the cache key, so the origin sees it but the cache does not fragment.
Cache per path, not globally. Give static and dynamic paths their own behaviors and policies instead of one compromise setting for everything.
Attach a managed cache policy by id rather than a legacy ForwardedValues block.
Prefer versioned URLs over invalidations. Fingerprinted filenames (app.9f2a1c.js) change the cache key on each build, busting cache for free instead of paying per invalidation path.
Set long TTLs on hashed assets, no-cache on HTML. Cache immutable assets for a year and always revalidate the small HTML entry point that references them.
Invalidate with /*, not file lists, when you must. A single /* counts as one billable path; listing hundreds of files counts as hundreds.
Generate a fresh CallerReference per invalidation. Reuse dedupes to the previous batch or errors on a changed one.
Reserve invalidations for emergencies. A mistaken publish or takedown warrants one; a routine deploy should not.
Bust cache by changing the URL; keep invalidations for the rare forced purge.
Treat every update as read-modify-write.GetDistributionConfig, change the returned config, then UpdateDistribution with its ETag as IfMatch.
Send the full config, not a patch.UpdateDistribution replaces the entire configuration; a partial object drops fields.
Keep every Quantity in sync with its Items.Origins, CacheBehaviors, Paths, and AllowedMethods all reject a count that does not match the list length.
Handle PreconditionFailed (412) by refetching. A stale ETag means someone else changed the config; re-read and reapply.
Wait for Deployed before depending on a change. A returned call is InProgress; use the distribution_deployed waiter when correctness depends on propagation.
Read, mutate the whole config, then update with the ETag.
Lock S3 origins with Origin Access Control. Use OAC (via CreateOriginAccessControl) plus a bucket policy so only CloudFront reads the bucket; do not expose it publicly.
Prefer OAC over legacy OAI. OAC supports SigV4 and SSE-KMS buckets; CreateCloudFrontOriginAccessIdentity is legacy and should not be chosen for new work.
Enforce HTTPS to the viewer. Set ViewerProtocolPolicy to redirect-to-https or https-only on every behavior.
Use HTTPS to custom origins. Set OriginProtocolPolicy to https-only so the edge-to-origin hop is encrypted too.
Gate private content with signed URLs/cookies and key groups. Register a public key in a key group and set TrustedKeyGroups on the behavior; keep the private key in a secret store.
Use managed origin-request and response-headers policies. They add security, CORS, and forwarding behavior declaratively without a custom policy or edge function.
Reach for CloudFront Functions before Lambda@Edge. Cheaper and faster for header rewrites, redirects, and URL normalization; escalate to Lambda@Edge only for origin events, network calls, or heavier compute.
Associate only published, versioned Lambda@Edge ARNs in us-east-1. Never $LATEST; the function and version must be in us-east-1 to replicate.
Test and publish edge functions before associating. Validate with TestFunction, then PublishFunction with the ETag; a runtime error on viewer-request fails the request.
Rely on built-in retries and log the request id. Both SDKs retry throttling with backoff; capture ResponseMetadata.RequestId (boto3) or $metadata.requestId (SDK v3) on failure for support.
Keep the cache key narrow. Use a cache policy that includes only the query strings, headers, and cookies that genuinely change the response; every extra field multiplies cached copies and lowers hits.
Should I invalidate on every deploy?
No. Use fingerprinted asset URLs so each build changes the cache key for free, and keep HTML on no-cache. Reserve invalidations for emergencies like a bad publish or takedown.
Why is /* cheaper than listing files?
Invalidations bill per path. /* is a single path that clears everything, while listing 300 files is 300 billable paths. /* also evicts unchanged objects, so use it sparingly.
What is the correct way to update a distribution?
Read the current config with GetDistributionConfig, mutate the full object, and call UpdateDistribution with the config's ETag as IfMatch. Never send a partial config.
Why do I get a 412 PreconditionFailed on update?
The ETag you passed as IfMatch is stale - the config changed since you read it. Re-fetch with GetDistributionConfig and reapply your change against the new ETag.
What is the Quantity field and why does it reject my update?
Every list-shaped CloudFront field carries both Items and a Quantity count that must equal the list length. Recompute Quantity after any mutation or the request fails.
Should I use OAC or OAI to lock an S3 origin?
Use OAC (CreateOriginAccessControl). It supports SigV4 and SSE-KMS buckets. OAI (CreateCloudFrontOriginAccessIdentity) is legacy and should not be chosen for new distributions.
Do I still use ForwardedValues to control caching?
No. Use managed or custom cache policies (CachePolicyId) and origin request policies (OriginRequestPolicyId). ForwardedValues is legacy and cannot be combined with a policy id on one behavior.
When should I choose Lambda@Edge over a CloudFront Function?
When you need an origin-side event, network or filesystem access, request-body access, or more than a sub-millisecond compute budget. Otherwise prefer a CloudFront Function - it is cheaper and faster.
How do I serve private content through CloudFront?
Lock the origin with OAC, register a public key in a key group, set TrustedKeyGroups on the behavior, and hand out signed URLs or signed cookies generated with the matching private key.
How do I know a config change is live everywhere?
Wait until the distribution reports Deployed, not just that the update returned. Use the distribution_deployed waiter (boto3) or waitUntilDistributionDeployed (SDK v3).
Should I write my own retry logic for CloudFront calls?
Usually no. Both SDKs retry throttling and transient errors with backoff. Tune retry mode and max attempts instead, and log the request id on failures for support.