Bedrock Web Search: Grounding an Agent Without Building a Scraper

Cleber Rodrigues
Written by Cleber Rodrigues
Bedrock Web Search: Grounding an Agent Without Building a Scraper

On August 4, 2026, AWS made Web Search on Amazon Bedrock generally available, and the entire integration surface is one JSON object: {"type": "web_search"}. That is the whole change from an API perspective. What it replaces is a lot more interesting than what it adds. Every team that wired an agent to a third-party search API built roughly the same four things: a tool schema, a client-side loop that catches the tool call and executes it, an HTTP client with retry and rate-limit handling for the search vendor, and a citation extractor that stitches URLs back into the answer text. Bedrock now runs all four inside its own service boundary.

The price is $12.00 per 1,000 queries in US East (N. Virginia), US East (Ohio), and US West (Oregon), per the Amazon Bedrock pricing page. That number matters more than it looks, and I’ll come back to it, because for two of the three GPT-5.6 tiers the retrieval fee costs more than the inference it grounds.

What a server-side tool actually changes

Bedrock now documents three distinct modes of tool use, and Web Search sits in the second one. The distinction is not cosmetic. It determines who holds the retry budget, who sees the raw retrieved bytes, and where your failure modes live.

Mode Who executes the tool APIs Where retries and rate limits live
Client-side tool use Your application code, after the model returns a tool-call request Responses, Chat Completions, Converse, InvokeModel Your code
Server-side tool use Amazon Bedrock, via a registered Lambda function or AgentCore Gateway Responses API only Bedrock
Anthropic Claude tool use Your code, using Anthropic-defined types (computer_*, bash_*, text_editor_*, memory_*) Anthropic Messages format on bedrock-runtime or bedrock-mantle Your code

Source: Amazon Bedrock User Guide, “Use a tool to complete an Amazon Bedrock model response.”

With a custom search tool, your process is in the middle of every hop. The model says “call search with this query,” your handler runs, you call Brave or Tavily or Exa, you get JSON back, you decide how much of it to forward, you push a toolResult block back into the conversation, and you call the model again. Two model invocations minimum per grounded answer. Sometimes five, if the model reformulates.

Web Search collapses that to one request and one response. AWS is explicit about the mechanism in the launch blog post: the model identifies the knowledge gap, Bedrock formulates the query, retrieves from Amazon’s web index and knowledge graph, injects snippets plus source URLs and titles into the context window, and the model composes a grounded answer. Your code sees the finished product.

That is a real reduction in moving parts. It also means you gave up the seam where you used to inspect, filter, and truncate retrieved content before the model ever saw it. If you’ve read the server-side tool execution patterns in AgentCore Gateway, the tradeoff is familiar: you trade observability at the tool boundary for a much shorter code path. Web Search takes that trade further, because you don’t even register the tool. AWS did.

Web Search on Amazon Bedrock server-side request flow, showing the Responses API call to the bedrock-mantle endpoint, in-Region search and fetch against Amazon's web index and knowledge graph, and the grounded response with url_citation annotations

The endpoint gotcha nobody mentions in the announcement

Here’s the first thing that will break someone’s afternoon. Web Search is not available on the Converse API. It’s not available on bedrock-runtime at all.

Check the GPT-5.6 Terra model card. Under “APIs supported,” only Responses has a checkmark. Chat Completions is marked unsupported. Under “Endpoints supported,” bedrock-runtime is marked unsupported and bedrock-mantle is supported. The Web Search documentation says the same thing from the other direction: the tool is available for OpenAI GPT models served through the bedrock-mantle endpoint using the Responses API, currently openai.gpt-5.4, openai.gpt-5.5, and openai.gpt-5.6 in the luna, terra, and sol variants.

So if your platform standardized on ConverseStream because it gave you one message shape across Claude, Nova, and Llama, adopting Web Search means running a second client against a second endpoint with a second auth flow. That’s not a small refactor for a shared inference gateway. It’s a fork in your abstraction layer.

