CLAUDE LABJP
QUOTA — The 50 percent weekly usage boost for Claude Code subscribers ends August 19. One day left to front-load any heavy parallel workPRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1MIGRATION — The legacy Workbench and the experimental prompt tool APIs retired on August 17. Anything that depended on them now needs a hand-rolled equivalent on the Messages APIGOVERNMENT — Claude for Government is in beta. Anthropic remains the contracting and billing party, so agencies can start without a separate cloud provider relationshipPROVENANCE — Claude models released from August 2 onward embed a machine-readable watermark in generated text. It applies worldwide rather than only in the EU, with no stated effect on quality, speed, or priceCLI — v2.1.233 adds memory cgroup support for Bash tool commands on Linux, so CLAUDE_CODE_TOOL_MEMORY_LIMIT keeps a runaway build from stalling the sessionQUOTA — The 50 percent weekly usage boost for Claude Code subscribers ends August 19. One day left to front-load any heavy parallel workPRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1MIGRATION — The legacy Workbench and the experimental prompt tool APIs retired on August 17. Anything that depended on them now needs a hand-rolled equivalent on the Messages APIGOVERNMENT — Claude for Government is in beta. Anthropic remains the contracting and billing party, so agencies can start without a separate cloud provider relationshipPROVENANCE — Claude models released from August 2 onward embed a machine-readable watermark in generated text. It applies worldwide rather than only in the EU, with no stated effect on quality, speed, or priceCLI — v2.1.233 adds memory cgroup support for Bash tool commands on Linux, so CLAUDE_CODE_TOOL_MEMORY_LIMIT keeps a runaway build from stalling the session
Articles/Claude Code
Claude Code/2026-08-18Intermediate

Choosing a WebFetch Cache TTL That Fits the Way You Research

Claude Code lets you tune the WebFetch URL cache TTL. Here is how to see the shape of your own research sessions with a tiny observation server, then trade fetch count against freshness with numbers instead of guesswork.

Claude Code223WebFetchEnvironment VariablesCachingSolo Development3

Running several apps as an indie developer means periodically re-reading the same pages: dependency release notes, store policy documents, changelogs. I hand most of that reading to Claude Code. One afternoon I was scrolling through a local proxy log and noticed the same release-notes page being pulled several times inside a single session.

In hindsight it is obvious. When you compare several pages against each other, you keep returning to whichever one you treat as the baseline. Humans do it. Agents do it too.

Claude Code v2.1.233 added CLAUDE_CODE_WEBFETCH_CACHE_TTL_MS, which controls how long a fetched URL stays reusable. The documented default is 15 minutes. Whether you should touch that number depends entirely on the shape of your own research — so let us look at that shape first, then decide.

Start by watching how often you hit the same URL

Counting repeat requests against a real website is awkward. It is much faster to stand up a small local server and point the work at that instead.

# server.py — an observation server that does nothing but count hits
import http.server, socketserver, datetime, collections
 
HITS = collections.Counter()
 
class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        HITS[self.path] += 1
        ts = datetime.datetime.now().strftime("%H:%M:%S")
        print(f"{ts}  GET {self.path}  (hit #{HITS[self.path]})", flush=True)
        body = f"doc for {self.path}\n".encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)
 
    def log_message(self, *args):
        pass  # silence the default access log so counts stay readable
 
with socketserver.TCPServer(("127.0.0.1", 8731), Handler) as httpd:
    print("listening on http://127.0.0.1:8731", flush=True)
    httpd.serve_forever()

Start it with python3 server.py, request the same path three times, and you get:

