Skip to content
Use Prompt Caching for Repeated Analysis of Web Documents
Article

Use Prompt Caching for Repeated Analysis of Web Documents

Guide

Use your LLM provider's prompt caching when you analyze many extracted web pages. Keep stable prompt sections first, put fresh records last and measure cached versus uncached cost.

By MrScraper Team 13 min read

Many web data jobs send almost the same prompt over and over. You might classify 2,000 extracted pages with the same long instructions. Or you might ask 30 different questions about the same long document.

In both cases, most of each prompt is identical. Prompt caching lets your LLM provider reuse the work for that repeated part instead of processing it again, which can lower cost and speed up responses.

We tested both job shapes on 39 extracted book pages. Caching cut the total cost by about 56% for the first shape and 79% for the second. Apart from normal model variation, the answers stayed the same. The full setup and numbers are near the end of this guide.

Prompt caching is a feature of your LLM provider, not of MrScraper. MrScraper supplies the page content. Your application decides how to arrange the prompt, and the provider decides what gets cached. This guide uses Anthropic's Claude API for the code, because it lets you mark exactly what to cache. The same design rules apply to other providers.

How prompt caching works, in one paragraph

The provider stores the processed form of a prompt's prefix, the part from the start up to a point you choose. When a later request starts with exactly the same prefix, the provider reads it from the cache. Reading from the cache is billed at a fraction of the normal input price. Writing to the cache costs a bit more than normal input. The cache expires after a short time, so it pays off only when requests that share a prefix arrive close together.

Which prompt sections remain stable?

Anything that is the same for every request in a job is stable. Put those sections first, in a fixed order, and put everything that changes after them.

Usually stable Usually changes every request
Task instructions The extracted record or page
Label definitions and rubrics The question being asked
Output schema or format rules Timestamps and request IDs
Worked examples User names or session details
Tool definitions Anything with "today's date" in it
A long document, when you ask many questions about it

Anthropic's prompt caching docs build the cache in a fixed order: tools, then system, then messages. A change at one level invalidates that level and everything after it. Changing a tool definition, for example, invalidates the whole cache.

There are two common job shapes:

  • Same instructions, many records. The instructions are the stable prefix. Each extracted record goes after it.
  • Same document, many questions. The instructions and the document are the stable prefix. Each question goes after it.

How do you separate reusable context from fresh records?

Build the prompt in two parts: a stable prefix you mark for caching, and a fresh suffix with the record or question. Make sure the prefix is byte-for-byte identical on every request.

Get the page content first

The MrScraper Scrape JSON endpoint returns structured records. The Scrape Markdown endpoint returns a whole page as Markdown, which suits the "many questions" shape. Save each result with a hash of its content.

python
import hashlib
import json
import os
import requests
from datetime import datetime, timezone
 
MRSCRAPER_URL = "https://api.mrscraper.com/"
MRSCRAPER_TOKEN = os.environ["MRSCRAPER_API_TOKEN"]
 
 
def fetch_markdown(url: str) -> dict:
    response = requests.post(
        MRSCRAPER_URL,
        params={"token": MRSCRAPER_TOKEN, "markdown": "true", "html": "false"},
        headers={"x-api-token": MRSCRAPER_TOKEN, "Content-Type": "application/json"},
        json={"url": url, "maxRetries": 2},
        timeout=330,
    )
    response.raise_for_status()
    markdown = response.json().get("markdown", "")
    return {
        "url": url,
        "retrieved_at": datetime.now(timezone.utc).isoformat(),
        "sha256": hashlib.sha256(markdown.encode()).hexdigest(),
        "markdown": markdown,
    }

Shape 1: same instructions, many records

Put the instructions in the system prompt and mark the end of it with cache_control. The record goes in the user message, after the cached part.

python
import anthropic
 
client = anthropic.Anthropic()
MODEL = "claude-sonnet-5-5"   # Check the minimum cacheable length for your model
 
RUBRIC_VERSION = "policy-review-v3"
INSTRUCTIONS = f"""Rubric version: {RUBRIC_VERSION}
You review refund policies extracted from company websites.
Return JSON with these fields: refund_window_days, conditions, needs_human_review.
... (full instructions, label definitions and worked examples go here) ...
"""
 
 
def record_request(record: dict) -> dict:
    return {
        "model": MODEL,
        "max_tokens": 300,
        "system": [{"type": "text", "text": INSTRUCTIONS, "cache_control": {"type": "ephemeral"}}],
        "messages": [{"role": "user", "content": "Record:\n" + json.dumps(record, sort_keys=True)}],
    }

Shape 2: same document, many questions

Here the document is part of the stable prefix. Put it in the first content block of the user message and mark that block for caching. Each question goes in a second block, after the breakpoint.