The auth flow is the second surprise. You don’t sign requests with SigV4 directly. You mint a bearer token from your existing AWS credentials using the aws-bedrock-token-generator package, and you hand that to the OpenAI client’s api_key parameter. AWS describes it as a short-lived credential, valid up to 12 hours, derived from your IAM identity through SigV4 and packaged in the format the OpenAI SDK expects.

from openai import OpenAI
from aws_bedrock_token_generator import provide_token

REGION = "us-east-1"

client = OpenAI(
    base_url=f"https://bedrock-mantle.{REGION}.api.aws/openai/v1",
    api_key=provide_token(region=REGION),
)

response = client.responses.create(
    model="openai.gpt-5.6-terra",
    input="What changed in Amazon Bedrock pricing this week?",
    tools=[{"type": "web_search", "external_web_access": False}],
)

print(response.output_text)

A 12-hour token is long enough that people will cache it in a module-level variable and forget it. In a long-running container that’s a slow-burning outage waiting for hour 13. Refresh it on a timer, or mint per request and accept the overhead.

The IAM trap that returns 403 by default

Read this part twice, because the default configuration is the broken one.

The external_web_access field on the tool defaults to true. AWS chose that default to match the OpenAI Responses API so existing code ports without edits. But true requires the bedrock-websearch:ExternalWebAccess IAM permission, and the AmazonBedrockFullAccess managed policy does not grant it. It grants bedrock-websearch:InvokeSearch and bedrock-websearch:InvokeFetch only.

The result: an identity with what most engineers consider maximum Bedrock permissions, calling the tool with default settings, gets a 403 AccessDenied on the authorization check. The request itself does not fail. The model grounds its answer in Search and cached Fetch anyway, then tells you it couldn’t reach external web access. You get a working answer with a scary log line, which is the worst possible combination for a debugging session at 2 AM.

Set the field explicitly:

tools=[{"type": "web_search", "external_web_access": False}]

That configuration needs no extra permission, keeps retrieval inside the Amazon Bedrock web index and cache, and keeps request data inside the AWS boundary. It’s also the only configuration that behaves consistently today, because AWS states that only indexed-web retrieval is currently served. Live-web retrieval is described as a future update, with the parameter already present so your code won’t have to change when it lands.

Three permissions, three different jobs:

IAM action What it unlocks Granted by AmazonBedrockFullAccess
bedrock-websearch:InvokeSearch Titles, URLs, and snippets from Amazon’s index and knowledge graph Yes
bedrock-websearch:InvokeFetch Cached full page content for a specific URL Yes
bedrock-websearch:ExternalWebAccess Retrieval that may reach the live external web (future behavior) No

Source: Amazon Bedrock User Guide, Web Search. If InvokeSearch is denied outright, AWS says Web Search is effectively disabled and the model answers from training data instead. That’s a silent quality regression, not an error, so alarm on it. If you already enforce residency boundaries with the SCP-based zero-data-retention patterns, add bedrock-websearch:ExternalWebAccess to the explicit deny list now rather than after live retrieval ships.

Citations come back structured, and you are contractually required to show them

This is the part that genuinely beats a hand-rolled tool. Getting citations right in a custom loop is tedious: you have to keep a map from result index to URL, prompt the model to emit reference markers, then post-process the markers back into links, and it drifts constantly.

Bedrock returns character offsets. Each citation is a url_citation annotation attached to the output text:

{
  "type": "url_citation",
  "start_index": 120,
  "end_index": 303,
  "title": "Top announcements of AWS re:Invent 2025 | AWS News Blog",
  "url": "https://aws.amazon.com/blogs/aws/top-announcements-of-aws-reinvent-2025"
}

start_index and end_index point into output_text, so you can render inline footnotes or highlight the exact span a source supports. In raw JSON the annotations live at output[].content[].annotations[]. Streaming works too: text arrives as response.output_text.delta events and each citation arrives as a response.output_text.annotation.added event as the model grounds a statement.

for item in response.output:
    if item.type == "message":
        for block in item.content:
            if block.type == "output_text":
                for ann in block.annotations or []:
                    if ann.type == "url_citation":
                        print(f"- {ann.title}: {ann.url}")