15:06:33  GET /release-notes  (hit #1)
15:06:33  GET /release-notes  (hit #2)
15:06:33  GET /release-notes  (hit #3)
15:06:33  GET /policy  (hit #1)

Two small details matter here. flush=True makes each line appear the instant the request lands, and silencing log_message keeps the built-in access log from doubling every entry. Skip either one and the counts become hard to read at exactly the moment you need them to be clear.

From there, give Claude Code a research prompt that includes local URLs such as http://127.0.0.1:8731/release-notes and watch how the hit counters climb. If a path keeps incrementing, your work has a refetch-heavy shape and the TTL is worth tuning. If it does not, changing the setting will buy you very little.

How the cache is scoped — whether it is shared across sessions or processes — is the kind of thing that varies by environment. Rather than assuming, use this same server to observe what your setup actually does.

Fetch count and freshness move in opposite directions

Once you know the shape, you can estimate what different TTLs would do. Re-running full sessions repeatedly is slow, so it is quicker to feed an access trace into a short script.

# ttl_model.py — turn an access trace into fetch counts and data age
import random
 
def build_trace(minutes=90, seed=7):
    """Return often to a few baseline pages, occasionally open a new link."""
    rnd = random.Random(seed)
    core = [f"https://example.test/core/{i}" for i in range(4)]
    trace = []
    for t in range(minutes):
        for _ in range(rnd.choice([0, 1, 1, 2])):
            if rnd.random() < 0.62:
                trace.append((t, rnd.choice(core)))
            else:
                trace.append((t, f"https://example.test/leaf/{rnd.randrange(400)}"))
    return trace
 
def count_fetches(trace, ttl_min):
    last, fetches = {}, 0
    for t, url in trace:
        prev = last.get(url)
        if prev is None or t - prev >= ttl_min:
            fetches += 1
            last[url] = t
    return fetches
 
def staleness(trace, ttl_min):
    """How old the content in hand is at each point of access."""
    last, ages = {}, []
    for t, url in trace:
        prev = last.get(url)
        if prev is None or t - prev >= ttl_min:
            last[url] = t
            ages.append(0)
        else:
            ages.append(t - prev)
    return max(ages), sum(ages) / len(ages)
 
trace = build_trace()
for ttl in (0, 5, 15, 60, 240):
    mx, av = staleness(trace, ttl)
    print(f"TTL={ttl:>3}m  fetches={count_fetches(trace, ttl):>3}  "
          f"worst age={mx:>2}m  mean age={av:>4.1f}m")

Here is what that produced on my machine, for a trace of 86 accesses across 30 unique URLs standing in for a 90-minute research session.

TTLActual fetchesRefetches avoidedWorst age of data in handMean age
0 min (always fetch)860.0%0 min0.0 min
5 min6425.6%4 min0.6 min
15 min (default)4547.7%14 min3.8 min
60 min3460.5%59 min14.4 min
240 min3065.1%85 min24.3 min

These figures belong to this particular input, not to your work. Replace build_trace with a trace derived from your own logs and read the slope of your own curve rather than borrowing mine.

The direction of that slope is still telling. Going from 15 minutes to 60 removes only another 13% of fetches, while the mean age of the content in hand grows from 3.8 minutes to 14.4 — roughly four times older. Nearly all the benefit lives between 0 and 15 minutes; past that point you are trading freshness away for very little. The 15-minute default sits just before the bend.

Decide per use case which side you care about

Which side of that trade you favor depends on how fast the thing you are reading changes. Across the three kinds of lookups I do most often, it breaks down like this.

Kind of lookupHow fast it changesLean towardWhy
Tracking release notes and changelogsDaysDefault, or shorterUpdates rarely land mid-session, but reaching a conclusion from an outdated line is expensive to undo
Reading through terms and policiesMonthsLongerYou return to the same clauses repeatedly, and mid-session changes are not a realistic concern
Verifying a page you just publishedYou change it yourselfShort, or offYou are fetching something you just edited, so a cached copy defeats the entire purpose

That third row is where I lost the most time. I was asking for a check on a page I had just fixed, and the answer came back clean — based on the version from before the fix. I spent a while doubting the fix itself before suspecting my own configuration. A cache is simply a liar about anything you are actively rewriting.

Apply the setting, then confirm it landed

The value is in milliseconds. One hour looks like this:

# scoped to a single invocation
CLAUDE_CODE_WEBFETCH_CACHE_TTL_MS=3600000 claude
 
# for verification work, where caching gets in the way
CLAUDE_CODE_WEBFETCH_CACHE_TTL_MS=0 claude

You can export it from your shell profile to make it permanent, but I settled on prefixing it per invocation instead. Research and post-publish verification want opposite settings, so pinning one value guarantees friction in the other case.

After setting it, go back to the observation server and check that it took effect. Run the same research prompt before and after the change; if the hit counters climb differently, the setting is live. If nothing changes, either the value is malformed or it is not reaching the process you think it is.

For the wider picture, see Claude Code Environment Variables — The Complete Practical Reference. The Bash tool memory limit that shipped in the same v2.1.233 is covered in A Runaway Build Dies Very Differently Under cgroup Than Under ulimit.

What to do next

Start the observation server and run one of your usual research prompts through it unchanged. If any path climbs into double-digit hits, a longer TTL will pay for itself. If nothing climbs, leave the default alone. Looking at the shape of your own work before picking a number takes ten minutes and removes most of the guesswork.

It is one environment variable, but putting numbers around it makes the trade visible: exactly what you are giving up, and exactly what you are getting for it.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-07-15
The Types Landed, but Nothing Got Safer — Where I Delegated a JS→TS Migration to Claude Code, and Where I Didn't
A green tsc run does not mean any is gone. Measuring a migration by type coverage, drawing a clear line between what Claude Code handles well and what a human must decide, and a CI ratchet that refuses regressions.
Claude Code2026-04-29
Two Personalities for Claude Code — A Morning and Afternoon Workflow That Separates Exploration from Implementation
Treating Claude Code as a different persona in the morning and afternoon stabilises design decisions. A solo developer's six-month run with two CLAUDE.md files, a switching script, and the failure modes I hit along the way.
Claude Code2026-08-17
Now that forking is the default, review agents are the ones I still call by name
Subagent forking became the default, so delegated work now inherits the parent conversation. Work can inherit context. Judgment cannot. Here is how I declare that boundary in the repository and catch it drifting.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →