# Files and uploads on Cloud Run: from disk to a Cloud Storage bucket

> A Cloud Run instance's disk is memory and dies with the instance. How to move uploads to Cloud Storage with signed URLs, serve downloads, move existing files, and what it costs.

Published 2026-10-11 · Updated 2026-10-13 · [By Exodus team](https://exodus.elim.io/about) · Patterns, Data
<https://exodus.elim.io/blog/files-and-uploads-with-cloud-storage>

Files that users upload to a Cloud Run service belong in a Cloud Storage bucket, not on the instance's disk. That disk is memory: other instances never see a file on it, and it is gone when the instance stops. The service signs short-lived URLs and the browser sends the bytes to the bucket. Storage costs $0.02 per GB a month in Belgium; the bill is the downloads.

## In short

- An instance's file system is in memory and does not persist: fifty 10 MB uploads are enough to crash a 512 MiB instance.
- Cloud Run caps an HTTP/1 request at 32 MiB, so uploads go from the browser to the bucket with a signed PUT URL, valid 15 minutes in the sample, and downloads with a signed GET URL of 5 minutes.
- A bucket in `europe-west1` costs $0.02 per GB a month, $0.005 per 1,000 uploads and $0.12 per GB sent to the internet: a medium app pays $1.32 for storage and operations and $12.00 for transfer.
- Soft delete keeps deleted objects for 7 days and bills them at the storage rate; `--soft-delete-duration=0` turns it off.

Prices are Google's list prices for `europe-west1`, read on October 11, 2026, before VAT.

## Why files on local disk break

On a server with a disk, uploads go to a folder such as `./uploads`, which comes up in every move [from a VPS to Cloud Run](https://exodus.elim.io/blog/a-vps-to-cloud-run); on Cloud Run it is one of the [patterns that break](https://exodus.elim.io/blog/patterns-that-break-on-cloud-run). Cloud Run gives each instance a writable file system, but Google's runtime contract says two things about it: it is an in-memory file system, so writing to it uses the instance's memory, and data written to it does not persist when the instance stops. Three habits fail because of that.

- **Other instances never see the file.** Cloud Run starts a second instance when traffic grows, and a deploy starts new ones. An upload saved to `./uploads` on instance A is a 404 when the next request lands on instance B.
- **The file is gone when the instance stops.** A service with no traffic scales to zero. Every file written since the instance started goes with it.
- **The file eats memory.** You cannot set a size limit on that file system, and the contract warns that filling it can use up all the memory and crash the instance. Fifty 10 MB uploads on a 512 MiB instance are enough.

![Two moments of one service. At 10:00 a browser posts an avatar, instance A writes ./uploads/42.png into its own memory. At 10:05 the next request for the file lands on instance B, which never saw it, and gets a 404. At night instance A stops and the file is gone](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-8d2715095d8f56ede2351b954291c1bc7059043e48c91bb0dba27f076b4763fc-680x330-svg/bytes "A file on local disk lives in one instance's memory, and only until that instance stops")

The code that does this is usually a library default: Multer's `dest` (its memory storage can run the app out of memory, its docs warn), Active Storage's Disk service, Django's `FileSystemStorage` under `MEDIA_ROOT`, Laravel's `local` and `public` disks, or `fs.writeFile` into a data folder.

A temporary file inside one request is fine. Resizing an image, unzipping an archive or building a PDF can use `/tmp`, as long as the file fits in memory next to the app and is deleted at the end of the request.

## What Cloud Storage is

Cloud Storage keeps **objects** (files) in **buckets**. A bucket lives in a location, for example `europe-west1` in Belgium, the same region as your service. An object has a name, its bytes and some metadata such as `Content-Type`. There are no real folders: `uploads/42.png` is one name with a slash in it, and tools show the slashes as folders.

Every instance reads the same bucket, the files outlive any instance, and an object can be up to 5 TiB. Google allows one write per second to the same object name and no limit across names, so give each upload its own random name.

Access is controlled by IAM, Google Cloud's permission system. **Uniform bucket-level access** turns off per-object permission lists (ACLs), and **public access prevention** stops anyone making objects public by mistake. Both are set in Step 2.

## Three ways to use a bucket from Cloud Run

**Signed URLs: the browser talks to the bucket.** Your service checks the user and signs a URL that allows one PUT (upload) or one GET (download) of one object for a few minutes. The bytes go between the browser and Google; your service handles two small JSON requests. This is the default for user uploads and downloads, and the rest of the article builds it.

![Sequence of eight steps between the browser, your service and the bucket: request an upload URL, get a key and signed PUT URL, PUT the file straight to the bucket, report done, the service reads size and type and saves the key, request a file, get a 302 to a signed GET URL, download straight from the bucket](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-c8039bf014f5a15c95df6ba9049e7489661b9fe05bf6f5ad3c5e3d60166dbe24-680x440-svg/bytes "Only small JSON passes through your service; the file goes between the browser and the bucket")

**Through your service with the client library.** The browser posts the file to your service, which streams it to the bucket with `file.createWriteStream()` or the like. Every byte passes through your instance, the request lasts as long as the user's upload, and Cloud Run caps an HTTP/1 request at 32 MiB (no limit with an HTTP/2 server). Use it for small files the server creates, such as a generated invoice.

**A Cloud Storage volume mount.** `gcloud run services update --add-volume type=cloud-storage` mounts a bucket as a folder through Cloud Storage FUSE, so old code that reads and writes paths keeps working. Google lists the catch: FUSE is not a POSIX file system, has no file locking, and "when multiple writes try to replace a file, the last write wins and all previous writes are lost." It fits read-mostly data such as templates or the model of a [GPU service on Cloud Run](https://exodus.elim.io/blog/gpus-on-cloud-run), not user uploads.

For scratch space, not storage, an **in-memory volume** with a size limit fails a write past the limit instead of killing the instance, and an **ephemeral disk** (Preview on Oct 11, 2026) gives each instance a disk of 1 to 100 GiB, deleted when it stops.

## What it costs

Cloud Storage bills four things. The prices are list prices for a bucket in `europe-west1`, from Google's pricing page and the Cloud Billing Catalog API, read Oct 11, 2026, before VAT:

- **Storage:** $0.02 per GB a month for the Standard class.
- **Class A operations** (uploads, listings): $0.005 per 1,000.
- **Class B operations** (reads of an object or its metadata): $0.0004 per 1,000.
- **Data sent to the internet:** $0.12 per GB to most of the world up to 10 TiB a month, then less. Uploads into the bucket are free.

The Always Free tier (5 GB-months, 5,000 Class A and 50,000 Class B operations, 100 GB of transfer) applies to US regions only, so a Belgian bucket pays from the first byte.

Three cases, each with a month of files:

- **Small:** 2 GB stored, 2,000 uploads, 20,000 downloads, 10 GB sent. Storage and writes come to about $0.05 a month; reads bring it to $0.06, and the 10 GB sent is $1.20.
- **Medium:** 50 GB stored, 20,000 uploads, 500,000 downloads, 100 GB sent: $1.32 for storage and operations, $12.00 for transfer.
- **Large:** 500 GB stored, 200,000 uploads, 5 million downloads, 1 TiB sent: $13.23 and $122.88.

![Bar chart in US dollars a month for three apps. Small: storage and operations $0.06, downloads $1.20. Medium: $1.32 and $12.00. Large: $13.23 and $122.88](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-c889f32746401f0ed1eeb2470e49497fa4c19a2dd92a562cef73f9cea3ae632e-680x360-svg/bytes "Storage is cheap; the bill is the downloads. europe-west1, list prices of 2026-10-11 before VAT")

The bytes sent dominate, so that is the number to estimate. Sending a file through your Cloud Run service costs about the same ($0.105 per GiB to users in Europe) plus the instance time, so it saves little. [Egress on Google Cloud explained](https://exodus.elim.io/blog/egress-on-google-cloud-explained) covers the rates and how to lower them.

The storage figures include soft delete. A new bucket keeps deleted objects for 7 days, and they keep billing at the storage rate the whole time. Google's blog gives the formula, 7 / 30.4375 of a month per deleted GB: the large case deletes 50 GB a month, which adds $0.23. For a bucket of short-lived files, such as exports deleted after an hour, the 7 days can cost more than the live data. Set `--soft-delete-duration=0` on such buckets.

The shared disk you might reach for instead, Filestore over NFS, starts at 1 TiB on the Basic HDD tier and bills what you provision: $184.32 a month in Belgium.

## Step by step

The examples use a project `PROJECT_ID`, the region `europe-west1`, a service `shop` that runs as its own service account `shop-api` (its identity), and a bucket `shop-uploads`. Every gcloud flag here was checked against gcloud 587.0.0.

### Step 1: find the code that writes files

```shell
grep -rnE "multer|diskStorage|createWriteStream|writeFile|uploads/" --include='*.[jt]s' --include='*.mjs' . | grep -v node_modules
grep -rnE "MEDIA_ROOT|FileSystemStorage|DEFAULT_FILE_STORAGE|STORAGES" --include='*.py' .
grep -rnE "service: Disk|active_storage.service|:local" config/
grep -rnE "Storage::disk|'local'|'public'" --include='*.php' app config
```

For each hit, write down what the file is, who may read it, how big it gets, and whether the database stores its path. Files the app can rebuild may not need a bucket.

### Step 2: create the bucket

```shell
gcloud storage buckets create gs://shop-uploads --project PROJECT_ID \
  --location europe-west1 --default-storage-class STANDARD \
  --uniform-bucket-level-access --public-access-prevention
```

Bucket names are global, so `shop-uploads` may be taken; add your project name. gcloud's default for uniform access is off, so pass the flag. Soft delete is on with 7 days unless `--soft-delete-duration` says otherwise (`0` turns it off).

Here is one of our buckets, private and IAM-only, where nobody set soft delete:

![Terminal: gcloud storage buckets describe with a yaml format prints default_storage_class STANDARD, location EUROPE-WEST1, public_access_prevention enforced, a soft delete policy with retentionDurationSeconds 604800, and uniform_bucket_level_access true](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-18d0b77d7b022da010224a6f740a1e6537e0fc8b857ec1ec84227c043ca0c39f-680x300-svg/bytes "A bucket's settings: private, IAM only, and 7 days of soft delete nobody asked for")

### Step 3: give the service permission to use the bucket and to sign

```shell
gcloud services enable iamcredentials.googleapis.com --project PROJECT_ID

gcloud storage buckets add-iam-policy-binding gs://shop-uploads \
  --member serviceAccount:shop-api@PROJECT_ID.iam.gserviceaccount.com \
  --role roles/storage.objectUser

gcloud iam service-accounts add-iam-policy-binding shop-api@PROJECT_ID.iam.gserviceaccount.com \
  --member serviceAccount:shop-api@PROJECT_ID.iam.gserviceaccount.com \
  --role roles/iam.serviceAccountTokenCreator --project PROJECT_ID
```

The service account comes from `gcloud iam service-accounts create shop-api`. The first role lets it create, read and delete objects in this bucket only; `roles/storage.objectCreator` alone cannot read or delete, and the code needs both.

The second grant is the one that gets missed. A signed URL needs a private key, and on Cloud Run there is no key file, so the client library asks the IAM Credentials API to sign with Google's key for the account (`signBlob`). For that the account must hold Service Account Token Creator on itself and the API must be on; without them, signing fails while every other bucket call works. django-storages notes that `signBlob` has a quota.

Then deploy the service as that account, with the bucket in an environment variable:

```shell
gcloud run deploy shop --source . --region europe-west1 \
  --service-account shop-api@PROJECT_ID.iam.gserviceaccount.com \
  --set-env-vars BUCKET=shop-uploads
```

### Step 4: allow the browser to PUT to the bucket

A PUT from `https://shop.example.com` to `storage.googleapis.com` is a cross-origin request, which browsers block unless the target agrees, so the bucket needs a CORS rule in `cors.json`:

```json
[
  {
    "origin": ["https://shop.example.com"],
    "method": ["PUT", "GET"],
    "responseHeader": ["Content-Type", "x-goog-content-length-range"],
    "maxAgeSeconds": 3600
  }
]
```

```shell
gcloud storage buckets update gs://shop-uploads --cors-file=cors.json
```

On a preflight (the browser's check request before the PUT), Cloud Storage checks the headers it announces against `responseHeader`; a missing header means no CORS answer, and the browser blocks the upload. Add every origin the app is served from, including staging.

### Step 5: sign uploads in the service

The service below has three routes. `POST /uploads` checks the type and declared size, picks a random object name and signs a PUT URL for 15 minutes, with the content type and a size range inside the signature. `POST /uploads/done` reads the object's real size and type, deletes it if they are wrong, and stores the key. `GET /files/ID` redirects to a signed GET URL for 5 minutes. It uses Node's `http` module so it runs as is.

```ts
// Uploads straight to Cloud Storage: the service signs, the browser sends the bytes.
import http from 'node:http'
import { randomUUID } from 'node:crypto'
import { Storage } from '@google-cloud/storage'

const { BUCKET, PORT = 8080 } = process.env
const bucket = new Storage().bucket(BUCKET)
const MAX = 10 * 1024 * 1024 // 10 MiB
const TYPES = new Set(['image/jpeg', 'image/png', 'application/pdf'])
const files = new Map() // stands in for your database table

const json = (res, status, body) =>
  res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify(body))
const readJson = async (req) => {
  let s = ''
  for await (const chunk of req) s += chunk
  return JSON.parse(s || '{}')
}

http.createServer(async (req, res) => {
  try {
    // 1. The browser asks for an upload URL. Check the user and the file here.
    if (req.method === 'POST' && req.url === '/uploads') {
      const { type, size } = await readJson(req)
      if (!TYPES.has(type) || !(size > 0 && size <= MAX)) return json(res, 400, { error: 'type or size' })
      const key = `uploads/${randomUUID()}`
      const headers = { 'content-type': type, 'x-goog-content-length-range': `0,${MAX}` }
      const [url] = await bucket.file(key).getSignedUrl({
        version: 'v4', action: 'write', expires: Date.now() + 15 * 60 * 1000,
        contentType: type, extensionHeaders: { 'x-goog-content-length-range': `0,${MAX}` },
      })
      return json(res, 200, { key, url, headers })
    }
    // 2. The browser PUT the file to `url` with `headers`, then says it is done.
    if (req.method === 'POST' && req.url === '/uploads/done') {
      const { key } = await readJson(req)
      if (!/^uploads\/[0-9a-f-]{36}$/.test(key ?? '')) return json(res, 400, { error: 'key' })
      const [meta] = await bucket.file(key).getMetadata()
      if (Number(meta.size) > MAX || !TYPES.has(meta.contentType)) {
        await bucket.file(key).delete()
        return json(res, 400, { error: 'rejected' })
      }
      const id = key.slice(8)
      files.set(id, { key, type: meta.contentType, size: Number(meta.size) })
      return json(res, 200, { id, size: Number(meta.size) })
    }
    // 3. Downloads: check access, then redirect to a short-lived signed GET URL.
    const m = /^\/files\/([0-9a-f-]{36})$/.exec(req.url)
    if (req.method === 'GET' && m && files.has(m[1])) {
      const [url] = await bucket.file(files.get(m[1]).key).getSignedUrl({
        version: 'v4', action: 'read', expires: Date.now() + 5 * 60 * 1000,
        responseDisposition: 'attachment',
      })
      return res.writeHead(302, { location: url }).end()
    }
    json(res, 404, { error: 'not found' })
  } catch (e) {
    console.error(e)
    json(res, e.code === 404 ? 404 : 500, { error: 'failed' })
  }
}).listen(PORT, () => console.log(`uploads on :${PORT}`))
```

In the browser, the upload is two requests and a confirmation:

```ts
const r = await fetch('/uploads', { method: 'POST', body: JSON.stringify({ type: file.type, size: file.size }) })
const { key, url, headers } = await r.json()
await fetch(url, { method: 'PUT', headers, body: file })
await fetch('/uploads/done', { method: 'POST', body: JSON.stringify({ key }) })
```

The size check works twice. A signed URL lists the headers that every request using it must carry (`X-Goog-SignedHeaders`), so the browser has to send `x-goog-content-length-range: 0,10485760`, and Cloud Storage answers 400 to a PUT whose body falls outside that range. If someone strips the header, the signature no longer matches and the PUT fails. The check in `/uploads/done` catches anything else, and is where you would check an image's real format.

We ran this service against fake-gcs-server, an open-source Cloud Storage emulator, with real V4 signing from a throwaway key. The emulator checks neither signatures nor sizes, so the last PUT got through to step 2:

![Terminal: a zip upload is refused with type or size; a png gets a key and a signed URL with X-Goog-Expires=900 and signed headers content-type, host and x-goog-content-length-range; the PUT returns 200; done returns the id and size 2048; GET /files/ID returns 302 to a signed URL with response-content-disposition=attachment; an 11 MiB PUT without the size header is rejected at done](https://exodus.elim.io/media/v2026-09-16/t/exodus/assets/image-2376b64e948a917d0ccd0e23969d23d1b4b167558ee43bbd26e6f37ec03a9caa-680x400-svg/bytes "The sample's upload flow end to end, against an emulator with real V4 signing")

For large files on slow connections, a resumable upload continues after a dropped connection. Google suggests the server start the session (a POST with `x-goog-resumable: start`) and hand the session URI to the browser, where it works as a credential like a signed URL.

### Step 6: downloads

With `GET /files/ID` your code decides who may read a file and the bytes come from the bucket; `attachment` makes the browser save it instead of rendering it. Serve user files from the bucket's own address and never from your domain, so an uploaded HTML or SVG file cannot run script as your site.

Files that everyone may see, such as product photos, can live in a second, public bucket: create it without `--public-access-prevention` and grant `roles/storage.objectViewer` to `allUsers`. With prevention enforced, that grant fails with `412 Precondition Failed`, which is why user uploads stay in the other bucket.

### Step 7: move the files you have

```shell
gcloud storage rsync ./uploads gs://shop-uploads/uploads --recursive --dry-run
gcloud storage rsync ./uploads gs://shop-uploads/uploads --recursive
```

Run it a day before the switch and again during it; the second run copies only what changed. Object names match the old paths, so database rows that store `uploads/42.png` still point at the right file. If your files live in Amazon S3, as in a [move from AWS to Cloud Run](https://exodus.elim.io/blog/from-aws-to-cloud-run), gcloud reads `s3://` URLs once your AWS keys are in `~/.aws/credentials`: `gcloud storage rsync s3://old-bucket gs://shop-uploads --recursive`. For millions of objects, Storage Transfer Service runs it as a managed job, free on Google's side; AWS bills its egress.

Code that built `public/uploads/...` URLs now builds `/files/ID` or the public bucket's address.

### Step 8: work after an upload

Thumbnails and virus scans should not run while the user waits. Eventarc, Google's event router, can deliver Cloud Storage's "object finalized" event to a Cloud Run service:

```shell
gcloud eventarc triggers create shop-uploads-finalized --location europe-west1 \
  --destination-run-service shop-worker --destination-run-region europe-west1 \
  --destination-run-path /events/upload \
  --event-filters type=google.cloud.storage.object.v1.finalized \
  --event-filters bucket=shop-uploads \
  --service-account shop-events@PROJECT_ID.iam.gserviceaccount.com
```

Eventarc charges $0 for events from Google sources; the Pub/Sub transport underneath bills cents for most apps. Google's guide lists the roles the trigger needs. For work you start yourself, see [background workers with Cloud Tasks](https://exodus.elim.io/blog/background-workers-with-cloud-tasks).

## How your framework maps

- **Express with Multer:** replace `dest` and memory storage with the signed URL flow above. For small server-side files, stream into `bucket.file(key).createWriteStream()`.
- **Rails Active Storage:** declare a `GCS` service in `config/storage.yml` and turn on direct uploads; the Rails guide lists the CORS headers it sends (`Content-Type`, `Content-MD5`, `Content-Disposition`).
- **Django:** django-storages' `GoogleCloudStorage` backend with `GS_BUCKET_NAME`, and `GS_IAM_SIGN_BLOB = True` so URLs are signed through `signBlob` instead of a key file.
- **Laravel:** the framework has no Google driver; its `s3` driver can talk to Cloud Storage's XML API with HMAC keys (Google's "simple migration"). Test temporary URLs on a staging bucket first.

## What breaks

- **The browser says CORS.** The bucket has no rule for your origin, or a header the upload sends is missing from `responseHeader`.
- **403 SignatureDoesNotMatch.** The PUT's `Content-Type` or another signed header differs from what was signed. Send exactly the `headers` the service returned.
- **Signing fails on Cloud Run only.** Your laptop signs with a key file; Cloud Run needs Token Creator and the IAM Credentials API (Step 3).
- **A leaked URL.** Anyone who has a signed URL can use it until it expires (V4 allows up to 7 days; `gcloud storage sign-url` caps at 12 hours with Google's key), so keep download URLs to minutes.
- **The instance restarts under load.** Something still buffers whole files in memory: Multer memory storage, a FUSE mount, `/tmp`.
- **Files never confirmed.** A user who closes the tab after the PUT leaves an object with no database row. A nightly job that deletes `uploads/` objects older than a day with no row fixes it; see [cron and timers with Cloud Scheduler](https://exodus.elim.io/blog/cron-and-timers-with-cloud-scheduler).

## Where Exodus comes in

When Exodus audits a repository with your coding agent, writes to local disk are one of the things it looks for: Multer's `dest`, an Active Storage `local` service, `MEDIA_ROOT`, a `public/uploads` folder in the repository. Each one shows up in the report as a change to make before the move, with the bucket, the permissions and the upload flow above as the plan, and the bucket's storage and transfer counted in the monthly estimate.

## FAQ

### Does Cloud Run have a persistent disk for uploads?

No. Cloud Run's file system lives in the instance's memory and is lost when the instance stops, and other instances never see what one instance wrote. User files go to a Cloud Storage bucket. An ephemeral disk of 1 to 100 GiB, in Preview, is scratch space that is deleted with the instance.

### How large a file can I upload to Cloud Run?

Cloud Run caps an HTTP/1 request at 32 MiB, with no limit for an HTTP/2 server. A signed URL avoids the cap: the browser sends the file straight to Cloud Storage, where one object can be up to 5 TiB, and the size range in the signature limits what a user may send.

### Can I mount a Cloud Storage bucket as a folder on Cloud Run?

Yes, a Cloud Storage volume mount uses Cloud Storage FUSE. It is not a POSIX file system: it has no file locking, and when two writes replace the same file, the last one wins. It suits read-mostly data such as templates or a model file, not user uploads.

### Why does signing a URL fail on Cloud Run but work on my laptop?

Your laptop signs with a key file; Cloud Run has none, so the client library asks the IAM Credentials API to sign. The service account needs Service Account Token Creator on itself, and the IAM Credentials API must be on.

### How much does Cloud Storage cost for a small app?

In europe-west1, 2 GB stored, 2,000 uploads and 20,000 downloads cost about $0.06 a month. Sending 10 GB to users adds $1.20. Uploads into the bucket are free, and the Always Free tier applies to US regions only.

## Sources

1. [Container runtime contract, Google (the file system is in memory and does not persist; updated Oct 9, 2026)](https://docs.cloud.google.com/run/docs/container-contract)
2. [Cloud Run quotas and limits, Google (32 MiB HTTP/1 request and response, 32 GiB memory; read Oct 11, 2026)](https://docs.cloud.google.com/run/quotas)
3. [Configure in-memory volume mounts for services, Google (size limit, half of memory by default; read Oct 11, 2026)](https://docs.cloud.google.com/run/docs/configuring/services/in-memory-volume-mounts)
4. [Configure an ephemeral disk for Cloud Run services, Google (Preview; 1 to 100 GiB, 10 GiB per instance by default; read Oct 11, 2026)](https://docs.cloud.google.com/run/docs/configuring/services/ephemeral-disk)
5. [Configure Cloud Storage volume mounts for services, Google (Cloud Storage FUSE, no file locking, last write wins; read Oct 11, 2026)](https://docs.cloud.google.com/run/docs/configuring/services/cloud-storage-volume-mounts)
6. [Cloud Storage FUSE overview, Google (not POSIX: no hard links, no locking, no patching; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/cloud-storage-fuse/overview)
7. [Cloud Storage pricing, Google (Standard $0.02/GB-month in Belgium, Class A $0.005 and Class B $0.0004 per 1,000, internet transfer $0.12/GB up to 10 TiB, Always Free in US regions only; read Oct 11, 2026)](https://cloud.google.com/storage/pricing)
8. [Cloud Storage pricing examples, Google (read Oct 11, 2026)](https://cloud.google.com/storage/pricing-examples)
9. [Cloud Storage quotas and limits, Google (5 TiB per object, one write per second to the same name; read Oct 11, 2026)](https://docs.cloud.google.com/storage/quotas)
10. [Soft delete overview, Google (on by default, 7 days, deleted objects keep billing; updated Oct 9, 2026)](https://docs.cloud.google.com/storage/docs/soft-delete)
11. [Managing Cloud Storage soft delete at scale, Google Cloud blog (the 7 / 30.4375 cost formula; read Oct 11, 2026)](https://cloud.google.com/blog/products/storage-data-transfer/understanding-cloud-storages-new-soft-delete-feature)
12. [Public access prevention, Google (adding allUsers fails with 412 when enforced; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/public-access-prevention)
13. [Uniform bucket-level access, Google (IAM only, no object ACLs; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/uniform-bucket-level-access)
14. [Signed URLs, Google (anyone with the URL can use it; 7 days at most; XML API only; signed headers; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/access-control/signed-urls)
15. [V4 signing process with Cloud Storage tools, Google (roles, Service Account Token Creator, signBlob; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/access-control/signing-urls-with-helpers)
16. [gcloud storage sign-url reference, Google (12 hours at most with a system-managed key; read Oct 11, 2026)](https://docs.cloud.google.com/sdk/gcloud/reference/storage/sign-url)
17. [IAM Service Account Credentials API, Google (signBlob lives at iamcredentials.googleapis.com; read Oct 11, 2026)](https://docs.cloud.google.com/iam/docs/reference/credentials/rest)
18. [XML API headers, Google (x-goog-content-length-range: a PUT outside the range gets 400; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/xml-api/reference-headers)
19. [Cross-origin resource sharing, Google (preflight headers checked against the bucket's CORS; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/cross-origin)
20. [Set up and view CORS configurations, Google (read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/using-cors)
21. [Signed URLs and resumable uploads, Google (the session URI acts as the credential; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/xml-api/post-object-resumable)
22. [gcloud storage rsync reference, Google (read Oct 11, 2026)](https://docs.cloud.google.com/sdk/gcloud/reference/storage/rsync)
23. [Interoperability with other storage providers, Google (gcloud storage rsync from s3://; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/interoperability)
24. [Storage Transfer Service pricing, Google (agentless transfers: no charge; read Oct 11, 2026)](https://cloud.google.com/storage-transfer/pricing)
25. [Route Cloud Storage events to Cloud Run, Google (google.cloud.storage.object.v1.finalized; read Oct 11, 2026)](https://docs.cloud.google.com/eventarc/standard/docs/run/route-trigger-cloud-storage)
26. [Eventarc pricing, Google (events from Google sources $0, Pub/Sub transport billed; read Oct 11, 2026)](https://cloud.google.com/eventarc/pricing)
27. [Filestore service tiers, Google (Basic HDD from 1 TiB; read Oct 11, 2026)](https://docs.cloud.google.com/filestore/docs/service-tiers)
28. [Filestore pricing, Google (billed on provisioned capacity; Basic HDD $0.18/GiB-month in Belgium per the Catalog API, Oct 11, 2026)](https://cloud.google.com/filestore/pricing)
29. [Network pricing, Google (Premium Tier internet transfer $0.12/GiB to Europe and North America, 1 GiB to 1 TiB; Cloud Run uses it; read Oct 11, 2026)](https://cloud.google.com/vpc/network-pricing)
30. [Simple migration from Amazon S3 to Cloud Storage, Google (XML API with HMAC keys; read Oct 11, 2026)](https://docs.cloud.google.com/storage/docs/aws-simple-migration)
31. [File Storage, Laravel docs (temporary upload URLs on the s3 and local drivers only; read Oct 11, 2026)](https://laravel.com/docs/12.x/filesystem)
32. [Multer, Express (dest writes to disk; memory storage keeps whole files in memory; read Oct 11, 2026)](https://expressjs.com/en/resources/middleware/multer/)
33. [Active Storage overview, Rails Guides (GCS service, direct uploads, CORS headers; read Oct 11, 2026)](https://guides.rubyonrails.org/active_storage_overview.html)
34. [django-storages, Google Cloud Storage backend (GS_IAM_SIGN_BLOB; read Oct 11, 2026)](https://django-storages.readthedocs.io/en/latest/backends/gcloud.html)
35. Exodus team: Cloud Billing Catalog API prices, gcloud 587.0.0 help, a bucket's settings, and the sample upload service run against fake-gcs-server, Oct 11, 2026