You should also surface the retrieval steps, because they tell you whether grounding actually happened. Items of type web_search_call carry an action, which is either a search with a list of queries or an open_page with a url. Zero web_search_call items means the model answered from parametric memory, and you should decide whether that’s acceptable for the query class before you ship it.

Now the part teams skip. The acceptable use terms in the Web Search documentation are not advisory: you must retain and display the source citations and links provided in model outputs in any output you surface to your end users. You also may not extract, store, or reproduce Search Results in bulk, and you may not use them to build or populate a competing index or database.

Read that against your caching strategy. A team that stores grounded answers plus retrieved snippets in DynamoDB to avoid paying $12 per 1,000 queries twice for the same question is somewhere in the gray zone between “response cache” and “bulk storage of search results.” I’d get that reviewed rather than assume. A safer pattern is caching the final generated answer and its citation list, and re-querying when the answer ages past your freshness window.

The numbers, and why $12 per 1,000 queries reframes your model choice

The price cut on July 30, 2026 makes this arithmetic interesting. AWS reduced on-demand Bedrock prices for GPT-5.6 Luna by 80% and GPT-5.6 Terra by 20%, matching OpenAI’s first-party change, with Sol unchanged and no customer action required. Current in-region on-demand prices per million tokens, from the Bedrock pricing page for US East (N. Virginia) and US East (Ohio):

Model Input (272K ctx) Output (272K ctx) Cache read (272K) Input (1M ctx) Output (1M ctx) Cache read (1M)
GPT-5.6 Sol $5.50 $33.00 $0.55 $11.00 $49.50 $1.10
GPT-5.6 Terra $2.20 $13.20 $0.22 $4.40 $19.80 $0.44
GPT-5.6 Luna $0.22 $1.32 $0.022 $0.44 $1.98 $0.044
GPT-5.5 $5.50 $33.00 $0.55 n/a n/a n/a
GPT-5.4 $2.75 $16.50 $0.275 n/a n/a n/a

Source: Amazon Bedrock pricing, OpenAI Frontier Models, in-region on-demand, retrieved August 5, 2026. Crossing into the 1M context window doubles the per-token rate. That is a cliff, not a ramp.

Note the delta against first-party. OpenAI’s own price-performance announcement lists Luna at $0.20 in and $1.20 out, and Terra at $2 and $12. Bedrock lists $0.22/$1.32 and $2.20/$13.20. That’s a consistent 10% premium for in-region inference under AWS controls. AWS notes in-region inference is priced at parity with OpenAI’s data residency tier, so you’re comparing against the residency SKU, not the base rate.

Now combine that with $12 per 1,000 queries. Take a support agent that answers one grounded question. AWS does not publish the token footprint of injected search observations, so the retrieval token counts below are my estimate, not a documented figure: assume two search steps returning about 4,000 tokens of snippets, titles, and URLs total, on top of an 800-token prompt, producing a 700-token answer.

Model Input cost (4,800 tok) Output cost (700 tok) Inference subtotal Web Search (2 queries) Total Search share
GPT-5.6 Luna $0.00106 $0.00092 $0.00198 $0.02400 $0.02598 92%
GPT-5.6 Terra $0.01056 $0.00924 $0.01980 $0.02400 $0.04380 55%
GPT-5.6 Sol $0.02640 $0.02310 $0.04950 $0.02400 $0.07350 33%

Token rates from the Bedrock pricing page; Web Search at $12.00 per 1,000 queries. Token volumes are an illustrative estimate.

Read the last column again. On Luna, the search fee is roughly twelve times the inference cost. The 80% price cut that made Luna the obvious choice for high-volume classification does almost nothing for a grounded workload, because you moved the cost from tokens to queries. Optimizing the model tier is now the wrong lever. Optimizing how often the model searches is the right one.

At 100,000 grounded requests per month averaging two searches each, that’s 200,000 queries and $2,400 in Web Search charges alone, before a single token is billed. Tag it. The granular cost attribution setup for Bedrock matters more here than for plain inference, because search charges will show up as a line item nobody forecast.

