aws s3 cp copies what you point it at; aws s3 sync compares source and destination and copies only what differs — by size and last-modified time, not by content hash. Sync never deletes anything unless you pass --delete, and that flag is the one that turns a typo into an outage. The patterns below are the ones that survive contact with production: dry-run first, understand the comparison rules, order your exclude/include filters correctly, and tune concurrency before blaming the network.
What’s happening
Both commands sit on the same transfer engine in the CLI. Files above the multipart threshold (8 MB by default) are split into parts and uploaded in parallel; smaller files upload whole, several at a time, governed by max_concurrent_requests (10 by default). That engine is why a single cp of a 50 GB file saturates a link that a naive script never would — and why an interrupted big copy leaves incomplete multipart parts you keep paying for.
Sync’s comparison logic is the part people misunderstand. A file is copied when the sizes differ or when the source’s last-modified time is newer than the destination’s. Same size and older-or-equal timestamp means skipped — even if the bytes changed. There is no checksum comparison. --size-only ignores timestamps entirely (useful when a build regenerates identical files with fresh mtimes), and --exact-timestamps makes S3-to-local syncs re-download when times differ at all rather than only when the remote is newer. Full flag reference lives in the S3 documentation.
What to check first
- Which direction and what should happen to extra files at the destination. Sync without
--deleteonly adds and updates; with it, the destination becomes a mirror — including deletions. - Whether the destination is versioned. A bad
--deleteon a versioned bucket is recoverable; on an unversioned bucket it is not. - Object count. Sync lists both sides before copying; on millions of objects the listing alone takes serious time and request money.
- Who you are.
aws sts get-caller-identitybefore anything destructive — wrong-profile accidents are a classic.
How to verify
--dryrun prints every operation without performing it, and it works on both commands:
# Preview exactly what a deploy would change — and what --delete would remove
aws s3 sync ./dist s3://my-bucket/site/ --delete --dryrun
Confirm what actually landed:
aws s3 ls s3://my-bucket/site/ --recursive --summarize | tail -2
# Spot-check one object's metadata and storage class
aws s3api head-object --bucket my-bucket --key site/index.html
The runbook
-
Dry-run anything with
--delete, every time. The flag removes destination files missing from the source. Pointing sync at the bucket root instead of a prefix, with--delete, is how whole sites disappear:aws s3 sync ./dist s3://my-bucket/site/ --delete --dryrun aws s3 sync ./dist s3://my-bucket/site/ --delete -
Order exclude/include filters left to right — the last match wins. To copy only one file type, exclude everything first, then re-include:
aws s3 sync ./media s3://my-bucket/media/ --exclude "*" --include "*.jpg"Reversing the order (
--includefirst) silently copies everything. -
Set the storage class at upload time instead of paying for a transition later:
aws s3 cp backup.tar.gz s3://my-bucket/backups/ --storage-class STANDARD_IA -
Stream through the CLI with
-to skip local disk entirely. Database dumps are the everyday case:mysqldump --single-transaction mydb | gzip | aws s3 cp - s3://my-bucket/dumps/mydb.sql.gzThe reverse (
aws s3 cp s3://... -) pipes an object to stdout for restores. -
Tune the transfer engine before blaming bandwidth. On a machine with headroom, raising concurrency is usually the whole fix:
aws configure set default.s3.max_concurrent_requests 20 aws configure set default.s3.multipart_chunksize 64MB -
Use
--size-onlywhen mtimes lie. CI checkouts and build steps reset timestamps, making sync re-upload identical files;--size-onlystops the churn (at the cost of missing same-size content changes).
When this is the wrong move
Sync is the wrong tool for millions of objects on a schedule — the double listing dominates runtime; use S3 Batch Operations, replication, or inventory-driven jobs instead. It is the wrong tool for guaranteed integrity decisions, because it never compares content — if bytes must match, verify checksums separately. And for continuous replication between buckets, S3 Replication does it server-side without a machine in the middle.
Common mistakes
- Running
--deleteagainst the wrong prefix (or the bucket root) and mirroring the mistake instantly — dry-run exists for exactly this. - Putting
--includebefore--exclude "*"and wondering why everything copied — filter order is left to right, last match wins. - Expecting sync to catch edited files whose size and mtime did not change; it compares metadata, not content.
- Running one sync per directory in a loop instead of one sync with filters — each invocation pays the listing cost again.
Keeping it under control
- Wrap production syncs in a script that hardcodes the profile, region, prefix, and a mandatory dry-run step.
- Turn on versioning for any bucket that is a sync destination with
--deletein reach. - Put the abort-incomplete-multipart lifecycle rule on upload buckets so interrupted transfers do not bill forever.
- Pin CLI transfer settings (
max_concurrent_requests,multipart_chunksize) in the automation’s own config rather than relying on whatever the host has.
The CLI rewards operators who read the comparison rules once and script defensively ever after — most s3 sync disasters are one missing --dryrun away from being non-events. If nobody on your team truly owns the AWS environment end to end, that is the root of most of these problems — Stack Harbor runs AWS accounts daily under AWS Operations, and you can book an AWS review.