Manage hosted images with Workers

Last updated Sep 2, 2026 A binding connects your Worker to external resources on the Developer Platform, like Images, R2 buckets, or KV namespaces.

When managing hosted images, the Images binding lets your Worker upload, list, retrieve, update, and delete hosted images without calling the REST API directly. The hosted namespace exposes storage and management operations. This binding can also be used to optimize hosted images.

Bindings can be configured in the Cloudflare dashboard for your Worker or in the Wrangler configuration file in your project's directory.

Billing

Hosted image operations require a paid Images plan with storage. Calls count against your storage allowances in the same way as if you had used the REST API or the dashboard.

Setup

To bind Images to your Worker, add the following to your Wrangler configuration file:

{
    "images": {
        "binding": "IMAGES", // available in your Worker on env.IMAGES
    },
}
[images]
binding = "IMAGES"

Within your Worker code, you can manage hosted images using the env.IMAGES.hosted namespace.

Methods

The env.IMAGES.hosted namespace lets you upload and list images across your account. To manage a specific image, call .image(imageId) to get a handle, then call a method on it.

.upload(image, options)

Uploads a new image to your account. You can pass image bytes as a stream or an ArrayBuffer. Returns ImageMetadata.

Accepts the following options as an ImageUploadOptions object:

  • id string — A custom ID to assign to the image. If omitted, Cloudflare generates a UUID. Refer to Upload to a custom path.
  • filename string — The filename to associate with the image.
  • requireSignedURLs boolean — Sets whether the image should require a signed URL to view. Defaults to false.
  • metadata Record<string, unknown> — Arbitrary metadata to store alongside the image.
  • creator string — A user-defined identifier for the image creator.
  • encoding 'base64' — Set to base64 if the provided bytes are base64-encoded. The binding will decode them before upload.

.createDirectUpload(options)

Creates a Direct Creator Upload URL that a client can upload an image to directly without exposing an API token. Returns DirectUploadResult.

Accepts the following options as a DirectUploadOptions object:

  • id string optional — A custom ID to assign to the image. If omitted, then Cloudflare automatically generates a UUID. Refer to Upload to a custom path.
  • metadata Record<string, unknown> optional — Arbitrary metadata to store alongside the image once it is uploaded.
  • requireSignedURLs boolean optional — Sets whether the uploaded image should require a signed URL to view. Defaults to false.
  • creator string optional — A user-defined identifier for the image creator.
  • expiresIn number optional — How long the upload URL stays valid, in seconds. Must be between 120 and 21600. Defaults to 1800.

.list(options)

Lists images in your account with pagination. Returns ImageList.

Accepts the following options as an ImageListOptions object:

  • limit number — The maximum number of images to return in a page.
  • cursor string — The continuation token returned by the previous list() call. Omit on the first page.
  • sortOrder 'asc' | 'desc' — The order to sort results in by uploaded timestamp. Defaults to asc.
  • creator string — Filter results to images uploaded with this creator identifier.
  • filter ImageListFilter — Filter results by image properties. Accepts a metadata field to filter by custom metadata.

Filter by custom metadata

When you list images, you can pass filter.metadata to .list() to return images by their custom metadata fields.

Each entry in filter.metadata is a metadata field name, and its value sets the conditions that the field must meet. When you pass more than one entry, an image must match all of them to be returned.

Field names may contain only letters, numbers, underscores, and dots. If a metadata field name contains other characters, such as hyphens or spaces, then you cannot filter on it.

To filter on a nested field, separate the levels with dot notation, for example, { "config.region": "eu-west" } matches { config: { region: "eu-west" } }. Field paths can be up to five levels deep.

Accepts the following operators as an ImageMetadataFilterOperators object:

  • eq string | number | boolean — Matches a field exactly.
  • in Array<string> | Array<number> — Matches a field against any value in an array. An array accepts up to 10 values, and a string value cannot contain the pipe character ( |).
  • gt number — Matches a field that is greater than the value.
  • gte number — Matches a field that is greater than or equal to the value.
  • lt number — Matches a field that is less than the value.
  • lte number — Matches a field that is less than or equal to the value.