python
DOC_INSTRUCTIONS = "Answer questions using only the document. Quote the sentence you used."
 
 
def question_request(doc: dict, question: str) -> dict:
    document_block = f"Source: {doc['url']}\nVersion: {doc['sha256'][:12]}\n\n{doc['markdown']}"
    return {
        "model": MODEL,
        "max_tokens": 400,
        "system": DOC_INSTRUCTIONS,
        "messages": [{
            "role": "user",
            "content": [
                {"type": "text", "text": document_block, "cache_control": {"type": "ephemeral"}},
                {"type": "text", "text": "Question: " + question},
            ],
        }],
    }

Notice what is not in the prefix: the retrieval time. It changes on every fetch even when the page does not. The content hash is there instead, because it only changes when the page does.

Rules that keep the prefix identical

  • Serialize the same way every time. Use json.dumps(..., sort_keys=True), so key order never changes.
  • Never put timestamps, IDs or user names in the prefix. One changed character means a cache miss.
  • Version the instructions. When you change the rubric, bump RUBRIC_VERSION. That makes the change visible in your logs.
  • Check the minimum length. Anthropic's docs list minimum cacheable lengths by model, from 512 to 4,096 tokens. A shorter prefix is processed without caching, and no error is returned.

How do you invalidate changed context?

The provider's cache already ignores changed content, because a different prefix simply does not match. The real job is making sure your own application never reuses an answer that came from old content.

Key your stored results by content, not by URL

If you save answers in your own database, include the content hash, the rubric version and the model in the key. When a page changes, its hash changes, and the old answer no longer matches.

python
def result_key(doc: dict, question: str) -> str:
    raw = json.dumps([doc["sha256"], RUBRIC_VERSION, MODEL, question])
    return hashlib.sha256(raw.encode()).hexdigest()

Refetch, compare, then rebuild

When you refresh a page:

  1. Fetch it again with MrScraper and compute the new hash.
  2. If the hash is the same, keep using the stored answers.
  3. If it changed, build new prompts from the new content. The first request writes a new cache entry, and later requests read from it.

Know what else breaks the cache

Content is not the only thing in the prefix. Anthropic's docs list settings that invalidate cached sections, including tool definitions, tool_choice, adding or removing images and some thinking settings. Keep those fixed for the length of a job.

Mind the time limit

Anthropic's default cache lasts 5 minutes, and each read refreshes it at no cost. A 1-hour option is available at a higher write price. For batch jobs, process all records that share a prefix back to back, so the cache stays warm.

How do you measure cached versus uncached cost?

Run the same requests twice, once with caching and once without, and compare the usage the provider reports. Do not rely on estimates.

Read the usage fields

Each Claude response reports three input counts:

  • cache_creation_input_tokens: tokens written to the cache
  • cache_read_input_tokens: tokens read from the cache
  • input_tokens: tokens after the last breakpoint, not cached
python
import time
 
 
def run(requests_to_send: list[dict], cached: bool = True) -> list[dict]:
    log = []
    for req in requests_to_send:
        if not cached:
            req = strip_cache_control(req)
        start = time.perf_counter()
        response = client.messages.create(**req)
        seconds = time.perf_counter() - start
        u = response.usage
        log.append({
            "seconds": seconds,
            "cache_write": u.cache_creation_input_tokens or 0,
            "cache_read": u.cache_read_input_tokens or 0,
            "uncached": u.input_tokens,
            "output": u.output_tokens,
        })
    return log
 
 
def strip_cache_control(req: dict) -> dict:
    clean = json.loads(json.dumps(req))
    blocks = list(clean["system"]) if isinstance(clean["system"], list) else []
    for m in clean["messages"]:
        if isinstance(m["content"], list):
            blocks += m["content"]
    for block in blocks:
        block.pop("cache_control", None)
    return clean

Turn usage into cost

Cache reads and writes are priced as a multiple of the model's base input price. On October 21, 2026, Anthropic's docs listed these multipliers:

Item Multiplier of base input price
5-minute cache write 1.25x
1-hour cache write 2x
Cache read, most models 0.1x
Cache read, Claude Opus 5.5 and Sonnet 5.5 0.05x
Cache read, Claude Fable 5.1 and Mythos 5.1 0.025x
python
def job_cost(log: list[dict], base_in: float, base_out: float,
             write_mult: float = 1.25, read_mult: float = 0.05) -> float:
    total = 0.0
    for r in log:
        total += (r["uncached"] + r["cache_write"] * write_mult + r["cache_read"] * read_mult) * base_in
        total += r["output"] * base_out
    return total / 1_000_000

Prices here are per million tokens. Take base_in and base_out from the pricing page for your model on the day you run the test.

The cache hit share is cache reads divided by all input tokens. A low share usually means something in the prefix is changing between requests, or requests are too far apart.

What we measured

We ran both job shapes with and without caching, using the code in this guide.

