The Amazon Bedrock pricing page publishes a training price for the Meta model you almost certainly are not fine-tuning.
Here is the customization pricing on the page, as of 8 September 2026:
| Model | Published training price |
|---|---|
| Llama 2 Pretrained 13B | $1.49 per 1M tokens |
| Llama 2 Pretrained 70B | $7.99 per 1M tokens |
| Cohere Command | $0.004 per 1,000 tokens |
| Titan Image Generator | $0.005 per image seen |
| gpt-oss-20b | $80 per training hour |
| Qwen3 32B | $80 per training hour |
| Llama 3.1, Llama 3.3 | none |
Other providers are covered. Two models are even priced by the hour. The only Meta model with a published training price is Llama 2, and the Llama you would actually fine-tune today is not on the list.
I have a fine-tuned Llama 3.3 70B running in production on Bedrock, and a line item on my AWS bill saying it cost real money to train. So the capability exists, it bills, and I could not find its price published anywhere.
A caution while you are here: I have seen third-party guides quote "$0.00799 per 1,000 tokens" as the Llama 3.3 70B training rate. That is the Llama 2 70B figure restated and applied to a different model. If you are budgeting, price it from your own first job rather than from a number someone inferred.
That gap turned out to be the least surprising thing I learned. This article is the map I wish I had before I started: which regions can actually do this, what the documented way around it says versus what happened when I took it, and the one piece of job state that decides whether a stalled job is costing you money or nothing at all.
Everything below is either something I measured or something AWS documents, and I have labelled which. Where the two disagree, you get both.
I did not want to work from a documentation table. The Bedrock catalogue varies by region more than people expect, and a page describing 33 regions is a hard thing to keep current. So I asked the API directly, in every region the SDK knows about.
import boto3, concurrent.futures as cf
from botocore.config import Config
cfg = Config(connect_timeout=6, read_timeout=12, retries={"max_attempts": 1})
regions = boto3.Session().get_available_regions("bedrock")
def probe(region):
try:
client = boto3.client("bedrock", region_name=region, config=cfg)
models = client.list_foundation_models(
byCustomizationType="FINE_TUNING"
)["modelSummaries"]
ids = sorted({m["modelId"].split(":")[0] for m in models})
return region, len(ids), ids
except Exception as e:
return region, "ERR " + type(e).__name__, []
with cf.ThreadPoolExecutor(max_workers=12) as ex:
for region, count, ids in sorted(ex.map(probe, regions)):
print(region, count, any("llama3-3-70b" in i for i in ids))
byCustomizationType="FINE_TUNING" is the important part. It returns only the models you can actually train, not the much longer list you can invoke.
33 regions. Here is what came back.
| Result | Regions |
|---|---|
| Can fine-tune Llama 3.3 70B | 1 (us-west-2) |
| Answered, some tunable models, no Llama | 2 (us-east-1, eu-west-2) |
| Answered, zero tunable models | 17 |
| Opt-in regions, not enabled on my account | 13 |
One region can fine-tune Llama 3.3 70B. Out of the 33 the SDK lists, and out of the 20 that gave me a real answer. Not a short list. One.
An honest caveat, because the difference matters. Those 13 are not a fine-tuning answer. They returned UnrecognizedClientException, which is what you get when you call an opt-in region your account has never enabled. I cannot rule them out from this account, and neither can you from yours unless you have opted in. Twenty regions gave a real answer.
The two regions that answered with something other than zero are more interesting than the seventeen that answered zero.
us-west-2, five tunable models:
amazon.titan-embed-image-v1
anthropic.claude-3-haiku-20240307-v1
meta.llama3-1-8b-instruct-v1
meta.llama3-1-70b-instruct-v1
meta.llama3-3-70b-instruct-v1
us-east-1, six tunable models:
amazon.nova-micro-v1
amazon.nova-lite-v1
amazon.nova-pro-v1
amazon.nova-2-lite-v1
amazon.nova-canvas-v1
amazon.titan-embed-image-v1
Read that second list again. In the region most people default to, every model you can fine-tune is an Amazon model. No Llama. No Anthropic. If you want to customise anything that is not Amazon's own, us-east-1 cannot do it.
The console agrees. Opening Create Fine-tuning job in us-east-1 on 8 September 2026, the model picker offers one category, "Serverless model providers", and one provider under it: Amazon. The six models it lists are the same six the API returns.
And eu-west-2, London, where a lot of UK and EU workloads want to sit for latency or residency reasons, has exactly one tunable model. It is not a Llama.
This has been stable. I first ran the sweep on 1 August 2026 and re-ran it on 8 September 2026 before publishing. Same answer both times. That is five weeks, not five years, so re-run it yourself rather than trusting my table. The snippet above takes about a minute.
The obvious way out is to train the model somewhere else and bring the weights in. Bedrock has Custom Model Import for exactly that, and on paper it fits.
The documentation is clear and it is worth quoting accurately, because my experience differed from it and I want you to have both.
Custom Model Import lists Llama 3.3 among its supported architectures. It states the ceiling in terms of weight size rather than parameter count: under 200GB for text models, with a maximum context length below 128K. It is available in eu-central-1, us-east-1, us-east-2 and us-west-2, which is four regions to native customization's one. AWS has published a walkthrough of importing DeepSeek-R1-Distill-Llama-70B through it.
By that description, importing a fine-tuned Llama 3.3 70B should work. My artifacts were 141GB in bf16, comfortably inside the documented 200GB.
The import failed:
too large to fit on available hardware
That is the whole error. It does not say which limit was hit, whether the constraint was my artifacts, the region, or capacity at that moment. Searching it turns up other people receiving the same generic message on far smaller models, including an 8B, which suggests it is a catch-all rather than a statement about 70B specifically.
So the honest position is narrower than "it cannot be done." The documented path supports the architecture and the size. It did not complete for me, on my artifacts, in my account, on the day I tried, and the error was not specific enough to tell me why. Someone else may well succeed with it, and AWS's own material suggests they do.
What that means practically, if you are planning:
For my own build, native customization is what worked, so that is where the model lives, and the application runs in a different region from the custom model deployment. To be precise about which half of that was forced: the customization and its deployment had to be in us-west-2. Where the application runs was my choice, and I could move it.
This is the part I have not found documented anywhere, and it is the one that costs money.
Launch a customization job and poll it. The status reads InProgress.
That tells you nothing useful, because InProgress covers two states that are not remotely the same:
From the outside these look identical. Same status, same API, same console. The only thing that separates them is nested one level down:
job = bedrock.get_model_customization_job(jobIdentifier=job_id)
job["status"]
job["statusDetails"]["trainingDetails"]["status"]
statusDetails.trainingDetails.status is the only honest signal. Check it before you stop anything.
Why it matters in cash terms. Over one week of capacity contention I stopped five jobs. Four had never left the queue.
| Jobs stopped | Reached the trainer | Billed |
|---|---|---|
| 4 | No | $0.00 |
| 1 | Yes | $29.89 |
Four jobs, each killed after many hours, cost nothing at all. Queue time is free. Had I believed the top-level status, I would have assumed all five cost me something and drawn exactly the wrong conclusion about how expensive iteration is.
This is consistent with what AWS documents, once you read it precisely. The guidance on stopping a job says Bedrock charges for the tokens it used to train the model before you stopped it. If training never started, no tokens were used, and there is nothing to charge for. The documentation and the invoice agree. What neither tells you is which of the two states your job is in, and that is the gap trainingDetails fills.
One more thing that trips people up here: validation completing does not mean training has started. Validation proves your S3 permissions and your data schema are fine. It says nothing about whether a GPU has been allocated.
Here is the mistake, because it is more useful than the finding.
Jobs were stalling in the queue. I went back through my own job history and worked out that the longest queue wait ever followed by successful training was about 11 hours. So I adopted a rule: past 11 hours the job is hung, kill it and relaunch.
That rule was garbage, and it took me a while to see why.
Every job in my history that had waited longer than 11 hours had been killed by me before it could recover. The number did not measure Bedrock's behaviour. It measured my patience. I then used it to justify killing the next job, which fed the same number back into the same conclusion.
The data point that broke it: one job waited 23 hours and 15 minutes in the queue, then trained normally and produced a working model.
An earlier job had been stopped at 17 hours 31 minutes on the strength of a threshold that did not exist. It cost $0.00, because it had never left the queue, but it cost most of a day.
The queue has no published upper bound that I could find. The documented threshold applies to a different state. AWS's troubleshooting guidance says training time runs "between 3-4 hours, up to 24 hours, depending on configuration and traffic", and that you should contact Support if a job has been in Training for more than 24 hours. In Training, not in queue. The phrase "depending on configuration and traffic" is doing real work there: capacity is shared, and the wait before training is not the thing being bounded.
The correct response to a stalled customization job, given that queue time is free and every relaunch goes to the back of the queue: wait. Impatience has a real cost here. Patience has none.
Four more, briefly, all learned the expensive way.
The schema is per model family. The limits are not. Llama 3.3 takes the Converse format, with schemaVersion, system[] and messages[]. Llama 3.1 rejects it outright, with ValidationException: Unable to parse S3 file due to invalid data schema/format, and wants flat {"prompt": ..., "completion": ...} instead. Both share the same 16,000 token ceiling, the same 100 to 10,000 record range, the same epoch and learning rate bounds. Matching limits are not matching schemas, and I lost a launch to assuming otherwise.
prompt/completion has no system field. Move from 3.3 to 3.1 and your system prompt has to be prepended into every training record, then reproduced byte for byte at inference. Get one character wrong later and the model quietly degrades, with no error anywhere.
Your real concurrency limit may be smaller than you think. The quota that binds is Custom models with a creating status per account, which on my account is 2. Not the scheduled-customization-jobs quota, which on my account is 10 and looks like the constraint until you hit the other one. Check both in Service Quotas rather than assuming my numbers are the defaults.
Storage is per model per month. On my bill, a retained rollback model comes to about $1.95 a month to sit there. Cheap, and worth it for the rollback, but not free, and easy to accumulate.
If you are considering fine-tuning a Llama on Bedrock, the honest version is short.
trainingDetails, never the top-level status,
None of it sits in one place, which is why it is here. The pricing page still says Llama 2.