CLAUDE LABJP
2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days noticeMCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by handNEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to itWINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a causeHANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days noticeMCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by handNEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to itWINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a causeHANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one
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 Code253WebFetch2Environment Variables2CachingSolo 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 $15 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-09-12
My overnight crawl never came back, and the reason was that WebFetch had no ceiling on how long it would wait
Claude Code v2.1.268 gives WebFetch a 300-second deadline. Here is how an unattended run stalls on a server that never closes its response, why CLAUDE_CODE_WEBFETCH_DEADLINE_MS=0 does not mean 'do not wait', and how I now place two separate ceilings.
Claude Code2026-09-10
When Every Request Fails With Not signed in to the Cloud gateway, Check the Version Before the Config
A Claude Code 2.1.265 regression broke every request for gateway and proxy setups. I use it as an excuse to build a small snapshot of the auth input surface, so the next time something breaks you can tell config from version in seconds.
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.
📚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