Earth Engine supports assets backed by Cloud Optimized GeoTIFFs (COGs) stored in
Google Cloud Storage (GCS).

When you register a COG-backed asset, Earth Engine indexes the spatial and
metadata fields of the image at creation time. This makes the image performant
when used in collections, with performance comparable to standard ingested
assets (see the [Cloud Optimized GeoTIFF (COG) overview](https://developers.google.com/earth-engine/guides/cog_overview)).

This guide covers preparing your files, writing the manifest, submitting the
registration request, configuring permissions, and updating or deleting
registered assets.

## Before you begin

Before registering a COG, you must set up your
[Cloud Storage](https://cloud.google.com/storage) resources correctly.

### Bucket location

For Earth Engine to access your Cloud Storage bucket, the bucket **must** be
located in one of the following data center regions or multi-regions:

- The **`us`** multi-region
- Any **US dual-region** that includes `us-central1`
- The **`us-central1`** region

If your bucket is placed in any other regional data center (such as `us-east4`
or `europe-west1`), Earth Engine will reject registration and read requests.
See
[supported Cloud Storage bucket locations](https://developers.google.com/earth-engine/guides/cog_overview#supported_cloud_storage_bucket_locations).

### Storage class and billing

- We recommend using the **Standard** storage class. Standard storage has no minimum storage duration rules, preventing early deletion billing penalties if you update or replace temporary files.
- While Earth Engine computation loops don't incur Cloud Storage data retrieval surcharges, accessing non-Standard storage tiers from external tools (such as desktop GIS software) incurs costly retrieval fees.
- You must have billing enabled on your Google Cloud project to use Cloud Storage. See the [Cloud Storage quickstart](https://cloud.google.com/storage/docs/quickstart-console) to get started.

## Prepare your COGs

Earth Engine reads COGs directly from Cloud Storage, fetching only the header
and the tiles a computation needs, so the file layout matters. The TIFF
**must** be:

- **Tiled** , with square power-of-2 tile dimensions from 16x16 to 2048x2048. We recommend **512x512** for best performance.
- Arranged so that all Image File Directories (IFDs) are at the beginning.
- Include power-of-2 overviews, if any. Overviews aren't required, but are recommended for images larger than a single tile.
- Use **BAND** or **PIXEL** interleave (see [single-band versus multi-band files](https://developers.google.com/earth-engine/guides/cog_overview#single-band_versus_multi-band_files) for the trade-offs).

To convert an image, use GDAL's `COG` driver, which produces the required
layout (tiling, overviews, and header placement) in one step. Add
`OVERVIEW_RESAMPLING=NEAREST` for categorical data; the default is `CUBIC`. For
compression, overview, and layout recommendations, see
[best practices for preparing external COGs](https://developers.google.com/earth-engine/guides/cog_overview#best_practices_for_preparing_external_cogs).

### GDAL 3.11+ CLI

    gdal raster convert in.tif out.tif \
      -f COG \
      --co BIGTIFF=YES \
      --co BLOCKSIZE=512 \
      --co INTERLEAVE=BAND \
      --co OVERVIEWS=IGNORE_EXISTING \
      --co COMPRESS=ZSTD \
      --co LEVEL=16 \
      --co PREDICTOR=YES \
      --co NUM_THREADS=ALL_CPUS

### Classic CLI

Omit `INTERLEAVE=BAND` on GDAL versions earlier than 3.11.

    gdal_translate in.tif out.tif \
      -of COG \
      -co BIGTIFF=YES \
      -co BLOCKSIZE=512 \
      -co INTERLEAVE=BAND \
      -co OVERVIEWS=IGNORE_EXISTING \
      -co COMPRESS=ZSTD \
      -co LEVEL=16 \
      -co PREDICTOR=YES \
      -co NUM_THREADS=ALL_CPUS

## Construct the manifest

Whether you register a COG using the Earth Engine Python client library, the
`earthengine` command-line tool, or the Earth Engine REST API, you must define
an image upload manifest specifying the asset destination, Cloud Storage URIs,
and optional band metadata. The API and command-line tool refer to COG-backed
assets as *external images* (`ee.data.startExternalImageIngestion()`,
`earthengine upload external_image`, `projects.image.importExternal`).

**Example: Single COG backing all bands**
If your COG contains all bands (for example, RGB), the manifest requires only a
single tileset:

    {
      "name": "projects/my-project/assets/my_cog_asset",
      "tilesets": [
        {
          "id": "default_tileset",
          "sources": [
            {
              "uris": [
                "gs://google-sample-bucket/my_optimized_image.tif"
              ]
            }
          ]
        }
      ]
    }

**Example: Multiple COGs (one per band)**
If you have separate files for different bands (for example, red, green, and
blue in separate files):

    {
      "name": "projects/my-project/assets/my_multi_band_cog",
      "tilesets": [
        {
          "id": "red_tileset",
          "sources": [ { "uris": [ "gs://google-sample-bucket/band_red.tif" ] } ]
        },
        {
          "id": "green_tileset",
          "sources": [ { "uris": [ "gs://google-sample-bucket/band_green.tif" ] } ]
        }
      ],
      "bands": [
        { "id": "R", "tilesetId": "red_tileset", "tilesetBandIndex": 0 },
        { "id": "G", "tilesetId": "green_tileset", "tilesetBandIndex": 0 }
      ]
    }

### Manifest restrictions

Manifests for COG-backed assets have restrictions that don't apply to standard
ingestion manifests:

- **Single bucket:** All URIs in the manifest must reside in the same Cloud Storage bucket.
- **One URI per source:** Each `ImageSource` must contain exactly one URI. You cannot mosaic multiple files together into a single tileset source. To mosaic multiple COG scenes, register each COG as an asset and combine them into an `ee.ImageCollection` using `.mosaic()`.
- **No `mask_bands`:** The top-level `mask_bands` field is not supported. Use `missingData` in the manifest or ensure your TIFF has an internal nodata value.
- **No `data_type` in tileset:** Do not specify `data_type` in the `tilesets` section; it is automatically detected.
- **No `dimensions` in source:** Do not specify `dimensions` in the `ImageSource`.
- **No `subdataset_prefix` / `subdataset_suffix`**.
- **No `min_tile_area_ratio`** or `skip_metadata_read`.

## Submit the registration request

You can submit your manifest to Earth Engine using the Earth Engine Python
client library, the `earthengine` command-line tool, or the Earth Engine REST
API. Choose the method that best fits your workflow.

### Python client library

In a Python script or Jupyter notebook, use the Earth Engine Python client
library (`ee.data`) after initializing your project:

    import ee

    # 1. Authenticate and initialize
    ee.Authenticate()
    ee.Initialize(project='your-project-id')

    # 2. Define the manifest as a Python dictionary
    manifest = {
      "name": "projects/your-project-id/assets/my_cog_asset",
      "tilesets": [
        {
          "id": "default_tileset",
          "sources": [
            {
              "uris": [
                "gs://google-sample-bucket/my_optimized_image.tif"
              ]
            }
          ]
        }
      ]
    }

    # 3. Register the asset
    result = ee.data.startExternalImageIngestion(manifest)
    print(f"Asset registered: {result['name']}")

### Command-line tool

In your terminal, save your manifest to a file named `manifest.json` and run
the `earthengine` upload command:

    # 1. Verify your environment is authenticated
    earthengine authenticate

    # 2. Register the asset
    earthengine --project your-project-id \
      upload external_image \
      --manifest manifest.json

### REST API

For automated workflows without the client library, send an authorized `POST`
request directly to the Earth Engine REST API endpoint
[`projects.image.importExternal`](https://developers.google.com/earth-engine/reference/rest/v1alpha/projects.image/importExternal)
(demonstrated here using Python):

    import json
    from pprint import pprint
    import ee
    from google.auth.transport.requests import AuthorizedSession

    # 1. Authenticate and start an authorized HTTP session
    ee.Authenticate()
    ee_project = 'your-project-id'
    session = AuthorizedSession(
        ee.data.get_persistent_credentials().with_quota_project(ee_project)
    )

    # 2. Define the REST request body wrapping the manifest
    request = {
      'imageManifest': {
        'name': f'projects/{ee_project}/assets/my_cog_asset',
        'tilesets': [
          {
            'id': 'default_tileset',
            'sources': [
              {
                'uris': [
                  'gs://google-sample-bucket/my_optimized_image.tif'
                ]
              }
            ]
          }
        ]
      }
    }

    # 3. Send the POST request
    url = (
        'https://earthengine.googleapis.com/v1alpha/'
        f'projects/{ee_project}/image:importExternal'
    )
    response = session.post(url=url, data=json.dumps(request))
    pprint(json.loads(response.content))

Once submitted, the registration request initiates a lightweight metadata
indexing operation. Because COG registration does not copy pixel data, this
operation typically completes in seconds.

> [!NOTE]
> **Synchronous registration:** Because COG registration is a fast, synchronous API call rather than an asynchronous batch compute job, it does not create a background task in the Code Editor **Tasks** tab or batch task manager. The API call blocks until metadata validation and indexing are complete, after which the asset is immediately available in the Code Editor **Assets** tab or using `ee.data.getAsset()`.

## Configure permissions

Unlike standard Earth Engine assets, the pixel data of a COG remains in Cloud
Storage. When sharing a COG-backed asset with others, **you must grant them
read access to both the Earth Engine asset and the Cloud Storage files.**

### Grant Cloud Storage bucket permissions

Collaborators need permissions to read the files from Cloud Storage.

**Required permissions:**

- `storage.buckets.get` (to retrieve bucket metadata and location)
- `storage.objects.get` (to read the pixel data)

You can grant these permissions using the following standard Cloud Storage
roles:

- **Storage Bucket Viewer** (provides `storage.buckets.get` to read bucket metadata and location)
- **Storage Object Viewer** (provides `storage.objects.get` to read pixel data)

**Steps to configure in Google Cloud Console:**

1. Go to the [Cloud Storage Browser](https://console.cloud.google.com/storage/browser).
2. Click your bucket name.
3. Select the **Permissions** tab.
4. Click **GRANT ACCESS**.
5. Add the email addresses of your collaborators (or a Google Group, or `allUsers` for public access).
6. Assign the roles **Storage Bucket Viewer** and **Storage Object Viewer**.
7. Click **Save**.

### Share the Earth Engine asset

Share the asset itself using the Earth Engine Asset Manager or CLI. See the
[Asset Management
Guide](https://developers.google.com/earth-engine/guides/manage_assets#set_asset_permissions) for details.

## Update, replace, and delete COG-backed assets

A COG-backed asset is a pointer to a specific version of a file in Cloud
Storage, plus metadata (footprint, projection, bands, and overview layout) that
Earth Engine indexed when you registered it. Earth Engine pins the file version
so that this metadata can't drift out of sync with the pixels, which is what
makes updating COG-backed assets different from updating standard assets.

### Object generations

When you register a COG, Earth Engine records the Cloud Storage object's
**generation** (its version number) alongside the URI. You can see the pinned
generation in the asset's metadata; it appears as a `#<generation>` suffix on
the URI:

### Python

    asset = ee.data.getAsset('projects/your-project-id/assets/my_cog_asset')
    print(asset['tilesets'][0]['sources'][0]['uris'])
    # ['gs://google-sample-bucket/my_optimized_image.tif#1750427474244728']

You can also pin a specific version yourself by including the `#<generation>`
suffix in the manifest URI when you register (or re-register) the asset.
Without it, Earth Engine reads the current object and pins whatever generation
it finds. Pinning explicitly is useful for rolling back to an earlier version
that a bucket with object versioning has retained.

If you overwrite the file in Cloud Storage it gets a new generation, and if you
delete it the pinned generation is gone. Either way, reading the asset's pixels
fails, unless
[object versioning](https://cloud.google.com/storage/docs/object-versioning) on
the bucket retains the old generation, in which case Earth Engine continues to
read the *old* pixels. Metadata operations that use the indexed record (such as
`ee.data.getAsset()`, `bandNames()`, and collection filtering) keep working
either way.

### Update an asset after changing its file

To point an existing asset at a new version of its file, **re-register it with
the same asset name and overwrite enabled** . Earth Engine re-reads the GeoTIFF
header, re-indexes the metadata, and pins the new generation, in a single
synchronous call. The asset's existing permissions are preserved, but its
properties, time range, and band names come from the new manifest, so include
anything you previously set with `ee.data.updateAsset()`.

- **Python:** `ee.data.startExternalImageIngestion(manifest, allow_overwrite=True)`
- **Command line:** `earthengine upload external_image --force --manifest manifest.json`
- **REST API:** include `"overwrite": true` as a top-level field alongside `"imageManifest"` in the [`projects.image.importExternal`](https://developers.google.com/earth-engine/reference/rest/v1alpha/projects.image/importExternal) request body.

You don't need to delete the asset first, and deleting it is counterproductive:
a standalone asset loses its permissions, and an image in a collection briefly
disappears from the collection. Note that `ee.data.updateAsset()`
(`projects.assets.patch`) can only change properties, time range, and band
names; it can't change which file an asset points to.

### Independent lifecycles

Earth Engine assets and Cloud Storage objects are managed separately:

- Deleting the Earth Engine asset (`earthengine rm` or `ee.data.deleteAsset()`) removes only the asset record. Your files in Cloud Storage are untouched.
- Deleting the file in Cloud Storage doesn't remove the asset; it still appears in listings and in its collection (see [object generations](https://developers.google.com/earth-engine/guides/cog_registration#object_generations)).

### Work with large collections

There is no batch or recursive delete in the Earth Engine API: each asset must
be deleted individually, and a collection must be empty before it can be
deleted. `earthengine rm -r` handles this for you but issues the deletions one
at a time, so a collection with tens of thousands of images can take hours to
remove.

- **To replace a collection's images, overwrite rather than delete.** Re-register each image by name with overwrite enabled (see above). This needs one request per image instead of two, keeps the collection populated throughout, and avoids deleting and recreating the collection itself, which is where its permissions live. Delete only the images that aren't in the new set.
- **To delete many assets, parallelize from a script.** List the assets with
  `ee.data.listAssets()` (using the `BASIC` view, which returns only names and
  types rather than full metadata for every image) and delete them from a
  small thread pool. Asset requests share project-wide rate limits, so keep
  concurrency modest (about 10 workers) and let the client library's built-in
  retries handle any `429 Too Many Requests` responses:

  ### Python

      import concurrent.futures
      import ee

      ee.Initialize(project='your-project-id')
      collection = 'projects/your-project-id/assets/my_collection'

      listing = ee.data.listAssets({'parent': collection, 'view': 'BASIC'})
      asset_ids = [a['name'] for a in listing['assets']]

      def delete(asset_id):
        try:
          ee.data.deleteAsset(asset_id)
        except ee.EEException as e:
          return f'{asset_id}: {e}'

      with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
        failures = [f for f in executor.map(delete, asset_ids) if f]

      if failures:
        print('\n'.join(failures))
      else:
        ee.data.deleteAsset(collection)

  The collection is deleted only if every image was, so a partial failure
  leaves it in place for a re-run.

  The same pattern applies to registering many images: replace
  `ee.data.deleteAsset(asset_id)` in the helper with
  `ee.data.startExternalImageIngestion(manifest, allow_overwrite=True)` and
  map it over your list of manifests. Passing `allow_overwrite=True` even on
  the first run makes the batch safe to re-run after a partial failure, since
  any asset that was already created is replaced rather than causing an error.

## Use registered assets

Once registered, a COG-backed asset is accessed in Earth Engine scripts just
like any standard ingested image asset:

### JavaScript

    var image = ee.Image('projects/your-project-id/assets/my_cog_asset');

### Python

    image = ee.Image('projects/your-project-id/assets/my_cog_asset')

For answers to common questions about permissions, updating files, and managing
large collections, see the [COG section of the FAQ](https://developers.google.com/earth-engine/faq#cogs).