AWS S3 CLI Uploads: cp vs sync vs mv Explained
Use aws s3 cp for one-off copies, aws s3 sync for folders, deployments, and recurring backups, and aws s3 mv when you want copy-then-delete behaviour. Choosing the right command cuts upload time and avoids accidental data loss.
Simple explanation
What is S3 in this context? Amazon S3 is object storage: a place to put files (called objects) organised inside containers called buckets. There is no real folder hierarchy. Each object has a key (its full name, including any slashes). A key like reports/2026/january.pdf looks like a folder path but is just a flat string. The part before the filename, reports/2026/, is called a prefix.
Analogy
Think of an S3 bucket like a filing cabinet with one enormous drawer. Every file gets a full label, like reports/2026/january.pdf. The slashes make it look like folders inside folders, but the drawer has no dividers: it is just a flat list of labels. When you search for reports/2026/, S3 finds all objects whose label starts with that text. That starting text is called a prefix.
What does s3://bucket-name/prefix/file.pdf mean? This is an S3 URI. s3:// is the scheme. bucket-name is the bucket. prefix/file.pdf is the object key. When you upload a local file, you provide a local path on one side and an S3 URI on the other. The CLI figures out the direction from the URI scheme.
Why use aws s3 instead of aws s3api? The AWS APIs are powerful but verbose: s3api requires you to manage multipart uploads, handle pagination, and specify every option explicitly. The aws s3 high-level commands wrap that complexity. They handle recursive operations, parallel transfers, progress bars, and multipart uploads automatically. For everyday uploading, downloading, and syncing, aws s3 is the right tier.
Before you start
You need four things in place before any aws s3 command will work:
- AWS CLI installed and configured. Follow the AWS CLI installation guide to get set up. Run
aws --versionto confirm it is installed. - Credentials available. Your CLI must know which AWS identity to use: a named profile, environment variables, or an attached IAM role. See the AWS CLI overview for how profiles and credential chains work. If you are working inside AWS, AWS CloudShell comes pre-authenticated.
- Target bucket exists. You cannot upload to a bucket that does not exist. Create the bucket first with the console, Terraform, or
aws s3api create-bucket. - IAM permission to upload. Your identity needs at minimum
s3:PutObjecton the target bucket/prefix. If uploads fail withAccessDenied, see the S3 access denied errors guide.
cp vs sync vs mv at a glance
Mental model
cp is like emailing a file attachment: you pick something and send it, no questions asked, whether or not the recipient already has it. sync is like Dropbox: it checks what is already there and only sends the difference. mv is like cutting and pasting: the original disappears after the operation completes.
| Command | Best for | What it does | Re-uploads unchanged files? | Deletes source? | Common risk |
|---|---|---|---|---|---|
aws s3 cp | Single file or one-off directory copy | Copies exactly what you specify, always | Yes, always | No | Slow and costly for large repeated deployments |
aws s3 sync | Deployments, backups, folder mirroring | Compares source and destination, transfers only what changed | No, skips unchanged | No (unless --delete) | --delete can remove objects you intended to keep |
aws s3 mv | Renaming or reorganising objects inside S3 | Copies to destination, then deletes source | N/A, one-time operation | Yes, deletes source after copy | Not atomic: a failed delete leaves both copies |
How uploads work
The AWS CLI uses S3 URIs to determine direction:
- Local to S3:
aws s3 cp ./file.pdf s3://my-bucket/file.pdfuploads from your machine. - S3 to Local:
aws s3 cp s3://my-bucket/file.pdf ./file.pdfdownloads to your machine. - S3 to S3:
aws s3 cp s3://bucket-a/file.pdf s3://bucket-b/file.pdfcopies entirely within S3’s infrastructure, not through your machine. No data transfer cost within the same region.
The key difference between cp and sync is state awareness. cp is stateless: it copies whatever you point it at, every time. sync checks the destination first. It compares object size and last-modified timestamp (not a checksum by default) and skips any file that already matches. This makes sync the better choice for anything you run more than once.
aws s3 cp
Use cp when you need to copy a specific file once, for example uploading an artifact, downloading a config, or making a point-in-time copy between two S3 locations.
# Upload a single file to S3
aws s3 cp report.pdf s3://my-company-assets/reports/report.pdf
# Download a file from S3 to the current directory
aws s3 cp s3://my-company-assets/reports/report.pdf ./report.pdf
# Copy between two S3 locations (transfer happens inside S3, not through your machine)
aws s3 cp s3://my-company-assets/reports/report.pdf \
s3://my-company-archive/reports/report.pdf
# Upload an entire directory recursively
aws s3 cp ./logs/ s3://my-company-logs/app/ --recursive
# Upload with a specific storage class
aws s3 cp archive.tar.gz s3://my-company-archive/ --storage-class GLACIER
# Upload with custom metadata
aws s3 cp report.pdf s3://my-company-assets/reports/report.pdf \
--metadata '{"author":"jane.smith","department":"finance"}'When copying between two S3 locations, the data stays within S3; nothing passes through your machine. This is much faster than download-then-reupload, and copies within the same region incur no data transfer charge.
aws s3 sync
sync is the better command any time you are transferring a directory and do not want to re-upload files that have not changed. It is the standard tool for deploying static sites, running nightly backups, and mirroring folders.
# Deploy a local build directory to S3 (only upload changed files)
aws s3 sync ./dist/ s3://my-company-site/
# Preview what sync would do without transferring anything
aws s3 sync ./dist/ s3://my-company-site/ --dryrun
# Sync and remove objects from S3 that no longer exist locally
aws s3 sync ./dist/ s3://my-company-site/ --delete
# Sync S3 back down to a local directory (useful for disaster recovery)
aws s3 sync s3://my-company-assets/backups/ ./local-restore/
# Sync only .html and .css files, exclude everything else
aws s3 sync ./dist/ s3://my-company-site/ \
--exclude "*" \
--include "*.html" \
--include "*.css"If you are deploying a 500-file build directory and only three files changed, sync transfers three files. cp —recursive transfers all 500. Over dozens of daily deploys that difference compounds into real time and cost savings. Use sync for any workflow you run more than once.
The --delete flag makes the S3 bucket a mirror of your local source. In a CI/CD context this is usually what you want: old files get removed when you delete them locally. Outside of deployments, use it carefully. It permanently removes S3 objects without a recycle bin.
Always run —dryrun first before using sync —delete on any bucket with data you care about. There is no undo. Objects deleted by —delete are gone immediately unless the bucket has versioning enabled.
aws s3 mv
aws s3 mv is copy-then-delete, not a true atomic move. It copies the object to the destination first, then deletes the source. If the copy succeeds but the delete fails, you end up with both copies. There is no rollback.
# Rename or move an object within the same bucket
aws s3 mv s3://my-company-assets/incoming/report.pdf \
s3://my-company-assets/processed/report.pdf
# Move a local file to S3 (uploads it, then deletes the local copy)
aws s3 mv ./report.pdf s3://my-company-assets/reports/report.pdfWhen to use it: Moving processed files along a workflow inside S3, for example from an incoming/ prefix to processed/ after a Lambda function handles them.
When not to use it: For large-scale reorganisation, prefer sync with --delete, which handles partial failures more gracefully. Avoid mv on critical files where losing the source before confirming the destination copy would be a problem.
Filtering with —exclude and —include
Both cp --recursive and sync support --exclude and --include to filter which files are transferred. The flags accept glob patterns and are evaluated left to right.
# Sync but exclude all log files
aws s3 sync ./dist/ s3://my-company-site/ --exclude "*.log"
# Sync only JavaScript files (exclude everything first, then re-include *.js)
aws s3 sync ./dist/ s3://my-company-site/ \
--exclude "*" \
--include "*.js"
# Exclude a subdirectory
aws s3 sync ./project/ s3://my-company-assets/project/ \
--exclude ".git/*"
# Exclude multiple patterns
aws s3 sync ./dist/ s3://my-company-site/ \
--exclude "*.log" \
--exclude "*.tmp" \
--exclude "node_modules/*"Order matters. Filters are applied in the sequence you write them. --exclude "*" --include "*.js" means: exclude everything, then re-include JS files, so only .js files transfer. Reversing the order to --include "*.js" --exclude "*" would cause the final --exclude "*" to override the include and nothing would transfer at all.
Use —dryrun when testing a new filter combination. It shows exactly which files would be transferred without touching S3, so you can confirm the pattern is correct before committing.
Multipart uploads
For files over 8 MB, the AWS CLI automatically switches to multipart upload. The file is split into chunks that upload in parallel, then assembled at S3. If one chunk fails, only that chunk retries; not the whole file. For large files this is significantly faster than a single stream.
Analogy
Think of multipart upload like shipping a large order by splitting it across several smaller parcels. Each parcel travels independently. If one gets lost, only that parcel needs to be resent. The recipient (S3) reassembles the full order once all parts arrive. Sending one huge parcel would be slower and riskier: if anything goes wrong you start over from scratch.
You can tune the thresholds in ~/.aws/config:
[profile default]
s3 =
multipart_threshold = 64MB
multipart_chunksize = 16MB
max_concurrent_requests = 10Raising multipart_threshold means smaller files use a single stream (simpler, slightly fewer API calls). Raising max_concurrent_requests speeds up large directory syncs by running more parallel transfers.
Incomplete multipart uploads leave billable parts in your bucket. If a large upload is interrupted, the partial chunks stay in S3 and accrue storage charges. Use an S3 lifecycle rule with AbortIncompleteMultipartUpload to automatically clean them up. Seven days is a common default.
Output flags and scripting
By default, cp and sync print a progress bar to stderr and a summary of transferred files. In scripts and CI pipelines you usually want cleaner output.
# Suppress the progress bar but still print file-transfer lines
aws s3 sync ./dist/ s3://my-company-site/ --no-progress
# Print only errors (no progress bar, no per-file output)
aws s3 sync ./dist/ s3://my-company-site/ --only-show-errors
# Complete silence (no output at all, check the exit code instead)
aws s3 sync ./dist/ s3://my-company-site/ --quietThese three flags are distinct:
--no-progressremoves the animated progress bar but keeps the per-file transfer lines. Good for log files where you want a record of what transferred.--only-show-errorssuppresses everything except error messages. Useful for a clean CI log that only gets noisy on failure.--quietsuppresses all output including errors to stdout/stderr. Use this only when you are capturing the exit code and handling errors programmatically.
For CI/CD pipelines, —only-show-errors is the right default. Logs stay quiet on success, but failures surface immediately. Avoid —quiet unless you have explicit error handling downstream, because a failed deploy will produce no output at all.
When to use each command
| I want to… | Use this |
|---|---|
| Upload a single file | aws s3 cp |
| Download a single file from S3 | aws s3 cp |
| Deploy a static site build directory | aws s3 sync with --delete |
| Mirror a folder to S3 as a backup | aws s3 sync |
| Avoid re-uploading files that have not changed | aws s3 sync |
| Preview what would transfer before running for real | aws s3 sync --dryrun |
| Reorganise or rename objects inside S3 | aws s3 mv (within S3) |
| Move a local file to S3 and delete the local copy | aws s3 mv |
| Upload everything in a folder, even if unchanged | aws s3 cp --recursive |
| Run uploads in a CI/CD pipeline | aws s3 sync --only-show-errors |
Common mistakes
- Using
cp —recursivein deployment pipelines whensyncis appropriate.cp —recursivere-uploads every file every time, regardless of whether anything changed. For a 500-file build directory deployed ten times a day,syncis dramatically faster and cheaper. - Using
sync —deletewithout a—dryrunpass first. It is easy to accidentally delete objects that were intentionally placed in the S3 bucket but are not tracked in your local source. Always preview with—dryrunon a production bucket before committing. - Wrong profile or wrong account. If you have multiple AWS profiles, forgetting to pass
—profile my-profilesends commands to whichever account your default profile points to. Double-check withaws sts get-caller-identitybefore running destructive operations. - Wrong region. If your CLI default region differs from the bucket’s region, commands may fail or target the wrong endpoint. Pass
—region eu-west-1explicitly in scripts to avoid ambiguity. - AccessDenied errors. These usually mean your IAM identity lacks
s3:PutObjecton the target prefix, or a bucket policy is blocking access. Check the S3 access denied errors guide for a systematic debugging approach. - Broken flags from copy-paste. If you copy commands from a PDF, Word doc, or certain websites, double hyphens like
—deletemay arrive as typographic em-dashes. The CLI will silently ignore or misparse these. Always retype flags manually if a command behaves unexpectedly. - Not cleaning up incomplete multipart uploads. Interrupted uploads leave partial data in S3 that accrues charges. Add an S3 lifecycle rule to abort incomplete multipart uploads after 7 days.
Public access warning
You may see examples online that include —acl public-read to make uploaded files publicly accessible. This approach has several problems in modern AWS setups. S3 Block Public Access is enabled by default on new buckets and accounts, which causes this flag to fail with an error. Even when Block Public Access is disabled, S3 security best practices strongly discourage making objects publicly readable via ACLs. Object Ownership settings on newer buckets also disable ACL enforcement entirely.
For serving files publicly, use a bucket policy scoped to specific principals or a CloudFront distribution with an Origin Access Control. That is the correct path, not —acl public-read on individual uploads.
Summary
aws s3 cpcopies whatever you specify, always, every time. Use it for one-off file operations.aws s3 synccompares source and destination first, then only transfers what changed. Use it for deployments and backups.aws s3 mvis copy-then-delete and is not atomic. Use it for workflow-based object reorganisation, not bulk moves.- Use
—dryrunbefore any sync with—deleteon a production bucket. - Use
—excludeand—includeto filter transfers; order of flags matters. - The CLI auto-applies multipart upload above 8 MB. Set a lifecycle rule to clean up incomplete uploads.
- In CI/CD, use
—only-show-errorsto keep logs clean without hiding failures.
Frequently asked questions
What is the difference between aws s3 cp and aws s3 sync?
cp copies whatever you tell it to, every single run. sync compares source and destination first, then only transfers files that are new or have changed. Use cp for one-off uploads; use sync for deployments, recurring backups, and any workflow where re-uploading unchanged files wastes time and money.
Does aws s3 sync delete files from S3?
No, not by default. sync only adds or updates files. To also remove objects from S3 that no longer exist at the source, add the --delete flag. Always run --dryrun first to preview what would be deleted before committing on a production bucket.
How do I upload a whole folder to S3?
Use aws s3 sync ./my-folder/ s3://my-bucket/prefix/. It handles all files recursively and only uploads what changed. If you want to force-upload everything every time, use aws s3 cp ./my-folder/ s3://my-bucket/prefix/ --recursive instead.
When does multipart upload happen automatically?
The AWS CLI switches to multipart upload for files over 8 MB by default. The file is split into chunks that upload in parallel. You can raise the threshold in ~/.aws/config using the multipart_threshold setting if you prefer single-stream uploads for smaller large files.
Should I use cp, sync, or mv in CI/CD pipelines?
For deploying a build directory to S3, use sync: it only uploads what changed, making deploys faster and cheaper. Pair it with --delete to keep the bucket in sync with your build output, and always run --dryrun in a PR preview step before the real deploy.