Skip to content

Fixing AccessDeniedException Calling Bedrock From Lambda

10 min read · updated August 11, 2026

Two completely different failures surface as AccessDeniedException from InvokeModel, and the fixes do not overlap at all. The message text tells you which one you have, which is why the first thing to do is read it rather than start adding permissions.

Read the string before you change anything

The IAM authorisation failure names a principal and a resource:

botocore.errorfactory.AccessDeniedException: An error occurred
(AccessDeniedException) when calling the InvokeModel operation:
User: arn:aws:sts::111122223333:assumed-role/my-fn-role/my-fn is not
authorized to perform: bedrock:InvokeModel on resource:
arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0

The model-agreement failure does not. It has no principal ARN in it, because the request was authorised and then refused on a different ground — the account has no agreement for that model in that Region. Its wording is about the account and the model, not about a user and an action. That single structural difference is the fastest diagnostic available: if the message contains the words “is not authorized to perform” followed by an ARN, it is IAM. If it does not, stop editing policies.

There is also a timing case that produces the second form transiently. Amazon documents that the first invocation of a third-party model in an account starts a subscription in the background, that this can take up to fifteen minutes to finalise, and that after the necessary permissions are in place it can take up to two more minutes for the subscription to complete — during which calls keep returning AccessDeniedException. A denial that disappears on its own a few minutes after you touched nothing was this.

Cause one: the execution role

When the message does name a principal, look at the principal, not at the policy. The ARN in the error is what actually signed the request, and on Lambda it is an assumed-role ARN of the form arn:aws:sts::account:assumed-role/<role-name>/<function-name>. Three things go wrong here in roughly this order of frequency:

  • The role in the error is not the role you edited. Repeated deployments of an IaC stack can leave a function pointing at a previous role. Compare the name in the exception against aws lambda get-function-configuration --function-name X.
  • The resource ARN does not match. This is the inference-profile case. The policy names a foundation model, the code passes a profile ARN as modelId, and the two are distinct IAM resources — you need a statement for each. The ARN printed in the error is the one to add.
  • The action is wrong for the call. A policy granting only bedrock:InvokeModel denies bedrock:InvokeModelWithResponseStream, so the failure appears the day someone turns streaming on and nothing else changed. bedrock:InvokeModel* covers all four invocation actions.

Cause two: something above the role is denying

If the identity policy plainly grants the action and the call still fails, an explicit Deny is winning somewhere else. In AWS’s evaluation order any explicit Deny beats every Allow, and there are three places one hides:

  • A service control policy. Organisations frequently deny new services by default, and Amazon’s own model-access guidance recommends exactly this pattern for governing which models may be used — a Deny on bedrock:InvokeModel at the organisation or account level. You will not see it in the account, and you cannot fix it from the account.
  • A permissions boundary on the execution role, which caps what the role can do regardless of the policies attached to it.
  • A VPC endpoint policy. This is the resource policy that genuinely sits in this request path. If the function runs in a VPC and reaches Bedrock through an interface endpoint, the endpoint’s own policy is evaluated too, and a custom one that lists bedrock:InvokeModel but not the streaming action produces the same denial from a role that looks correct. See the endpoint policy section.

aws iam simulate-principal-policy distinguishes the first two: it returns explicitDeny rather than implicitDeny when an SCP or boundary is the cause. It does not evaluate VPC endpoint policies, so if the simulator says allowed and the runtime says denied, the endpoint is the remaining suspect.

Cause three: Region and model agreement

Model access is per-account and per-Region. A Lambda function inherits its Region from AWS_REGION in the execution environment, and the SDK uses that unless you pass region_name explicitly, so a function deployed into eu-west-1 will call eu-west-1 while your console tab — where everything works — is on us-east-1.

import boto3
# Pin it. Do not inherit it from wherever the function happens to live.
client = boto3.client("bedrock-runtime", region_name="us-east-1")

response = client.converse(
    modelId="anthropic.claude-3-haiku-20240307-v1:0",
    messages=[{"role": "user", "content": [{"text": "ping"}]}],
)

To check the agreement rather than guess at it, Amazon exposes GetFoundationModelAvailability. Its response carries agreementAvailability.status, which is AVAILABLE when access exists and NOT_AVAILABLE when it does not, alongside regionAvailability and entitlementAvailability:

aws bedrock get-foundation-model-availability \
  --model-id anthropic.claude-3-haiku-20240307-v1:0 \
  --region us-east-1

One further per-account gate applies to Anthropic models: Amazon documents a one-time use-case form, submitted through the console or the PutUseCaseForModelAccess API, required once per account or once at an organisation’s management account. If your automation creates fresh accounts, that form is the step it will forget.

The order to check in

  1. Read the exception. Principal ARN present means IAM; absent means agreement or Region.
  2. Confirm the role in the error is the role you think you edited, and that the resource ARN in the error appears verbatim in a policy statement.
  3. Run simulate-principal-policy for that exact action and resource. explicitDeny ends the investigation in the account and moves it to the organisation.
  4. Run get-foundation-model-availability in the Region the function actually uses — log AWS_REGION to be certain which that is.
  5. If the function is in a VPC, read the endpoint policy last, because it is the only one of these the simulator cannot see.

Stopping it recurring

Once the immediate denial is cleared, three changes stop the same incident arriving again in a different account or Region.

Do not retry it. AccessDeniedException is a 403 and the AWS SDKs correctly treat it as non-retryable, so if you are seeing three of them per request, something in your code is catching a broad exception class and looping. That turns an instant, legible failure into a slow one and, on a first invocation during the Marketplace subscription window, hammers an endpoint that was about to start working anyway. Catch ClientError, read err.response["Error"]["Code"], and let anything that is not a throttle or a timeout fail immediately.

Log the whole message, once. The default handler in a lot of application code logs str(e) truncated, or replaces it with a friendly string, and the principal ARN and resource ARN — the only two pieces of information that matter here — are exactly what gets thrown away. Log the full exception text at error level, along with the value of AWS_REGION and the modelId the call used. Those three fields answer this page’s first four sections without anyone having to reproduce anything.

Move the check to deploy time. The reason this exception is so common in a fresh environment is that nothing verifies the account’s Bedrock posture until real traffic arrives. GetFoundationModelAvailability is cheap, needs no tokens, and returns a definitive answer, so a post-deploy smoke step that calls it for each model id the stack uses — in the Region the stack is deployed to — converts a production 403 into a failed pipeline stage. Pair it with a single simulate-principal-policy call against the deployed execution role for the same model ARN and you have covered both causes at once, before any user sees either. It is the same reasoning behind deliberately exercising provider failure paths: the cases that only appear in production are the ones worth forcing somewhere else.