Two more cost mechanics deserve attention. First, Fetch pulls cached full page content, which is far heavier than a snippet. Three fetched pages at roughly 8,000 tokens each is 24,000 input tokens, which on Terra’s short-context rate is $0.0528 for that request alone. Second, prompt caching does not rescue you. Cache reads are cheap, at $0.22 per million for Terra, but retrieved web content changes every turn, so only your stable system-prompt prefix will ever hit the cache. The expensive part of a grounded request is the part that can’t be cached.

The comparison the announcement doesn’t make

AWS shipped Web Search on Bedrock AgentCore first, back on June 17, 2026, and the AWS News Blog states its price plainly: $7 per 1,000 queries, generally available in US East (N. Virginia). Same Amazon index. Same knowledge graph. Same zero-egress story. Reached through an AgentCore Gateway MCP connector target instead of a model API parameter.

Option Price per 1,000 queries Regions Integration Source control
Web Search on Bedrock (built-in tool) $12.00 us-east-1, us-east-2, us-west-2 One field in the Responses API tools array None documented
Web Search on Bedrock AgentCore $7.00 us-east-1 MCP connector target on AgentCore Gateway None documented
Bedrock Managed Knowledge Bases $1.00 per 1,000 Retrieve calls, plus $5.00 per GB/month index storage Per Bedrock regional availability Retrieve API or Gateway Total. It’s your corpus
Brave Search API (custom tool) $5.00 per 1,000 requests Vendor endpoint, not in-Region Your own client-side tool loop Goggles for reranking and domain filtering

Sources: Amazon Bedrock pricing for Web Search and Knowledge Bases; AWS News Blog on AgentCore Web Search for the $7 figure; Brave Search API pricing for the $5 Search plan at 50 queries per second.

You’re paying a 71% premium over the AgentCore path for the convenience of not registering a gateway target. On a 200,000-query month that’s $1,000. Whether that’s worth it depends on how much orchestration you already run: if you have an AgentCore Gateway deployed, the cheaper path is already built. If you don’t, $1,000 a month is a poor reason to stand one up.

The Brave comparison is uncomfortable in the other direction. It’s cheaper per query, its index is documented at over 30 billion pages with more than 100 million page updates a day, and it ships Goggles, a feature for custom reranking and result filtering. AWS describes its own index as spanning tens of billions of documents refreshed continually, which is the same order of magnitude stated less precisely. What Brave can’t offer is retrieval that never leaves your AWS account.

For scale context: Exa published on August 3, 2026 that it serves 80 billion pages and tracks 1.4 trillion URLs, estimating Google at roughly 1 trillion pages, Bing at 500 billion, and Yandex at 200 billion. That tweet drew over 813,000 views. Against those figures, “tens of billions” puts Amazon’s index in the same tier as the independent players and one to two orders of magnitude below Google. For AWS documentation, release notes, and mainstream news, that’s plenty. For obscure long-tail sources, test before you commit.

Failure modes, ranked by how badly they’ll hurt

Prompt injection from fetched content is the top risk, and it’s structurally worse here. The model reads content from arbitrary web pages and treats it as context. A page can contain text engineered to look like instructions. In a custom tool loop you at least had a chokepoint: your handler saw the raw text and could strip suspicious patterns, cap length, or reject a domain before the model ever tokenized it. With a server-side built-in tool, retrieval and generation happen in the same call. There is no interception point.

Your remaining defenses are the ones outside the model. Bedrock Guardrails has a dedicated prompt attack filter priced at $0.08 per 1,000 text units through the InvokeGuardrailChecks API, where a text unit is up to 1,000 characters. Contextual grounding checks run $0.10 per 1,000 text units. Neither is free, and neither inspects the retrieved snippet before the model sees it, so treat them as output-side controls. The Bedrock trust and safety production checklist covers the layering; the short version for this feature is that any agent with tool-calling authority and web-sourced context needs its high-impact actions gated behind something other than model judgment.

No source allowlist is the limitation that will block regulated adopters. I found no documented parameter for restricting retrieval to a domain list, excluding domains, or biasing toward authoritative sources. If your compliance posture requires that a customer-facing answer be grounded only in your documentation, a vendor knowledge base, and two regulator sites, this tool cannot express that constraint today. Brave exposes Goggles for exactly this. Bedrock Web Search, as documented on August 5, 2026, does not. If AWS has an undocumented filter, I couldn’t find it, and I’m not going to guess at a parameter name.

