Choosing a Foundation Model on Amazon Bedrock: A Practical Comparison
Compare Amazon Bedrock models on your own prompts: a reusable Python harness for quality, latency and cost, plus a test of cross-Region inference profiles.
Series: Amazon Bedrock (13 articles)
- 1Choosing a Foundation Model on Amazon Bedrock: A Practical Comparison This article
How to test several models on your own prompts, compare quality, speed and cost, and check whether cross-Region inference helps when traffic gets heavy.
Leaderboards are a fine place to start, but they won't tell you which model is right for your task. The only way to find out is to run your own prompts, score the answers, and look at what each one costs.
In the previous article we made a first call to Bedrock. This time we'll turn that call into a small test harness you can reuse every time a new model shows up. We'll compare models on quality, latency and cost, and then run a second test to see how regional endpoints and cross-Region inference profiles behave under load.
[Add a sentence or two here about why you needed to choose a model: the project, the task, what mattered most to you.]
What you need
- The setup from the first article: Python, boto3 and working Bedrock credentials
- Access to the 3 or 4 models you want to compare, with their model IDs copied from each model's card
- A current copy of the Amazon Bedrock pricing page open in another tab
Bedrock's catalogue changes often, and older models are retired on a schedule. Pick models marked active, and check the lifecycle status on each card before you begin.
Step 1: Pick your five prompts
Use prompts that look like your real work, not toy examples. Five is enough to see patterns without turning this into a research project. A good spread:
- Summarise a real document of a few paragraphs into three bullet points.
- Extract specific fields from messy text and return them as JSON.
- Reason through a small problem with a few steps, such as a pricing or scheduling question.
- Rewrite a paragraph in a different tone, for example from technical to friendly.
- Explain a short piece of code or a configuration file in plain English.
Write each prompt down once and don't change it between models. If you tweak the wording for one model, you're no longer comparing the models.
Step 2: Build the harness
This script sends every prompt to every model three times, records the latency and token counts that Bedrock returns, and works out a cost for each call.
Python
import csvimport boto3from botocore.exceptions import ClientErrorREGION = "us-east-1" # use your regionclient = boto3.client("bedrock-runtime", region_name=REGION)# Copy exact IDs from each model's card, inference profile prefix included.MODELS = {"Model A": "MODEL_ID_A","Model B": "MODEL_ID_B","Model C": "MODEL_ID_C",}# USD per 1 million tokens: (input, output). Copy from the Bedrock pricing page.PRICES = {"Model A": (0.0, 0.0),"Model B": (0.0, 0.0),"Model C": (0.0, 0.0),}PROMPTS = {"summarise": "Summarise this in three bullet points:\n\n<your text>","extract": "Return JSON with name, date and amount from:\n\n<your text>","reason": "<your reasoning problem>","rewrite": "Rewrite this in a friendly tone:\n\n<your text>","explain": "Explain what this does in plain English:\n\n<your code>",}RUNS = 3def ask(model_id, prompt):return client.converse(modelId=model_id,messages=[{"role": "user", "content": [{"text": prompt}]}],inferenceConfig={"maxTokens": 500, "temperature": 0},)rows = []for model_name, model_id in MODELS.items():price_in, price_out = PRICES[model_name]for prompt_name, prompt in PROMPTS.items():for run in range(1, RUNS + 1):try:r = ask(model_id, prompt)except ClientError as e:rows.append([model_name, prompt_name, run, e.response["Error"]["Code"], "", "", "", ""])continuetext = r["output"]["message"]["content"][0]["text"]tokens_in = r["usage"]["inputTokens"]tokens_out = r["usage"]["outputTokens"]latency = r["metrics"]["latencyMs"]cost = tokens_in / 1e6 * price_in + tokens_out / 1e6 * price_outrows.append([model_name, prompt_name, run, "ok", latency, tokens_in, tokens_out, round(cost, 6)])# Save the answer so you can score it later.with open(f"answers_{model_name}_{prompt_name}_{run}.txt", "w") as f:f.write(text)with open("results.csv", "w", newline="") as f:w = csv.writer(f)w.writerow(["model", "prompt", "run", "status", "latency_ms", "in_tokens", "out_tokens", "cost_usd"])w.writerows(rows)print("Done. See results.csv and the answers_*.txt files.")
A few choices worth explaining. Temperature is set to 0 so the answers are as repeatable as possible. The three runs per prompt smooth out one-off slow responses. And the answers are saved to files, because the numbers only tell half the story.
Step 3: Score the answers
Speed and cost are easy to measure. Quality is the part people skip, and it's the part that matters most.
Open the saved answers and score each one from 1 to 5 on three things: accuracy, following your instructions (format, length, tone) and usefulness without editing. If you can, hide the model names while you score, or ask a colleague to do it. It's surprisingly easy to favour the model you expected to win.
Record your scores in a file called
scores.csv with the columns model,prompt,run,quality , one row per answer you scored. The script in the next step reads it.Step 4: Build your comparison table
Don't fill the table in by hand. Run this short script in the same folder as `results.csv` and `scores.csv`. It reads both files and prints a ready-to-paste table with your own numbers, so the comparison is repeatable every time you test a new model.
Python
import csvimport statisticsfrom collections import defaultdictlatency = defaultdict(list)cost = defaultdict(list)for row in csv.DictReader(open("results.csv")):if row["status"] != "ok":continuelatency[row["model"]].append(float(row["latency_ms"]))cost[row["model"]].append(float(row["cost_usd"]))quality = defaultdict(list)for row in csv.DictReader(open("scores.csv")):quality[row["model"]].append(float(row["quality"]))print("| Model | Average quality (1 to 5) | Median latency (ms) | Cost per 100 calls (USD) |")print("|---|---|---|---|")for model in latency:q = f"{statistics.mean(quality[model]):.1f}" if quality[model] else ""print(f"| {model} | {q} | {statistics.median(latency[model]):.0f} | {statistics.mean(cost[model]) * 100:.2f} |")
The output is a table with one row per model. Here's what each column means and how to read it:
Column | Where it comes from | How to read it |
|---|---|---|
Average quality (1 to 5) | Your scores in scores.csv | Higher is better. A 4 is often good enough, so don't pay for a 5 you don't need. |
Median latency (ms) | The latencyMs value Bedrock returns with every call | Lower is faster. The median ignores the odd slow outlier. |
Cost per 100 calls (USD) | Token counts multiplied by the prices you entered | Lower is cheaper. Judge it next to quality, never on its own. |
If a model shows far fewer rows than the others, check the
status column in results.csv . Failed calls usually point to a permission, model ID or throttling problem to fix before you compare.Look for the model that's good enough, not the one that's best on paper. A cheaper, faster model that scores a 4 often beats a premium one that scores a 5 when you're running thousands of calls a day.
Step 5: Regional endpoint or cross-Region inference?
Quality and cost are one question. The other is what happens when traffic gets busy.
By default you call a model in a single Region, and your requests are limited by that Region's quotas. Cross-Region inference lets Bedrock route requests across several Regions to get more throughput. It works through inference profiles, and there are two kinds:
- Geographic profiles keep routing inside a geography such as the US or EU. Choose these when you have data residency requirements.
- Global profiles can route to supported commercial Regions worldwide, and the AWS documentation describes them as giving the highest available throughput.
Here's how the three options compare:
| Regional model ID | Geographic profile | Global profile |
|---|---|---|---|
Where requests can run | One Region | Within a geography, such as the US or EU | Supported commercial Regions worldwide |
Throughput | Limited by that Region's quotas | Higher than a single Region | Highest available |
Data residency | Stays in the Region | Stays within the geography | May be processed outside your home Region |
Model ID looks like | The plain model ID | Has a geography prefix, such as us. | Has the global. prefix |
Choose it when | Traffic is low and simple is fine | You need more throughput and have residency rules | You need maximum throughput and have no residency limits |
To test it, call the same model twice: once with the regional model ID, and once with the inference profile ID (the one with a prefix such as
us. or global. , copied from the model's card). Then send a small burst of requests and count what comes back.Python
from concurrent.futures import ThreadPoolExecutordef burst(model_id, n=20, workers=10):def one(_):try:ask(model_id, "Say hello in five words.")return "ok"except ClientError as e:return e.response["Error"]["Code"]with ThreadPoolExecutor(max_workers=workers) as pool:results = list(pool.map(one, range(n)))return {code: results.count(code) for code in set(results)}print("Regional:", burst("REGIONAL_MODEL_ID"))print("Profile: ", burst("PROFILE_MODEL_ID"))
If you see
ThrottlingException counts on one side and not the other, that's your answer for your quota and your load. Keep the burst small, since it costs money and counts against your quotas, and your results will depend on your account's limits.Things to check before you rely on a profile:
- Permissions. Your IAM policy (and any service control policies) must allow the inference actions the profile uses, including in the destination Regions.
- Data residency. A global profile may process requests outside your home Region. If that matters for your data, use a geographic profile or the regional ID.
- Pricing. Check the pricing page for the profile you choose. It's not always identical to the regional price.
- Quotas. Cross-Region quotas are listed separately in Service Quotas, so look up the right ones for your model.
What to take away
Choose a model by running your own prompts and scoring the answers, not by reputation. Keep the harness and the summary script in a repository so you can rerun the same comparison whenever a new model arrives. And if you're going to see real traffic, test the inference profile path before launch rather than after your first throttling error.
Next in the series, we'll add guardrails to a Bedrock app and test how well they hold up.
Model availability, IDs, quotas and pricing change often. Check the current Amazon Bedrock documentation before you follow any step here.
Series: Amazon Bedrock (13 articles)
- 1Choosing a Foundation Model on Amazon Bedrock: A Practical Comparison This article
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article