"I'm passing URLs to Gemini, but the response only mentions the homepage description" or "urlContextMetadata comes back empty even though I configured the tool" — anyone who tries to ship URL Context in production will run into one of these symptoms. I hit the exact same wall while building an internal summarization pipeline, and most of the debugging was simply learning which signal to trust.
This article walks through the three patterns that produce "URL Context looks broken" and the checks I now run before touching prompts. If you need the fundamentals first, the Gemini API URL Context introduction covers the basic wiring; this guide picks up where that one leaves off.
Three checks that resolve most cases
Eighty percent of the symptoms I've seen come down to one of these three. Run them in order before changing anything else.
1. Is the tool actually declared in tools?
Confirm your config has tools=[{ "url_context": {} }] — and confirm the array structure isn't broken by mixing in Function Calling. When developers chain multiple tools, URL Context is often quietly dropped because the array shape went wrong.
2. Does the prompt contain raw URLs?
URL Context auto-detects URLs that appear in the prompt body. If you wrap them in custom tags like <url>...</url>, JSON-encode them, or split them across lines with backslashes, detection silently fails. Write https://example.com/article as plain text in the prompt.
3. Are you logging urlContextMetadata on every call?
Before doubting the answer quality, check the metadata to see which URLs were fetched and what status came back. If the array is empty, the tool wasn't invoked at all — that's a different bug than fetch-failed.
from google import genai
from google.genai import types
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-pro",
contents="Summarize the three key points of: https://ai.google.dev/gemini-api/docs/url-context",
config=types.GenerateContentConfig(
tools=[types.Tool(url_context=types.UrlContext)],
),
)
# Always inspect metadata first
meta = response.candidates[0].url_context_metadata
for entry in meta.url_metadata:
print(entry.retrieved_url, entry.url_retrieval_status)
print("---")
print(response.text)Expected output: each URL shows URL_RETRIEVAL_STATUS_SUCCESS. If you see _ERROR or _PAYWALL, you're looking at site-side issues described below.
Symptom-by-symptom triage
Symptom A: The response ignores URL content and only gives generic answers
If url_metadata is empty in the metadata, the tool was never invoked. The usual culprits:
- The prompt's instructions are weak ("here's a link") and the model decided fetching wasn't necessary
- The URL is a shortener (bit.ly etc.), and the model didn't follow the redirect
- A
system_instructiontells the model not to access external resources
The single biggest fix is making the instruction explicit: "Fetch the content of the following URL before answering." That alone raises detection rates substantially. Expand shorteners ahead of time — don't rely on the model to do it.
Symptom B: URLs appear in metadata, but status is _ERROR
The URL was detected, but the fetch failed. The common causes:
- The site's
robots.txtblocks Google's crawlers - The URL requires authentication (logged-in dashboards, internal wikis)
- Cloudflare or another WAF treats the request as bot traffic
- The page is a JavaScript-rendered SPA and the initial HTML is empty
Internal tooling and private Notion pages are out of scope by design. Either export the data to a public page, or switch to passing the file via the Files API instead.
Symptom C: Requests time out or take far too long
URL Context fetches multiple URLs in parallel internally. Once a prompt has more than 10 URLs, even one or two slow fetches can drag the whole request past the timeout. In my experience, keeping concurrent URLs to 3–5 per request is the sweet spot for production stability. For larger workloads, batch the requests rather than packing more URLs into one.
For retry strategy around timeouts, see Gemini API retry and error-handling patterns — the two pieces work well together.
Sites that consistently fail (and what to do)
Operating this in production, you'll hit cases where one specific domain reliably fails even though it should "just work." Some patterns I've documented:
- Major news sites: Most return 401/403 to AI crawlers as policy. Use their RSS feeds or partner APIs instead
- Twitter/X, LinkedIn timelines: Login-required, so fetches won't succeed
- Notion public pages: The URL is public, but JS-rendered content often isn't in the initial HTML
- GitHub README files: Usually fetchable; private repos obviously aren't
For JS-rendered sites, the most stable pattern today is: render the page with your own headless browser, save the resulting HTML, then upload via Files API and pass it to Gemini. URL Context isn't the right tool for SPAs.
When URL Context is the wrong tool
When something doesn't work, it's worth pausing to ask whether you're using the right tool at all. My rules of thumb:
- Same URL fetched repeatedly → build your own cache layer; it's faster and cheaper
- Information that's legally restricted from automated access → don't use URL Context
- You need structured extraction → URL Context is good at summaries but inconsistent for structured output. Combine it with Function Calling or
responseSchemafrom the start
If the goal is search-driven research, Grounding (Google Search integration) is usually the better fit. For Grounding-side issues, see troubleshooting Grounding when it isn't working.
Defensive patterns for production
Here's the wrapper I always reach for when running URL Context in production:
def fetch_with_url_context(client, model, prompt: str, urls: list[str]) -> dict:
"""Run URL Context and return both the text and per-URL fetch results."""
full_prompt = f"{prompt}\n\nReference URLs:\n" + "\n".join(urls)
resp = client.models.generate_content(
model=model,
contents=full_prompt,
config=types.GenerateContentConfig(
tools=[types.Tool(url_context=types.UrlContext)],
temperature=0.2,
),
)
meta = resp.candidates[0].url_context_metadata
fetched = [
{"url": m.retrieved_url, "status": str(m.url_retrieval_status)}
for m in meta.url_metadata
]
failed = [f for f in fetched if "SUCCESS" not in f["status"]]
return {"text": resp.text, "fetched": fetched, "failed": failed}Always return per-URL fetch status to the caller. That way your UI can tell users "this source couldn't be retrieved — we skipped it" instead of silently producing an answer based on 3 of 5 sources. The silent partial failure is the scariest mode this tool has, so make it visible.
URL Context has a small set of failure modes, and they're all observable once you log urlContextMetadata. Start there, look at the source site honestly, and tighten the prompt — most issues you're hitting today should be fixable in the same session.
Observability you'll thank yourself for
URL Context failures are the kind of thing that look fine in dev and start mattering only when traffic ramps up. A few small habits make the difference between "we noticed within an hour" and "we noticed when a customer complained."
The first habit is to push every url_retrieval_status into your logs as a structured field, not as part of a free-text log line. With a structured field you can build a single dashboard panel that shows the rate of _ERROR and _PAYWALL per domain. Once that panel exists, regressions become obvious in minutes — usually long before they reach end users.
The second habit is to keep a tiny domain allowlist for production usage. URL Context will happily try to fetch any URL the model decides to follow, including any that happen to slip through user input. Limiting the tool to a vetted set of domains (your own docs, your partner sites, well-known references) eliminates an entire class of "why did the model summarize a random forum post" bugs.
The third habit is to alert on metadata-empty responses when you expected fetches. If your prompt names three URLs but url_metadata returns zero entries, that's not a soft failure — that's the tool not running. Treating those events as a real signal, not noise, has saved me from shipping silently broken summaries more than once.
A diagnostic checklist
Rather than reasoning from scratch every time, I run through this list before changing prompts or models:
- Are you logging
urlContextMetadata.url_metadataon every call? - Does
toolscontain exactly[Tool(url_context=UrlContext)]? - Are URLs written as plain text in the prompt (not wrapped in tags or JSON)?
- Are you avoiding URL shorteners (
bit.ly,t.co)? - Is the URL count per request 5 or fewer?
- Does any
system_instructionforbid external references? - Is the target page free of authentication?
- Is the page server-rendered (not a JS-only SPA)?
- Are per-URL
url_retrieval_statusvalues returned to the caller?
If any item fails, fix that first and rerun. Don't start tweaking prompts or switching models until all nine pass — most "URL Context is broken" tickets dissolve at this stage.
Where to go next
Once URL Context is reliable, the natural next step is putting it to work. The URL Context for competitive analysis automation walkthrough shows a production-style pipeline that builds on the diagnostic patterns here. In my own experience, once summarization tasks stabilized, URL Context started paying for itself in places I didn't initially expect — competitor monitoring, change tracking on documentation sites, weekly digest generation. The fastest way to expand what you can build with this tool is to make sure the failure modes are no longer mysteries.