Staleness is real but bounded. Retrieval today is served from the Amazon Bedrock web index and cache, described as a snapshot of web content hosted inside AWS rather than a live fetch at request time. AWS says the index is refreshed continually but publishes no freshness SLA, no maximum index lag, and no cache TTL. That is a gap you should measure rather than assume. Query for something you published in the last 24 hours and see whether it comes back. For a “what happened this morning” use case, indexed-web retrieval may simply not be current enough, and live-web retrieval is not available yet.

Silent degradation to parametric answers. When results don’t support an answer, AWS says the model tells you rather than filling the gap from training data. Good behavior, but “the model tells you” is prose in the response body, not a status code. Detect it structurally: count web_search_call items, count url_citation annotations, and treat zero citations on a query you expected to ground as a failed response in your metrics.

Latency is undocumented. AWS repeatedly says retrieval is fast and low-latency. No milliseconds anywhere. What you can reason about: a single round-trip that internally performs one to several searches plus optional page fetches will have higher and more variable time-to-first-token than an ungrounded call, and you cannot parallelize the retrieval yourself because you don’t control it. Stream the response and measure p50 and p99 against your own traffic before you put this in a synchronous user path. Anyone quoting a latency figure for this feature today is guessing.

The decision framework

Three options, and they are not interchangeable. Pick on the shape of the question your agent answers, not on price.

Use Bedrock Knowledge Bases or your own RAG when the authoritative answer lives in your corpus. Product documentation, contracts, runbooks, ticket history, internal wikis. You control chunking, you control the source set, retrieval is auditable, and at $1.00 per 1,000 Retrieve calls plus $5.00 per GB per month it’s the cheapest per query by a wide margin. The tuning work is real, which is why the hybrid RAG patterns combining Bedrock with OpenSearch exist, but you get precision and provenance that no web index can give you about your own business.

Use built-in Web Search when the answer is public, recent, and unowned. Current pricing from a vendor, a library’s breaking change last week, a regulatory update, market news. You cannot maintain an index of the open web, and you should not try. If you’re already on the Responses API with GPT-5.6 in a supported Region, adding one tool entry is a genuinely better use of an afternoon than integrating a search vendor.

Use a custom search tool when you need control the built-in tool doesn’t expose. Domain allowlists. A specialized index like PubMed or EDGAR or a legal corpus. Inspecting and sanitizing retrieved text before the model sees it. Sub-$5 per 1,000 economics at high volume. Multi-cloud portability, since the built-in tool ties you to bedrock-mantle. If any of those is a hard requirement, the built-in tool is not a candidate, and the MCP-based tooling patterns for Bedrock agents are the cleaner way to build the alternative.

My actual recommendation for most production agents: run both, and route. Public-recency questions get the built-in tool. Everything about your own product hits your knowledge base. The classifier that decides is cheap, and Luna at $0.22 per million input tokens is close to free for that job. The expensive mistake is letting a $12-per-1,000-queries tool answer questions your own documentation already covers.

Do not use built-in Web Search if you’re outside us-east-1, us-east-2, or us-west-2. Web Search is strictly regional: each Region runs its own search and fetch tier, and queries, fetches, index data, and results are not routed across Regions. There is no cross-Region inference path for this tool. EU and APAC workloads have no option today.

Implementation pattern that holds up

Wrap the call rather than sprinkling tools=[{"type": "web_search"}] across your codebase. You want one place to enforce the region pin, one place to count searches, and one place to fail closed when citations are missing.

import os
from openai import OpenAI
from aws_bedrock_token_generator import provide_token

SUPPORTED_REGIONS = {"us-east-1", "us-east-2", "us-west-2"}
REGION = os.environ["BEDROCK_WEB_SEARCH_REGION"]
if REGION not in SUPPORTED_REGIONS:
    raise RuntimeError(f"Web Search is not available in {REGION}")

