AWS S3 sync deletes existing uploads because --delete runs against the wrong prefix
A deploy script ran this:
aws s3 sync ./dist s3://acme-prod/uploads/ --delete
The intent was to publish build output. The result was that every object under uploads/ that was not in ./dist was deleted—including customer-uploaded files. The site still loaded, but profile images and attachments returned 404s until the bucket’s versioned objects were restored. The cost was not the sync; it was the rollback, support load, and the next deploy being delayed while everyone checked prefixes.
How an aws s3 sync --delete wrong prefix erases unrelated objects
aws s3 sync makes the destination match the source. With --delete, files present in the destination but absent from the source are deleted. [link needed]
The dangerous part is scope. S3 has no real folders; a “prefix” is just a key prefix. If the destination prefix is broader than the source’s intended target, --delete treats every object under that broader prefix as fair game. An aws s3 sync --delete wrong prefix can therefore remove unrelated assets, another team’s files, or user uploads that were never part of the build.
Two common forms:
# Source is narrower than destination: build output vs. all uploads
aws s3 sync ./dist s3://acme-prod/uploads/ --delete
# Destination prefix is one level too high: releases/ vs. releases/app/
aws s3 sync ./dist s3://acme-prod/releases/ --delete
In both cases the command is valid. The deletion is expected behavior. The path is the bug.
A safe preflight procedure
1. Write the source and destination as explicit variables and inspect them before running anything:
SRC="./dist/"
DST="s3://acme-prod/app/static/"
printf 'SRC=%s\nDST=%s\n' "$SRC" "$DST"
2. Confirm that the destination prefix is dedicated to this source. If uploads/ contains both build assets and user files, sync to app/static/ or omit --delete.
3. Run the exact command with --dryrun and --delete:
aws s3 sync "$SRC" "$DST" --delete --dryrun
4. Read the (delete) lines. If you see filenames that do not belong to the build, stop and fix the prefix.
5. Check the destination contents separately:
aws s3 ls --recursive "$DST" | wc -l
find "$SRC" -type f | wc -l
The counts do not need to match exactly—excludes and nested paths can differ—but a large gap is a warning. [link needed]
6. Make sure bucket versioning is enabled, or take a backup of the destination prefix. Versioning makes recovery from an aws s3 sync --delete wrong prefix much cheaper. [link needed]
7. Run the command without --dryrun, then spot-check a known old object and a newly uploaded object.
When NOT to use --delete
Do not use --delete when the destination prefix is shared by multiple producers: user uploads plus deploy artifacts, two services writing under one prefix, or manual files that are not in source control. Also skip it during a first migration or when syncing a partial build that intentionally excludes files.
In those cases, either sync without --delete or create a dedicated prefix per source. A dedicated prefix is usually simpler than maintaining complex --exclude/--include rules. This is where an aws s3 sync --delete wrong prefix hurts most: the command is correct, but the destination contract was never defined.
Preflight checklist
- Destination prefix is dedicated to this source.
- Source and destination are printed and reviewed.
--dryrun --deleteoutput has been read.- Destination count versus source count is sanity-checked.
- Versioning or a backup exists.
- Old and new objects are checked after the run.
- If the prefix is shared,
--deleteis omitted or the sync is moved to a subprefix.