AI image API cache isolation: safe reuse across tenants
Two customers submit the same product-photo prompt, but each uploads a different unreleased product. A cache indexed by prompt text can return the first customer's image to the second. AI image API cache isolation prevents this class of application error by treating identity, input content, generation settings, and access permission as separate requirements for reuse.
This tutorial builds a local cache-key and authorization prototype for a GenRelay integration. A cache is stored data reused instead of recomputing a result. A tenant is a customer or organization whose assets must remain separated from other customers. The examples run with Python's standard library, make no generation requests, and do not describe a built-in GenRelay caching feature.
When should an application reuse a generated image?
Reuse an image when the caller explicitly accepts a previous approved result and still has permission to access its source assets and output.
Identical prompts do not necessarily express identical user intent. Clicking “generate another variation” asks for a new sample, while reopening a saved preview usually asks for the existing artifact. Put that distinction in your application contract before calculating a cache key. An optimization that silently returns the previous image can make a variation button appear broken.
Use three explicit modes:
- Reuse approved: look for an authorized, previously accepted image matching the request.
- Generate fresh: create a new generation attempt without consulting the result cache.
- Resume existing: retrieve a known job or artifact by its application identifier.
Request deduplication prevents accidental duplicate work for one operation; result caching reuses outputs across operations. They solve different problems. See the image request deduplication guide for handling repeated submissions without confusing them with creative variations.
What belongs in an image generation cache key?
Include tenant scope, input content hashes, effective model settings, and a versioned application policy in the cache identity.
A content hash is a fixed-length digest computed from bytes. Hash the exact reference bytes your application submits, after any resizing or format conversion. Do not hash an expiring download URL: the URL can change while the image stays identical, or remain stable while its contents change. Preserve reference order when it affects generation semantics.
A canonical representation is a deterministic serialization of a structured request. Sorting JSON object keys avoids false misses caused by dictionary insertion order. Do not normalize away prompt punctuation, whitespace, or letter case unless your product deliberately defines those changes as irrelevant.
| Key component | Reason to include it |
|---|---|
| Tenant identifier | Separate customer asset namespaces |
| Model and application model revision | Avoid reusing an output after a routing change |
| Effective resolution and other settings | Distinguish different requested outputs |
| Prompt and ordered reference digests | Represent actual generation inputs |
| Policy version | Invalidate reuse when acceptance rules change |
| Cache schema version | Allow safe key-format migrations |
If a provider exposes no immutable model revision, do not invent one. Maintain an application routing version and use a conservative expiry policy. Record the submitted model identifier independently for auditing.
The following complete example uses an application request format, not an API payload. In production, validate model-specific settings before passing the resulting effective configuration into this function.
import hashlib
import json
def digest(data):
return hashlib.sha256(data).hexdigest()
def cache_key(tenant, request, references, policy_version):
if not tenant or not policy_version:
raise ValueError("tenant and policy version are required")
identity = {
"schema": 1,
"tenant": tenant,
"policy": policy_version,
"request": request,
"references": [digest(data) for data in references],
}
encoded = json.dumps(
identity, sort_keys=True, separators=(",", ":"),
ensure_ascii=False, allow_nan=False,
).encode("utf-8")
return digest(encoded)
request = {
"model": "nano-banana-pro",
"routing_version": "catalog-v1",
"prompt": "A studio product image with soft side lighting",
"settings": {"resolution": "1K"},
}
key = cache_key("tenant-a", request, [b"reference-a"], "review-v1")
assert key == cache_key(
"tenant-a", dict(reversed(list(request.items()))),
[b"reference-a"], "review-v1",
)
assert key != cache_key(
"tenant-b", request, [b"reference-a"], "review-v1",
)
assert key != cache_key(
"tenant-a", request, [b"reference-b"], "review-v1",
)
print("cache identity checks passed")
The byte strings are test fixtures, not image files. Use real submitted bytes in the integration. Restrict request values to validated JSON types; inconsistent representations such as a numeric setting supplied sometimes as a string create unnecessary misses.
How do you authorize a cache hit before returning an asset?
Check ownership, current permissions, acceptance status, expiry, and object availability before issuing a download response.
A matching digest is not an authorization credential. Never expose a storage object just because a caller knows its cache key. Resolve the current tenant from authenticated server-side context, rather than trusting a tenant field sent by the browser.
A signed URL is a temporary credential granting access to an object. Store a private object identifier in the cache, then mint a short-lived signed URL after authorization. Reusing an old signed URL can return an expired link or keep an asset accessible longer than intended.
This second standalone example models a conservative decision gate. The timestamp and Boolean inputs come from trusted server checks, not client assertions.
from dataclasses import dataclass
@dataclass(frozen=True)
class Entry:
tenant: str
object_id: str
expires_at: int
approved: bool
policy_version: str
def reusable(entry, tenant, now, policy_version,
source_access, output_access, object_exists):
return (
entry.tenant == tenant
and now < entry.expires_at
and entry.approved
and entry.policy_version == policy_version
and source_access
and output_access
and object_exists
)
entry = Entry("tenant-a", "private/asset-42", 2000, True, "review-v1")
args = dict(now=1000, policy_version="review-v1", source_access=True,
output_access=True, object_exists=True)
assert reusable(entry, "tenant-a", **args)
assert not reusable(entry, "tenant-b", **args)
assert not reusable(entry, "tenant-a", **{**args, "source_access": False})
assert not reusable(entry, "tenant-a", **{**args, "now": 2000})
print("cache authorization checks passed")
This gate is not a complete storage service. Production code needs transactional permission checks or version checks that address permission changes between authorization and delivery. Keep URL lifetimes short and document that already downloaded files cannot be recalled by removing a cache row.
How should cache writes, invalidation, and deletion work?
Publish cache entries after validation and approval, then invalidate them when their inputs, permissions, or acceptance policy change.
Use a uniqueness constraint on the tenant-scoped cache key. If two workers finish the same request simultaneously, choose one approved output for future reuse while preserving both attempts in the generation and billing records. Caching does not automatically prevent simultaneous paid submissions; coordinate job admission separately.
Before publishing the entry, apply image output validation. A successful generation response alone does not establish that the bytes are decodable, the dimensions are correct, or the image meets product requirements.
Maintain a reverse index from reference asset identifiers to derived cache entries. When a source is deleted or access is revoked, this index lets you invalidate affected results without scanning every request. Define retention for the generated object, thumbnails, backups, and audit records separately. Removing a lookup row is not equivalent to deleting the underlying image.
Do not share private outputs across tenants to improve hit rate. Public, intentionally reusable templates can live in a separate public asset catalog with explicit rights and publishing controls. Keep that catalog distinct from the private generation cache.
How much generation spend can reuse avoid?
Estimate avoided generation charges from actual eligible cache hits, not from the total number of preview requests.
As of November 2, 2026, the supplied GenRelay image rates are Nano Banana Pro at $0.030 per 1K or 2K image and $0.042 per 4K image; Nano Banana 2 at $0.020 per 1K image and $0.036 per 4K image; and GPT-Image-2 at $0.014 per image. Check the applicable configuration before deployment; prices do not establish equivalent quality or resolution capabilities across models.
For a hypothetical workload of 1,000 requests that would otherwise generate a Nano Banana Pro 1K image, 300 authorized reuse hits avoid 300 × $0.030 = $9.00 of generation charges. The remaining 700 generations cost $21.00. This excludes storage, downloads, processing, retries, and review. It is illustrative arithmetic, not a measured cache hit rate or a promised saving.
Track eligible requests, authorized hits, permission-denied hits, expired entries, fresh-generation requests, and cost per accepted output. A high hit rate is not useful if customers requested new variations. Review model options on the GenRelay image API page, then measure behavior using your own workload.
What questions do developers ask about image caching?
Does an identical prompt imply an identical image?
No. Generative outputs can vary, and inputs include more than prompt text. Cache reuse means intentionally returning a previous output, not claiming that a new model call would reproduce it.
Can I share cache entries between customers?
Keep private generations tenant-scoped. Cross-customer reuse needs an explicitly public asset workflow, appropriate rights, and separate access rules; matching prompts do not provide consent.
Is a SHA-256 key enough to protect private files?
No. Hashing creates an identifier, not an access-control system. Authenticate the requester, verify authorization, and keep storage private regardless of how difficult a key is to guess.
Should a cache miss automatically create a paid generation?
Not necessarily. A preview endpoint can report that regeneration is required and ask for a separate authorized generation action. This prevents routine page views or expired links from silently creating paid work.