Cloudflare AI Search Is GA: Native Image Retrieval, OCR, and What Billing on November 1 Changes

·10 min read·Evergreen Tools Team

Cloudflare's AI Search came out of beta on October 1, 2026, and the part worth your attention is not the GA label. It is that the retrieval pipeline stopped treating images as a text problem. Until now, an image was searchable only through whatever a caption happened to mention: detect objects, write a sentence, embed the sentence. Product texture, screenshot state, chart relationships and layout all vanished in that compression. The GA version embeds image pixels directly, keeps captions alongside for textual understanding, and uses Matryoshka Representation Learning to keep the resulting vectors storable. Billing starts November 1, 2026, with a free tier retained on every Workers plan, so the practical question is what your corpus will cost and which limit binds first. That is arithmetic, not vibes.

Server racks in a data centre

Managed retrieval: the failure modes are boring, which is what you want from infrastructure.

What GA actually changes

Three additions ship with general availability: native image embeddings, optical character recognition for PDFs, and support for larger files. The service is the same managed index and retrieval pipeline built from Workers AI, Vectorize, R2 and Browser Run.

The pricing change is scheduled rather than immediate. Usage-based billing begins November 1, 2026, with included monthly ingestion, storage, semantic query and full-text query usage, and a generous free tier continues on all Workers plans. If you are already running an instance, the next six weeks are the window to measure your real query and ingestion volume under the new metering, not to guess at it.

# Before you design around AI Search, check which limit binds first for your corpus.
FREE = {"instances": 100, "files_per_instance": 100_000, "queries_per_month": 20_000,
        "pages_crawled_per_day": 500, "max_file_mb": 4}
PAID = {"instances": 5_000, "files_per_instance": 1_000_000, "queries_per_month": None,
        "pages_crawled_per_day": None, "max_file_mb": 4}

def headroom(plan, docs, queries, pages, largest_file_mb):
    limits = FREE if plan == "free" else PAID
    months = [
        ("files", docs / limits["files_per_instance"]),
        ("pages/day", pages / (limits["pages_crawled_per_day"] or 1e9)),
        ("file size", largest_file_mb / limits["max_file_mb"]),
        ("queries", (queries / limits["queries_per_month"]) if limits["queries_per_month"] else 0),
    ]
    return sorted(((name, round(ratio, 2)) for name, ratio in months), key=lambda kv: -kv[1])

print(headroom("free", docs=40_000, queries=12_000, pages=180, largest_file_mb=3.2))
# [('queries', 0.6), ('pages/day', 0.36), ('files', 0.4), ('file size', 0.8)]
# Highest ratio is what you plan around. Free tier: file size and query volume usually win.

The multimodal fix is the real engineering

The old implementation was honest about its naivety: object detection, caption generation, embed the caption. Images became searchable, but only through details the caption captured. The new path does both, caption-based understanding and native image retrieval, and the caption no longer has to anticipate which details a future query will care about.

Native retrieval runs on Qwen3-VL-Embedding. At query time AI Search checks whether your instance's embedding model supports images. If it does, the query image is embedded directly into the same vector space as your indexed images and text. If it does not, the query image is converted to text with ToMarkdown and searched as a caption, which gives every model basic multimodal support while native models get the full signal.

Matryoshka Representation Learning is the reason this stays affordable: it lets a smaller embedding retain useful information, so storage and search latency do not scale linearly with the richness of the representation.

# Crawl budgets interact: pages per crawl, files per instance, pages per day.
def crawl_days(pages, plan="free"):
    per_day = FREE["pages_crawled_per_day"] if plan == "free" else float("inf")
    days = pages / per_day if per_day != float("inf") else 1
    return {"pages": pages, "plan": plan, "days_to_ingest": days,
            "files_limit_hit": pages > (FREE if plan == "free" else PAID)["files_per_instance"]}

print(crawl_days(12_000))              # free tier: 24 days at 500 pages/day
print(crawl_days(12_000, "paid"))      # paid: single pass, unlimited pages/day
# A discover crawl accepts up to 100,000 pages, but the lowest of the three limits binds.

Follow the query, then decide what to trust

A query moves through a fixed pipeline. Optional rewrite, then embedding, then vector and keyword search in parallel, then fusion, optional reranking, then either the top chunks or a generated answer from those chunks. Knowing the order matters because it tells you where to intervene. If results are semantically close but miss exact terms like error codes or SKUs, the keyword branch is underweighted. If results are keyword-exact but conceptually wrong, the embedding side is doing the work.

Fusion is where most hybrid systems quietly fail. Reciprocal rank fusion is a decent default because it needs no score calibration between two retrieval systems that were never meant to be compared. The snippet shows how little code it takes to reason about your own fusion instead of accepting a black box.

# Hybrid retrieval fuses vector and keyword results. Reciprocal rank fusion is 4 lines.
def rrf(vector_hits, keyword_hits, k=60):
    '''vector_hits / keyword_hits: ordered lists of chunk ids'''
    scores = {}
    for weight, hits in ((1.0, vector_hits), (1.0, keyword_hits)):
        for rank, doc in enumerate(hits, start=1):
            scores[doc] = scores.get(doc, 0.0) + weight / (k + rank)
    return sorted(scores.items(), key=lambda kv: -kv[1])