The setup

  • Date: October 21, 2026
  • Model: Claude Sonnet 5.5 (claude-sonnet-5-5)
  • Records: 39 books from books.toscrape.com, a practice site built for scraping, extracted with MrScraper's Scrape JSON endpoint. We fetched 40, but one page came back with an empty description, so we left it out.
  • Shape 1: classify each book into one of 15 genres, or "unknown." The cached prefix was the instructions, genre definitions and 8 worked examples, about 1,328 tokens. Each request then added one book record.
  • Shape 2: ask 10 questions about one catalog document holding all 39 descriptions. The cached prefix was about 16,297 tokens.
  • Prices: $2 per million input tokens and $10 per million output tokens, with cache writes at 1.25x and cache reads at 0.05x, per Anthropic's pricing and caching docs on the test date.
  • Order: for each shape, all requests ran one after another without caching, then again with caching.

The results

Same instructions, many records

Run Requests Cache hit share Input cost Output cost Total cost Median response time
Without caching 39 0% $0.1381 $0.0311 $0.1692 1.85 s
With caching 39 73% $0.0434 $0.0308 $0.0741 1.78 s

Same document, many questions

Run Requests Cache hit share Input cost Output cost Total cost Median response time
Without caching 10 0% $0.3268 $0.0168 $0.3436 2.85 s
With caching 10 90% $0.0563 $0.0163 $0.0726 2.58 s

What the numbers show

  • Caching changed the bill, not the prompt. Both runs of each shape sent exactly the same number of input tokens: 69,050 for shape 1 and 163,387 for shape 2. Only the billing of those tokens changed.
  • A longer stable prefix saved more. Input cost fell by about 69% in shape 1 and 83% in shape 2. In shape 2, the cached document made up almost all of each prompt. In shape 1, each book record was a larger share of the prompt, and that part is never cached.
  • The first request pays to write. In each cached run, only the first request wrote to the cache: 1,321 tokens in shape 1 and 16,292 in shape 2. Every request after that read from it.
  • Output cost did not change. Caching only affects input. If your job writes long answers, savings will be a smaller share of the total.
  • Response times improved a little. The median time fell from 1.85 to 1.78 seconds in shape 1 and from 2.85 to 2.58 seconds in shape 2. With one run each, treat that as a small effect, not a firm number.

Did caching change the answers?

No. Caching reuses the processed prompt, so it should not change what the model says, and our check agreed.

In 30 of the 39 records, both runs returned a complete answer, and 29 of those 30 got the same genre. The one difference was Buying In, a book about consumer culture that sits between business and general nonfiction. The model gave different labels on different runs, which is normal model variation.

The other 9 records hit our 150-token output limit in at least one run, before the JSON answer was complete. That was a setting in our test, not a caching effect. If you ask for JSON, give max_tokens enough room and check each response's stop reason before you trust the output.

Limits of this test

This was one run of each shape, on one model, on one day, with prices from that day. Your savings depend on how long your stable prefix is, how many requests share it and how close together they arrive. With short prefixes, few repeats or long gaps between requests, the cost of writing to the cache can outweigh the savings. Run the same comparison on your own job before you count on a number.

What about other providers?

The same design works elsewhere, with different controls:

  • OpenAI turns on caching automatically for supported models. Its prompt caching guide lists a 1,024-token minimum for GPT-5.6 and later, and reports cached tokens in input_tokens_details.cached_tokens.
  • Google Gemini has implicit caching on by default for Gemini 2.5 and newer models, per its caching docs. Cached tokens appear in usage.total_cached_tokens. In each case, a stable prefix first and fresh content last is what makes cache hits possible.

Hand off to your application

Store three things for every analysis run: the content hash of each page, the rubric version and the provider's usage numbers. They let you reuse answers safely, spot cache misses and show what caching actually saved.

FAQs

Which prompt sections remain stable?

Task instructions, label definitions, output rules, worked examples and tool definitions usually stay the same for a whole job. When you ask many questions about one document, the document is stable too. Put these first, in a fixed order, and keep timestamps, IDs and fresh records out of them.

How do you measure cached versus uncached cost?

Run the same requests with and without caching. Record the provider's usage fields for cache writes, cache reads and uncached input. Multiply each by its price for your model on the test date. Compare total cost, cache hit share and response time between the two runs. In our test, total cost fell by about 56% and 79% for the two job shapes.

Which parts does MrScraper provide, and which parts must my application implement?

MrScraper is a developer-focused web data API. It provides the page content: structured records from Scrape JSON or full pages as Markdown from Scrape Markdown. Prompt caching is a feature of your LLM provider, not of MrScraper.

Your application arranges the prompt, marks what to cache, hashes page content, stores results by content version and measures cost from the provider's usage fields. Your team also decides which pages it has permission to use.

Summarize this post

Open it in your assistant of choice with the prompt ready to send.

Take a Taste of Easy Scraping!

Your choices

Cookie preferences

Necessary cookies keep your selection. Optional categories are disabled until you switch them on.

Strictly necessary

Remembers your privacy selection and keeps the site working.

Always on