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
usmulti-region - Any US dual-region that includes
us-central1 - The
us-central1region
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
ImageSourcemust 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 anee.ImageCollectionusing.mosaic(). - No
mask_bands: The top-levelmask_bandsfield is not supported. UsemissingDatain the manifest or ensure your TIFF has an internal nodata value. - No
data_typein tileset: Do not specifydata_typein thetilesetssection; it is automatically detected. - No
dimensionsin source: Do not specifydimensionsin theImageSource. - No
subdataset_prefix/subdataset_suffix. - No
min_tile_area_ratioorskip_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.getto read bucket metadata and location) - Storage Object Viewer (provides
storage.objects.getto read pixel data)
Steps to configure in Google Cloud Console:
- Go to the Cloud Storage Browser.
- Click your bucket name.
- Select the Permissions tab.
- Click GRANT ACCESS.
- Add the email addresses of your collaborators (or a Google Group,
or
allUsersfor public access). - Assign the roles Storage Bucket Viewer and Storage Object Viewer.
- 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": trueas a top-level field alongside"imageManifest"in theprojects.image.importExternalrequest 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 rmoree.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 theBASICview, 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 any429 Too Many Requestsresponses: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 withee.data.startExternalImageIngestion(manifest, allow_overwrite=True)and map it over your list of manifests. Passingallow_overwrite=Trueeven 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.