A plain value is shorthand for an exact match, so { status: "active" } is equivalent to { status: { eq: "active" } }.

If an entry has multiple conditions, then an image must match every condition to be returned. To match a range, combine two operators on the same entry. For example, { priority: { gte: 2, lte: 5 } } returns images with a priority from 2 to 5.

A single call accepts up to five conditions. Each operator counts as one condition, so a bounded range such as { priority: { gte: 2, lte: 5 } } uses two.

A request that references an unsupported field name, uses an unsupported operator, or exceeds five conditions fails rather than returning unfiltered results.

export default {
    async fetch(request, env) {
        const { images } = await env.IMAGES.hosted.list({
            filter: {
                metadata: {
                    status: "active",
                    priority: { gte: 2, lte: 5 },
                },
            },
        });

return Response.json(images.map((image) => image.id));
    },
};
export default {
    async fetch(request, env) {
        const { images } = await env.IMAGES.hosted.list({
            filter: {
                metadata: {
                    status: "active",
                    priority: { gte: 2, lte: 5 },
                },
            },
        });

return Response.json(images.map((image) => image.id));
    },
};

.image(imageId)

Returns a handle for a single hosted image. The imageId can be the Cloudflare-generated UUID or a custom ID.

The handle itself does not make a network request, so it is cheap to construct.

.image(imageId).details()

Gets the metadata for an image. Returns ImageMetadata or null if no image with the given ID exists.

.image(imageId).bytes()

Gets the raw bytes of an image. Returns ReadableStream<Uint8Array> or null if no image with the given ID exists. This streams the original uploaded file. Pass the image bytes to .input() to optimize before serving, or use the URLs returned in ImageMetadata.variants or the image delivery URL to serve a predefined variant.

.image(imageId).update(options)

Updates the metadata or access controls for an image. All fields are optional; only the specified fields will be changed. Returns ImageMetadata with the updated values.

Accepts the following options as an ImageUpdateOptions object:

  • requireSignedURLs boolean — Whether signed URLs should be required to view the image. Cannot be set to true on an image that was uploaded with a custom ID.
  • metadata Record<string, unknown> — Replacement metadata for the image. This replaces the existing metadata rather than merging into it.
  • creator string — A user-defined identifier for the image creator.

.image(imageId).delete()

Deletes an image. Returns true if the image was deleted or false if no image with the given ID existed.

.image(imageId).signedUrl(options)

Generates a signed image delivery URL for an image that requires signed URLs. Returns a string.

Returning a signed URL lets a browser fetch a private image directly without proxying the bytes through your Worker. The URL is signed on the Cloudflare side, so your Worker never handles the account signing key.

Accepts the following options as an ImageSignedUrlOptions object:

  • variant string — The variant to serve.
  • expiresIn number optional — How long the URL stays valid, in seconds. If omitted, then the URL does not expire.
  • keyName string optional — The name of the signing key to use. Defaults to default.

Examples

Upload an image from a request body

export default {
    async fetch(request, env) {
        if (!request.body) {
            return new Response("Missing body", { status: 400 });
        }

const image = await env.IMAGES.hosted.upload(request.body, {
            filename: "upload.jpg",
            metadata: { source: "worker" },
            requireSignedURLs: false,
        });

return Response.json(image);
    },
};
export default {
    async fetch(request, env) {
        if (!request.body) {
            return new Response("Missing body", { status: 400 });
        }

const image = await env.IMAGES.hosted.upload(request.body, {
            filename: "upload.jpg",
            metadata: { source: "worker" },
            requireSignedURLs: false,
        });

return Response.json(image);
    },
};

Upload a base64-encoded image

Set encoding: "base64" and the binding will decode the body for you before uploading.

```js export default { async fetch(request, env) { if (!request.body) { return new Response("Missing body", { status: 400 }); }

const image = await env.IMAGES.hosted.upload(request.body, { encoding: "base64", filename: "upload.png", });