Register Cloud Optimized GeoTIFF (COG) assets

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).

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 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.

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 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 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.

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 (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.

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.
  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 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 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 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).

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.