VEC = ["chunk_9", "chunk_3", "chunk_14", "chunk_1"]
BM25 = ["chunk_3", "chunk_1", "chunk_22", "chunk_9"]
for doc, score in rrf(VEC, BM25)[:5]:
    print(f"{doc:10}{score:.4f}")
# AI Search runs vector and keyword search in parallel, fuses, optionally reranks,
# then returns chunks or hands the top ones to a generation model.
Close-up of a circuit board

Image pixels embedded directly, captions kept alongside. Both signals, not one.

The limits that decide your architecture

On Workers Free: 100 AI Search instances per account, 100 namespaces, 100,000 files per instance, 4 MB maximum file size, 20,000 queries per month, 10 instances per cross-instance search request, and 500 pages crawled per day. On Workers Paid: 5,000 instances, 1 million files per instance or 500,000 for hybrid search, unlimited queries, and unlimited daily crawling.

Two details catch people out. The 4 MB file limit is identical on both plans, so a corpus of large PDFs needs a pre-processing step regardless of what you pay. And a discover crawl accepts up to 100,000 pages but is simultaneously bounded by files per instance and pages crawled per day, so the effective ceiling is whichever limit is lowest. On the free plan that is 500 pages a day, which means a 12,000 page site takes 24 days to ingest.

# Image queries: route by whether the embedding model can take images natively.
def route_query(query_image, text_query, supports_image_embeddings):
    if query_image and supports_image_embeddings:
        return {"mode": "native_image", "note": "embedded by Qwen3-VL-Embedding, same vector space as indexed images"}
    if query_image:
        return {"mode": "caption_fallback", "note": "image converted to text with ToMarkdown before search"}
    if text_query:
        return {"mode": "text", "note": "standard hybrid retrieval"}
    return {"mode": "invalid", "note": "need an image or a query"}

print(route_query(True, None, True))
print(route_query(True, None, False))
print(route_query(None, "bird perched on a leafy branch", True))
# Text-only models still work with image queries, they just lose the fine visual signal.

What the price includes and what it does not

Storage, vector indexing and the Browser Run usage that website crawling consumes are included with AI Search. Workers AI and AI Gateway are still billed separately. That split is the one to model, because generation is usually the variable cost that dominates at scale while retrieval stays comparatively flat.

There is also a migration wrinkle. Instances created before AI Search moved to managed infrastructure ran on services in your own account, so older invoices may include separate R2, Vectorize, Workers AI and AI Gateway charges. After the move, crawled pages live in built-in storage, and the dedicated R2 bucket AI Search originally created is no longer used. It still exists in your account, and any objects left in it may still count toward R2 storage, so check the contents before you delete anything.

# Migration: after GA, AI Search stores pages itself. Old R2 buckets are no longer written.
import boto3

def legacy_bucket_report(bucket, prefix="ai-search/", sample=1000):
    s3 = boto3.client("s3", endpoint_url="https://<accountid>.r2.cloudflarestorage.com")
    listing = s3.list_objects_v2(Bucket=bucket, Prefix=prefix, MaxKeys=sample)
    objects = listing.get("Contents", [])
    size = sum(o["Size"] for o in objects)
    return {"sampled_objects": len(objects), "sampled_mb": round(size / 1e6, 1),
            "truncated": listing.get("IsTruncated", False)}

print(legacy_bucket_report("legacy-ai-search-bucket"))
# Check the sample before deleting anything: R2 storage for that bucket may still bill,
# and only you know whether anything else points at those objects.
Charts on a desk

The binding limit is usually file size or query volume, not the number of instances.

Build or buy, restated honestly

A managed index is the right answer when your corpus is documents and web pages, your team is small, and your retrieval quality bar is 'good enough to answer questions from our own content'. The free tier's 20,000 queries a month covers an internal tool or a docs site, and the failure modes are boring.

Build your own when the ranking has to encode domain judgement, when you need chunk-level control that a managed service will not expose, or when the corpus is not text at all. The multimodal addition narrows that gap for image-heavy catalogues, but it does not close it: you still cannot inspect the index, and a managed service will always be a step behind the retrieval trick you read about last week.

What I would do this month is unglamorous. Export a hundred queries your users actually type, run them against both your current setup and an AI Search instance, and score the top five results by hand. Then price out the ingestion you need under the new metering. Six weeks of measurement beats six months of architecture debate.

📌 Frequently Asked Questions

When did Cloudflare AI Search become generally available?

It reached general availability on October 1, 2026. The GA release added native image embeddings, OCR for PDFs and support for larger files.

When does billing start?

Usage-based billing begins November 1, 2026, with included monthly ingestion, storage, semantic query and full-text query usage. A free tier remains on all Workers plans.

How does native image retrieval differ from caption search?

Caption search embeds a generated description, so anything the caption omitted is unfindable. Native retrieval embeds the image pixels themselves with Qwen3-VL-Embedding, preserving texture, layout, colour and spatial relationships that a short caption cannot carry.

What are the free tier limits?

100 instances, 100 namespaces, 100,000 files per instance, 4 MB maximum file size, 20,000 queries per month, and 500 pages crawled per day, with up to 100,000 pages per discover crawl bounded by the lowest applicable limit.

Is storage and crawling included in the price?

Storage, vector indexing and the Browser Run usage consumed by website crawling are included with AI Search. Workers AI and AI Gateway usage is billed separately.