gsutil: The Legacy Cloud Storage CLI
Verified against gsutil 5.35, boto 2.49.0 (bundled with Google Cloud SDK 553.0.0), flags verified via · official docs
What it is and where it fits 🎯#
gsutil was Cloud Storage's original command-line tool, and gcloud storage (page 03) is its official
replacement, faster, better integrated with gcloud's own auth/config, and the one Google now recommends
for new scripts. This page exists because gsutil is still installed by default, still runs plenty of
existing production scripts and Terraform provisioners, and still covers a handful of things gcloud storage genuinely doesn't yet, not because it's the tool to reach for first. Read page 03 for everyday
object operations; read this page to understand gsutil's own config file, the commands it still uniquely
offers, and how to read (or migrate) a script written against it.
Core concepts: where gsutil's config actually lives#
Unlike gcloud storage, which reads gcloud's own config directory (page 00) directly, gsutil is built
on the third-party boto library and keeps its own separate config file, ~/.boto, plus a
per-account fallback under ~/.config/gcloud/legacy_credentials/<account>/.boto that gcloud auth login
writes automatically so gsutil has something to authenticate with even without its own gsutil config
step, confirmed directly against a real gsutil version -l invocation's reported config search path. This
is the practical reason a freshly-authenticated gcloud session can run gsutil commands immediately with
no separate gsutil config step, the legacy .boto credential file already exists by the time gcloud auth login finishes.
Config file: ~/.boto#
[Credentials]
# Populated automatically by `gcloud auth login` — rarely hand-edited directly
[Boto]
# proxy = myproxy.example.com:8080
[GSUtil]
parallel_thread_count = 10
parallel_process_count = 4
parallel_composite_upload_threshold = 150M
use_magicfile = True
[OAuth2][GSUtil] is the section worth hand-editing, parallel_composite_upload_threshold is what actually
controls whether a large upload gets split into concurrent chunks (composite upload) or sent as one
sequential stream, tuning it up or down directly trades upload speed against the API-request overhead of
managing more, smaller pieces. Regenerate or repair the file with gsutil config -n if it's ever missing
or corrupted, that flag skips the (obsolete, gcloud-superseded) interactive OAuth authorization step
and only rebuilds the config scaffolding.
Commands that still only exist in gsutil#
gsutil rewrite -k gs://my-bucket/** # re-encrypt every object under a bucket with the bucket's CURRENT default KMS key
gsutil rewrite -s STANDARD gs://my-bucket/old-logs/** # bulk storage-class change without a full copy
gsutil notification create -f json -t my-pubsub-topic -e OBJECT_FINALIZE gs://my-bucket
gsutil notification list gs://my-bucket
gsutil du -sh gs://my-bucket # human-readable total size, no equivalent gcloud storage flag as of this version
gsutil hash -h gs://my-bucket/file.zip # compute/display CRC32C and MD5 without downloading the objectrewrite -k is the bulk key-rotation tool, when a KMS key (page 08) is rotated, existing objects
encrypted under the old key version stay encrypted under it until something actually rewrites them,
gsutil rewrite -k is that something, applied in bulk across a whole bucket or prefix rather than one
object at a time. notification create predates Cloud Storage's newer Eventarc-based triggers (page 05)
and is still the direct path for a plain Pub/Sub notification on object changes without going through
Eventarc's extra routing layer at all.
Parallel operations: the -m flag#
gsutil -m cp -r ./local-dir gs://my-bucket/path/
gsutil -m rsync -r ./local-dir gs://my-bucket/path/-m is a top-level gsutil flag (comes before the subcommand, not after), enabling multi-threaded/
multi-process transfers, the single biggest lever for transfer speed on a large batch of files, and one
gcloud storage actually replicates with its own equivalent parallelism by default, without needing an
explicit flag, one of the concrete reasons gcloud storage is faster out of the box for the same operation.
Composing objects server-side#
gsutil compose gs://my-bucket/part1 gs://my-bucket/part2 gs://my-bucket/part3 gs://my-bucket/combinedcompose concatenates existing objects into a new one entirely server-side, with no data round-tripping
through the local machine, the mechanism underneath parallel composite uploads (page 03's large-file
upload path splits a file into parts, uploads each concurrently, then composes them), and occasionally
useful directly for stitching together log shards or chunked export files without downloading anything.
Performance diagnostics: perfdiag#
gsutil perfdiag -n 20 -s 10M gs://my-bucket
gsutil perfdiag -o results.json -t write,read,metadata gs://my-bucketperfdiag runs a structured suite of upload/download/metadata-latency tests against a real bucket and
reports throughput and latency numbers, genuinely useful evidence when a team suspects a transfer speed
problem is network/region-related rather than caused by application code, -o saves full results to share
with Google Cloud Support if a ticket is warranted, they specifically ask for this file's format.
Illustrative output (exact numbers vary by network path and region, this is a representative shape, not a captured run):
Operation Bytes Count Avg Time Throughput
--------- ----- ----- -------- ----------
write 10.0M 20 0.412s 24.3 MB/s
read 10.0M 20 0.187s 53.5 MB/s
metadata - 20 0.041s -
TCP connect times: min=0.021s max=0.089s avg=0.043s
A large gap between write and read throughput, or an unusually high TCP connect time, is the signal
worth escalating, a healthy bucket in the same region as the test client typically shows read throughput
comfortably higher than write, since reads can be served from more replicas than a write has to confirm
against.
Migrating a script from gsutil to gcloud storage#
| gsutil | gcloud storage equivalent |
|---|---|
gsutil cp src dst | gcloud storage cp src dst |
gsutil -m rsync -r src dst | gcloud storage rsync src dst (parallelism is automatic) |
gsutil ls -l gs://bucket | gcloud storage ls -l gs://bucket |
gsutil mb gs://bucket | gcloud storage buckets create gs://bucket |
gsutil rb gs://bucket | gcloud storage buckets delete gs://bucket |
gsutil iam ch user:x:objectViewer gs://bucket | gcloud storage buckets add-iam-policy-binding gs://bucket --member=user:x --role=roles/storage.objectViewer |
gsutil signurl key.json gs://bucket/obj | gcloud storage sign-url gs://bucket/obj --private-key-file=key.json |
gsutil rewrite -k, gsutil notification, gsutil du, gsutil hash | no direct gcloud storage equivalent as of this version |
Most day-to-day object/bucket operations map one-to-one, often literally just swapping the binary name and
gsutil's flag style for gcloud storage's, per page 03's own coverage of the modern surface. The bottom
row is the genuine, current reason to keep gsutil installed and reachable rather than treating it as fully
retired.
Note
Google has not announced a retirement date for gsutil as of this writing, it continues to receive
updates, but all new Cloud Storage features are documented against gcloud storage first, and gsutil
sometimes lags behind by a release or two on a brand-new capability. Treat gsutil as stable-but-legacy,
not deprecated-and-unsafe.
Real-world scenario: rotating a KMS key across an entire bucket's existing objects#
A security review requires re-encrypting every object in a bucket after rotating its default KMS key (page
08), since a KMS key rotation alone only affects new writes, not objects already stored:
gcloud storage buckets update gs://my-bucket \
--default-encryption-key=projects/my-project-id/locations/us-central1/keyRings/app-keyring/cryptoKeys/app-encryption-key
gsutil -m rewrite -k -r gs://my-bucket/**The bucket-level default key controls what new objects are encrypted with; rewrite -k is what actually
walks the existing objects and re-encrypts each one under whatever the bucket's current default key is at
the time it runs, -m parallelizes it across a large bucket rather than processing objects one at a time.
Real-world scenario: auditing storage cost by directory prefix before a cleanup#
-
gsutil du -sh gs://my-bucket/*/to see per-prefix totals at a glance, faster than paging throughls -loutput and summing manually for a bucket with thousands of objects - Identify the largest prefixes and confirm with the team whether they're still needed before deleting anything
- Add a lifecycle rule (page
03) for the prefixes that are needed but aging, rather than a one-time manual cleanup that has to be repeated again in six months - Re-run
du -shafter the lifecycle rule has had time to act, confirming it actually reduced the footprint as expected
Real-world scenario: a Terraform provisioner still shelling out to gsutil#
An older Terraform module uses a local-exec provisioner to sync static assets after creating a bucket,
predating the site's current preference (page 03) for handling this in the deploy pipeline instead:
resource "google_storage_bucket" "site" {
name = "my-static-site"
location = "US"
}
resource "null_resource" "sync_assets" {
provisioner "local-exec" {
command = "gsutil -m rsync -r ./dist gs://${google_storage_bucket.site.name}"
}
depends_on = [google_storage_bucket.site]
}Tip
A local-exec provisioner shelling out to either gsutil or gcloud storage runs only on terraform apply from whatever machine executes it, not on every deploy, it's a reasonable stopgap for a one-time
asset seed at bucket creation, but a genuine CI/CD deploy pipeline (this page's own recipe below, or
page 03's) is the better home for anything that needs to run on every release, Terraform state has no
way to know the remote bucket contents changed and won't re-run this step on its own.
CI/CD integration recipe: legacy pipeline still using gsutil, authenticated via WIF#
# .github/workflows/legacy-sync.yml
name: Sync build output (legacy gsutil pipeline)
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/<project-number>/locations/global/workloadIdentityPools/github-pool/providers/github-provider
service_account: ci@my-project-id.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v2
- run: gsutil -m rsync -r ./out gs://my-bucketAn existing pipeline that already works doesn't need to be rewritten onto gcloud storage just for its own
sake, google-github-actions/auth populates the same shared credential store either tool reads, gsutil
authenticates through it exactly like gcloud storage would; migrate the commands opportunistically
(the next time that pipeline needs a real change) rather than as a dedicated, disruptive rewrite effort.
Common pitfalls#
- Placing
-mafter the subcommand instead of before it.gsutil cp -m ...is a syntax error;-mis a top-level flag:gsutil -m cp .... - Assuming a KMS key rotation alone re-encrypts existing objects. It only changes what new writes use;
rewrite -kis required to touch existing ones, see the real-world scenario above. - Hand-editing the
[Credentials]section of.botoinstead of just re-runninggcloud auth login. The credentials section is machine-managed; editing it directly is far more error-prone than re-authenticating throughgcloud. - Writing brand-new scripts against
gsutilin 2026.gcloud storage(page03) is the current recommended default; reach forgsutilspecifically for the commands in the migration table with nogcloud storageequivalent yet, not out of habit. - Forgetting
parallel_composite_upload_thresholdchanges upload shape, not just speed. A composite upload creates the final object by composing several component objects server-side; a process that reads raw object metadata expecting a single simple upload can occasionally behave unexpectedly against one.
Exit codes#
0 success, non-zero on any API/validation error or a partial failure during a multi-file -m operation,
a parallelized cp/rsync that fails on some files but succeeds on others still reports a non-zero exit
overall, gsutil's own summary output (not just the exit code) is what shows exactly which files failed.
When to reach for something else#
For everything not in the migration table's bottom row, default to gcloud storage (page 03) instead,
it's faster, shares gcloud's own config and auth directly with no separate .boto file to maintain, and
is the surface Google documents new features against first. For declarative, reviewable bucket
provisioning, prefer Terraform's google_storage_bucket resource over either CLI, consistent with the IaC
guidance on every earlier page.