client = OpenAI(
    base_url=f"https://bedrock-mantle.{REGION}.api.aws/openai/v1",
    api_key=provide_token(region=REGION),
)


def grounded_answer(question, model="openai.gpt-5.6-terra", require_citations=True):
    resp = client.responses.create(
        model=model,
        input=question,
        tools=[{"type": "web_search", "external_web_access": False}],
    )

    searches = [i for i in resp.output if i.type == "web_search_call"]
    citations = []
    for item in resp.output:
        if item.type != "message":
            continue
        for block in item.content:
            if block.type != "output_text":
                continue
            for ann in block.annotations or []:
                if ann.type == "url_citation":
                    citations.append(
                        {
                            "title": ann.title,
                            "url": ann.url,
                            "span": (ann.start_index, ann.end_index),
                        }
                    )

    # Emit these as metrics. search_count drives cost; citation_count drives trust.
    metrics = {"search_count": len(searches), "citation_count": len(citations)}

    if require_citations and not citations:
        raise ValueError(f"ungrounded response: {metrics}")

    return {"text": resp.output_text, "citations": citations, "metrics": metrics}

Four things that wrapper buys you. The region check fails at import time instead of producing a confusing endpoint error in production. external_web_access is explicit, so nobody inherits the 403 default. search_count becomes a first-class metric, which is your only real cost control at $12 per 1,000 queries. And require_citations turns silent parametric fallback into a caught exception, so your alerting sees quality regressions instead of your users.

For the rendering side, use the character offsets rather than re-parsing the text. Walk the annotations in reverse order by start_index and splice footnote markers in, so earlier offsets stay valid while you mutate the string. Then emit the source list underneath. That satisfies the acceptable-use obligation and it’s a better reading experience than a wall of raw URLs.

Operational reality

The audit story is good, with one documentation conflict you should verify against your own trail before you write it into a control narrative. The launch blog describes Web Search calls as CloudTrail management events. The User Guide describes them as data events, which normally means you must explicitly enable data event logging for the resource type. Those are materially different operationally, and I could not reconcile them from the published docs. Turn on the tool in a sandbox account, run one search, and look at your trail before you assert coverage to an auditor.

What both sources agree on is more important. CloudTrail records the calling identity, timestamp, action, source identity including any forward-access-session originator, and the account and Region context. Access-denied outcomes are always logged, and each AccessDeniedException event includes the specific condition key that caused the denial, which makes IAM misconfiguration genuinely easy to diagnose. By design, CloudTrail does not record the query text, the URLs returned by search, or the raw page content retrieved by fetch. Query text is treated like an inference prompt.

That last point cuts both ways. Your security team gets a complete record of who used the tool and when, with no exposure of what users searched for. Your incident responders, investigating why the agent produced a wrong answer last Tuesday, get nothing about what it actually retrieved. If you need that, log it yourself from the web_search_call items and the citation annotations in your own application telemetry, with whatever retention and redaction your policy requires.

Two more operational notes. Guardrails still apply to the model interaction, and if you route Bedrock traffic through a central account, the cross-account Guardrails configuration is worth revisiting now that a new tool can inject third-party text into prompts. And on model access: teams already running OpenAI models on Bedrock through the Codex managed agents path will find the endpoint and token mechanics familiar, since it’s the same bedrock-mantle surface.

Depending on the model, your data is also subject to Amazon Bedrock’s automated abuse detection mechanisms, which the Web Search documentation calls out explicitly under data governance. Worth reading before you assume the zero-egress claim covers every data path.

What to take away

Web Search on Bedrock is the right default for public-recency grounding if you’re already on the Responses API in one of three US Regions, and the wrong tool the moment you need a source allowlist or a corpus you own. The number to watch isn’t the token price that just dropped 80% on Luna. It’s the $12 per 1,000 queries, which on the cheapest model tier costs roughly twelve times the inference it grounds. Instrument search count from day one, or your first grounded feature will teach you that lesson through a bill.

Cleber Rodrigues

Cleber Rodrigues

AWS Enthusiast | Cloud Architect | AWS Certified Solutions Architect – Professional

Comments

comments powered by Disqus