Verified8 commandsAI-assisted

gsutil: The Legacy Cloud Storage CLI

.md

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#

Diagram

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 object

rewrite -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/combined

compose 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-bucket

perfdiag 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#

gsutilgcloud storage equivalent
gsutil cp src dstgcloud storage cp src dst
gsutil -m rsync -r src dstgcloud storage rsync src dst (parallelism is automatic)
gsutil ls -l gs://bucketgcloud storage ls -l gs://bucket
gsutil mb gs://bucketgcloud storage buckets create gs://bucket
gsutil rb gs://bucketgcloud storage buckets delete gs://bucket
gsutil iam ch user:x:objectViewer gs://bucketgcloud storage buckets add-iam-policy-binding gs://bucket --member=user:x --role=roles/storage.objectViewer
gsutil signurl key.json gs://bucket/objgcloud storage sign-url gs://bucket/obj --private-key-file=key.json
gsutil rewrite -k, gsutil notification, gsutil du, gsutil hashno 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 through ls -l output 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 -sh after 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-bucket

An 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 -m after the subcommand instead of before it. gsutil cp -m ... is a syntax error; -m is 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 -k is required to touch existing ones, see the real-world scenario above.
  • Hand-editing the [Credentials] section of .boto instead of just re-running gcloud auth login. The credentials section is machine-managed; editing it directly is far more error-prone than re-authenticating through gcloud.
  • Writing brand-new scripts against gsutil in 2026. gcloud storage (page 03) is the current recommended default; reach for gsutil specifically for the commands in the migration table with no gcloud storage equivalent yet, not out of habit.
  • Forgetting parallel_composite_upload_threshold changes 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.