Common AWS errors and how to fix them
71 problems, each with the exact error text, what it means, the likely causes (most likely first, each with a check that proves it and the fix for it) and how to prevent it. Then learn it for good: break and fix each one yourself in the browser playground. To search by pasting an error, use the interactive version.
ExpiredToken: the security token included in the request is expired
Error text
An error occurred (ExpiredToken) when calling the GetCallerIdentity operation: The security token included in the request is expired
What it means
The CLI sent temporary credentials (an access key, a secret and a session token from assume-role, SSO or an instance role) and AWS checked their expiry time: it has passed. GetCallerIdentity in the message is only the call that happened to fail; any call fails the same way until the CLI uses fresh credentials.
Causes, most likely first
Old AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN are still set in the shell. The CLI uses environment variables before your profile, so it keeps sending the expired copy even after you log in again.
Check
aws configure list # if access_key shows Type env, the AWS_* variables are being used, not your profile env | grep -o '^AWS_[A-Z_]*' # names only, never values: AWS_SESSION_TOKEN listed means pasted session credentials are setFix
Unset them:
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN, then runaws sts get-caller-identityagain.The SSO session behind the profile has ended. The role credentials the CLI cached from it expire, and it cannot get new ones until you sign in again.
Check
aws configure list # Type sso or a profile with sso_session points here aws sts get-caller-identity --profile <profile> # still fails with ExpiredToken or an SSO token errorFix
Sign in again:
aws sso login --profile <profile>.A long job outlived its role session. Credentials from assume-role last one hour by default, and a script that fetched them once at the start cannot renew them.
Check
aws iam get-role --role-name <role> --query Role.MaxSessionDuration # 3600 means one hour, the defaultFix
Let the SDK refresh credentials itself from a profile with
role_arnandsource_profile, or from the instance or task role, instead of pasting keys. If the job really needs longer sessions, raise the role's MaxSessionDuration (up to 12 hours).
Prevent it
- Use profiles with SSO or role_arn, not pasted session variables
- Raise the role's MaxSessionDuration if jobs legitimately run longer
Now learn it: break and fix it yourself in ExpiredToken, then take Level 00 · Accounts, Regions and the CLI. Open in the site
InvalidClientTokenId: the security token included in the request is invalid
Error text
An error occurred (InvalidClientTokenId) when calling the GetCallerIdentity operation: The security token included in the request is invalid.
An error occurred (InvalidAccessKeyId) when calling the ListBuckets operation: The AWS Access Key Id you provided does not exist in our records.
What it means
AWS does not recognise the access key ID the CLI sent. It was deleted, deactivated or mistyped, or it is a temporary key sent without its session token, so AWS has no matching credentials and never gets as far as checking permissions.
Causes, most likely first
The key was deleted or set to Inactive. Rotating or disabling a key in IAM invalidates every copy of it at once.
Check
aws configure list # note the last four characters of access_key, and where it comes from aws iam list-access-keys --user-name <user> # run with other, working credentials: if that key is missing or Inactive, this is your causeFix
Use a profile with a valid key (
--profile), or create a new key for the user and replace the old one everywhere it was copied.A temporary key (it starts with ASIA) was sent without AWS_SESSION_TOKEN. Temporary keys are only valid together with their token, so AWS treats the key alone as unknown.
Check
[ -n "$AWS_SESSION_TOKEN" ] && echo token set || echo no token # a key starting ASIA with no token is this causeFix
Set AWS_SESSION_TOKEN with the key and secret from the same assume-role or SSO output, or better, use a profile that fetches all three itself.
The call goes to an opt-in Region (such as af-south-1 or ap-east-1) that is not enabled for the account, or uses there a session token from the global STS endpoint. By default, global STS tokens are valid only in Regions that are enabled by default.
Check
aws account list-regions --region-opt-status-contains ENABLED DISABLED --query 'Regions[].[RegionName,RegionOptStatus]' # the Region you call shows DISABLEDFix
Enable the Region for the account, or get session credentials from that Region's own STS endpoint (
--region <region>with regional endpoints).
Prevent it
- Prefer roles and SSO to long-lived access keys
Now learn it: break and fix it yourself in Root user access keys, then take Level 00 · Accounts, Regions and the CLI. Open in the site
SignatureDoesNotMatch
Error text
An error occurred (SignatureDoesNotMatch) when calling the GetCallerIdentity operation: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.
What it means
The access key ID exists, but the signature on the request does not match the one AWS calculated with that key's secret. Either the secret is wrong, or the request changed after it was signed. A wrong clock gives Signature expired or RequestTimeTooSkewed instead, not this error.
Causes, most likely first
The secret access key was copied with a missing or extra character, or with a trailing space or newline from a file or variable. A secret that differs by one character signs every request differently.
Check
aws configure list # the key ID (last four characters) and where it comes from aws configure get aws_secret_access_key --profile <profile> | tr -d '\n' | wc -c # length only: a secret access key is 40 characters; anything else means it was mangledFix
Enter the secret again with
aws configure set aws_secret_access_key ..., with no quotes or spaces, or create a new key pair.The secret belongs to a different key than this key ID, for example two variables filled from different places. AWS looks up the secret by the key ID, so the wrong secret never matches.
Check
aws configure list # access_key and secret_key should come from the same Type and LocationFix
Take the key ID and the secret from the same key, from one place: one profile, or one pair of variables.
A presigned S3 URL is used with a different method, headers or Content-Type than it was signed for. Those are part of what was signed, so changing them breaks the signature.
Check
curl -sS -o /dev/null -w '%{http_code}\n' '<presigned-url>' # works with a plain GET but fails from your code: the code adds or changes a signed headerFix
Send exactly the method and headers the URL was signed with, or sign it again with the headers your code sends.
A proxy or tool changes the request after it was signed (headers, path encoding). AWS checks the request it receives, not the one your client signed.
Check
env | grep -io '^\(https\?\|no\)_proxy' # names only: a proxy variable set means requests go through it; try once without itFix
Exclude AWS endpoints from the proxy with NO_PROXY, or make it pass requests through unchanged.
Prevent it
- Use SSO or roles so no secret is copied by hand
Now learn it: take Level 00 · Accounts, Regions and the CLI. Open in the site
Unable to locate credentials
Error text
Unable to locate credentials. You can configure credentials by running "aws configure".
What it means
The CLI or SDK looked through its whole credential chain (command-line flags, AWS_* environment variables, the shared config and credentials files, then the container and instance roles) and found nothing to sign the request with. No request was sent to AWS.
Causes, most likely first
No profile is configured on this machine, or the job runs as another user or in a container with a different home directory. The files in
~/.awsare per user, so a cron job or a container does not see yours.Check
aws configure list-profiles # an empty list means no profiles for this user echo "$HOME"; whoami # a different user or home than you expected is this causeFix
People:
aws configure sso. Jobs: give them their own role (instance profile, task role, OIDC in CI) rather than copying your files.AWS_PROFILE names a profile that has no credentials in it, for example one with only a Region. The CLI uses that profile and stops there.
Check
echo "$AWS_PROFILE" # the profile in use aws configure list # access_key shows <not set> for that profileFix
Point
--profileor AWS_PROFILE at a profile that has credentials, or add them to that profile.An EC2 instance has no instance profile, or IMDS is turned off, so the last step of the chain finds no role.
Check
aws ec2 describe-instances --instance-ids <id> --query 'Reservations[].Instances[].[IamInstanceProfile.Arn,MetadataOptions.HttpEndpoint]' # None, or HttpEndpoint disabled, is this causeFix
Attach an instance profile with only the permissions the server needs, and keep the metadata endpoint enabled with IMDSv2 required.
Prevent it
- Give servers roles, never keys in files
Now learn it: take Level 00 · Accounts, Regions and the CLI. Open in the site
Error when retrieving token from sso: Token has expired and refresh failed
Error text
Error when retrieving token from sso: Token has expired and refresh failed
What it means
The profile signs in through IAM Identity Center (SSO). The CLI's cached SSO login has ended, and its attempt to refresh it failed, so it cannot get role credentials for the profile.
Causes, most likely first
The SSO session reached its configured duration. Identity Center sessions last a set time (often 8 or 12 hours), after which you must sign in again.
Check
aws configure list # confirms which profile is in use aws sts get-caller-identity --profile <profile> # fails with the same messageFix
aws sso login --profile <profile>.You signed out in the browser, or an administrator ended your session. That revokes the cached token too.
Check
aws sso login --profile <profile> # if this works and the command then succeeds, the session had endedFix
Sign in again with
aws sso login.
Prevent it
- Use an sso-session block in ~/.aws/config so several profiles share one login
Now learn it: break and fix it yourself in ExpiredToken, then take Level 00 · Accounts, Regions and the CLI. Open in the site
You must specify a region
Error text
You must specify a region. You can also configure your region by running "aws configure".
botocore.exceptions.NoRegionError: You must specify a region.
What it means
The CLI or SDK found credentials but no Region to send the request to. Most AWS APIs are Regional (each Region has its own endpoint), so it will not guess one.
Causes, most likely first
The profile in
~/.aws/confighas noregionline. The Region is read from the profile in use, so a profile with only credentials has none.Check
aws configure get region --profile <profile> # prints nothing: this is your causeFix
aws configure set region us-east-1 --profile <profile>, or pass--region us-east-1on the command.The code runs in a container, cron job or CI runner where neither AWS_REGION nor AWS_DEFAULT_REGION is set, and there is no config file.
Check
env | grep REGION # nothing printed in that environment is this causeFix
Set AWS_DEFAULT_REGION (and AWS_REGION) in the image, job definition or workflow.
The code creates a client without a Region, using a profile that has credentials but no Region.
Check
aws configure list # region shows <not set> for that profileFix
Name it when you create the client:
boto3.client("ec2", region_name="us-east-1").
Prevent it
- Put
regionin every profile, next to its credentials - Set AWS_DEFAULT_REGION in container images, job definitions and CI workflows
Now learn it: take Level 00 · Accounts, Regions and the CLI. Open in the site
The config profile could not be found
Error text
The config profile (dev) could not be found
botocore.exceptions.ProfileNotFound: The config profile (dev) could not be found
What it means
The CLI was asked for a named profile, with --profile or AWS_PROFILE, and that name is in neither ~/.aws/config nor ~/.aws/credentials for the user running it.
Causes, most likely first
The profile name has a typo, or AWS_PROFILE is still set from another project or shell. The variable applies to every command until you unset it.
Check
echo "$AWS_PROFILE" # a name you did not mean to use is this cause aws configure list-profiles # the names that existFix
Use a name from
aws configure list-profiles, orunset AWS_PROFILE.In
~/.aws/configthe section is written[dev]. There it must be[profile dev]; only~/.aws/credentialsuses a plain[dev], so the CLI does not see the profile.Check
grep -n '^\[' ~/.aws/config ~/.aws/credentials # [dev] in the config file is this causeFix
Rename the section in
~/.aws/configto[profile dev].The command runs as another user, or in a container, with a different home directory, so it reads different files.
Check
whoami; echo "$HOME" # not the user whose ~/.aws holds the profileFix
Create the profile for that user (
aws configure --profile dev, oraws configure sso), or better, give the job its own role.
Prevent it
- Give profiles the same names on every machine (dev, staging, prod)
- In CI, assume a role with OIDC instead of relying on a profile that only exists on a laptop
Now learn it: take Level 00 · Accounts, Regions and the CLI. Open in the site
Could not connect to the endpoint URL
Error text
Could not connect to the endpoint URL: "https://ec2.us-east-1a.amazonaws.com/"
botocore.exceptions.EndpointConnectionError: Could not connect to the endpoint URL: "https://sqs.eu-west-1.amazonaws.com/"
What it means
The CLI built the service's address from the service name and the Region (for example sqs.eu-west-1.amazonaws.com) and could not reach it. Either the address is wrong, or the network has no way out to it.
Causes, most likely first
An Availability Zone was given as the Region (us-east-1a instead of us-east-1), or the Region has a typo. The CLI builds a host name that does not exist.
Check
aws configure get region # anything ending in a letter, like us-east-1a, is this cause nslookup sqs.eu-west-1.amazonaws.com # NXDOMAIN for the host in the message means the name is wrongFix
aws configure set region us-east-1.The machine is in a private subnet with no NAT gateway and no VPC endpoint for that service. The name resolves, but there is no route to the address.
Check
curl -sS -o /dev/null -w '%{http_code}\n' https://sqs.eu-west-1.amazonaws.com/ # hangs, then times out: no network path aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=<subnet-id> # no 0.0.0.0/0 route to a nat- gatewayFix
Add a route to a NAT gateway, or an interface VPC endpoint for the service (gateway endpoint for S3 and DynamoDB).
A proxy or firewall is in the way, or DNS does not work on the machine.
Check
env | grep -io '^https\?_proxy' # a proxy variable is set: the CLI must be able to use it nslookup sts.amazonaws.com # fails: DNS itself is brokenFix
Set HTTPS_PROXY for the CLI, or allow the AWS endpoints through the firewall.
The service is not offered in that Region, so its endpoint does not exist there.
Check
aws ssm get-parameters-by-path --path /aws/service/global-infrastructure/services/<service>/regions --query 'Parameters[].Value' # your Region missing is this causeFix
Use a Region where the service is available.
Prevent it
- Keep the Region in the profile, not typed into each command
- Give private subnets the VPC endpoints their workloads call (S3, ECR, SSM, Secrets Manager)
Now learn it: break and fix it yourself in Private instance cannot reach the internet, then take Level 00 · Accounts, Regions and the CLI. Open in the site
Signature expired or RequestTimeTooSkewed: the clock is wrong
Error text
An error occurred (InvalidSignatureException) when calling the ListFunctions operation: Signature expired: 20261031T101500Z is now earlier than 20261031T102000Z (20261031T103500Z - 15 min.)
An error occurred (RequestTimeTooSkewed) when calling the ListObjectsV2 operation: The difference between the request time and the current time is too large.
What it means
Every request is signed with the time it was made. AWS rejects a request whose time is more than about 15 minutes away from its own clock. The first message shows the signed time, the server's time, and the 15-minute limit, so you can read the drift directly.
Causes, most likely first
The machine's clock has drifted: a laptop after sleep, a VM restored from a snapshot, a container host without time sync. The request carries the wrong time, so AWS treats it as expired or not yet valid.
Check
date -u # compare with the real UTC time: minutes off is this causeFix
Turn time sync on:
sudo timedatectl set-ntp true, then run the command again. Changing the time zone does not help: signing uses UTC.No time sync service is running (chrony or systemd-timesyncd), so the clock keeps drifting.
Check
timedatectl status # System clock synchronized: no, or NTP service: inactive, is this causeFix
Enable chrony or systemd-timesyncd. On EC2, point chrony at the Amazon Time Sync Service, 169.254.169.123.
Prevent it
- Keep chrony or systemd-timesyncd running on every host and in every image
Now learn it: take Level 00 · Accounts, Regions and the CLI. Open in the site
AccessDenied: no identity-based policy allows the action
Error text
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: User: arn:aws:iam::111122223333:user/analyst is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::nimbus-kitchen-reports" because no identity-based policy allows the s3:ListBucket action
What it means
IAM checked this request and found no Allow for it. The message names all three parts: who asked (User: ...), what they asked to do (s3:ListBucket) and on what (resource: ...). no identity-based policy allows means none of the policies attached to that user or role allows that action on that resource; nothing denied it explicitly.
Causes, most likely first
The call runs as another identity than you meant: another profile, leftover AWS_* variables, or the instance role instead of yours. That identity simply has no such permission.
Check
aws sts get-caller-identity # if the Arn is not the identity you meant, this is your causeFix
Fix the credentials (profile, variables) rather than granting the wrong identity more.
The policy allows a different action, or the same action on a different resource. For S3,
s3:ListBucketneedsarn:aws:s3:::bucketwhiles3:GetObjectneedsarn:aws:s3:::bucket/*; an allow on one does not cover the other.Check
aws iam simulate-principal-policy --policy-source-arn <arn> --action-names <action> --resource-arns <resource-arn> # implicitDeny: no identity policy allows exactly this (resource policies are not included unless you pass one)Fix
Allow exactly that action on exactly that resource, for example
s3:ListBucketonarn:aws:s3:::nimbus-kitchen-reports. Nots3:*and not*.A condition in the policy does not match the request (source IP, MFA, tags, Region). The statement exists but does not apply to this call.
Check
aws iam simulate-principal-policy --policy-source-arn <arn> --action-names <action> --resource-arns <resource-arn> --context-entries ContextKeyName=aws:SourceIp,ContextKeyValues=<ip>,ContextKeyType=ip # allowed only with the right context: the condition is the causeFix
Make the request meet the condition, or correct the condition if it is wrong.
Across accounts, the other account's resource policy allows it but your own identity policy does not. Across accounts both sides must allow.
Check
aws sts get-caller-identity --query Account # differs from the account in the resource ARN: check your own policy tooFix
Add the allow to your identity policy as well, for that action and resource only.
Prevent it
- Test new policies with simulate-principal-policy or IAM Access Analyzer policy validation
Now learn it: break and fix it yourself in AccessDenied on ListBucket, then take Level 01 · IAM users, groups and policies. Open in the site
AccessDenied with an explicit deny
Error text
is not authorized to perform: ec2:StopInstances on resource: arn:aws:ec2:us-east-1:111122223333:instance/i-0abc with an explicit deny in an identity-based policy
with an explicit deny in a resource-based policy
What it means
A Deny statement matched this request. An explicit deny wins over every allow, AdministratorAccess included. The end of the message names the kind of policy that denied (identity-based, resource-based, session policy, permissions boundary or VPC endpoint policy), so start there.
Causes, most likely first
in an identity-based policy: a guard-rail Deny on the user, one of its groups or the role, often with Resource "*" and no condition.Check
aws sts get-caller-identity # whose policies to read: for assumed-role/<role>/<session>, read the role aws iam list-attached-role-policies --role-name <role>; aws iam list-role-policies --role-name <role> # read each for an Effect Deny that matches (for a user: the -user- and -group- versions)Fix
Narrow the Deny with a condition or a specific resource, rather than deleting it.
in a resource-based policy: the bucket, key, queue or secret policy denies, for example requests without TLS (aws:SecureTransport) or from outside a VPC endpoint.Check
aws s3api get-bucket-policy --bucket <bucket> --query Policy --output text # a Deny whose condition your request fails (or get-key-policy, get-resource-policy)Fix
Make the request meet the condition (use HTTPS, go through the VPC endpoint), or narrow the Deny.
in a permissions boundary: the boundary set on the user or role contains a Deny for this action.Check
aws iam get-role --role-name <role> --query Role.PermissionsBoundary # the boundary policy to readFix
Change the boundary only if the guard rail is wrong; it is usually there on purpose.
in a session policy: a policy was passed with--policyor--policy-arnswhen the session was created, and it denies.Check
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole --max-results 20 # requestParameters show the policy or policyArns passed for your sessionFix
Start the session again without that policy, or with one that allows the action.
in a VPC endpoint policy: the endpoint the request went through does not let it pass.Check
aws ec2 describe-vpc-endpoints --query 'VpcEndpoints[].[VpcEndpointId,ServiceName,PolicyDocument]' # a Deny, or no Allow, for your action or bucketFix
Allow that action and resource in the endpoint policy.
Prevent it
- Write denies with explicit conditions and test both what they block and what they allow
Now learn it: break and fix it yourself in Explicit deny wins, then take Level 03 · How AWS decides: policy evaluation. Open in the site
UnauthorizedOperation with an encoded authorization failure message
Error text
An error occurred (UnauthorizedOperation) when calling the RunInstances operation: You are not authorized to perform this operation. Encoded authorization failure message: ...
What it means
EC2 refused the request and does not say why in plain text. The encoded message holds the answer: the action, the resource, and whether an explicit deny matched. You decode it with STS.
Causes, most likely first
A permission on a related resource is missing:
iam:PassRolefor the instance profile,kms:CreateGrantfor an encrypted volume,ec2:CreateTagsfor tags in the request. RunInstances checks all of them.Check
aws sts decode-authorization-message --encoded-message <message> --query DecodedMessage --output text # explicitDeny false, and context.action names the missing permissionFix
Grant exactly
context.actiononcontext.resourcefrom the decoded JSON.An explicit deny matched, for example a guard rail on instance types or Regions.
Check
aws sts decode-authorization-message --encoded-message <message> --query DecodedMessage --output text # explicitDeny true; matchedStatements shows which statementFix
Change the request to meet the guard rail, or narrow the deny if it is wrong.
Prevent it
- Give developers sts:DecodeAuthorizationMessage so they can read their own failures
Now learn it: break and fix it yourself in UnauthorizedOperation: iam:PassRole · Explicit deny wins, then take Level 02 · Roles, STS and trust. Open in the site
Not authorized to perform iam:PassRole
Error text
is not authorized to perform: iam:PassRole on resource: arn:aws:iam::111122223333:role/web-role because no identity-based policy allows the iam:PassRole action
What it means
Giving a role to a service (an instance profile, a Lambda execution role, an ECS task role) counts as passing that role, and needs iam:PassRole on it. The message names the role, so you know exactly which permission is missing.
Causes, most likely first
The caller can create the resource but has no
iam:PassRoleat all. Creating an instance or function and handing it a role are two separate permissions.Check
aws iam simulate-principal-policy --policy-source-arn <caller-arn> --action-names iam:PassRole --resource-arns <role-arn> # implicitDeny is this causeFix
Allow
iam:PassRoleon that role's ARN, with aniam:PassedToServicecondition naming the service.PassRole is allowed, but for a different role, or with an
iam:PassedToServicecondition for another service.Check
aws iam get-role-policy --role-name <caller-role> --policy-name <policy> # check the PassRole Resource and its iam:PassedToService valueFix
Add this role's ARN, or the right service (
ec2.amazonaws.com,lambda.amazonaws.com), to that statement.
Prevent it
- Never grant iam:PassRole on *: it lets the caller hand an admin role to code they control
Now learn it: break and fix it yourself in UnauthorizedOperation: iam:PassRole, then take Level 02 · Roles, STS and trust. Open in the site
AccessDenied when calling AssumeRole
Error text
An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::111122223333:user/ci-bot is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::111122223333:role/deploy-role
What it means
The caller asked STS for credentials for a role and was refused. Two policies decide this: the caller's own policies must allow sts:AssumeRole, and the role's trust policy must allow the caller. The message is the same whichever one said no. Across accounts both must allow; in the same account a trust policy that names the caller's ARN is enough, but one that names only the account (:root) also needs the caller's own allow.
Causes, most likely first
The trust policy does not name this caller: it names a different account or role, or a user that was deleted (it then shows as AIDA... or AROA..., an ID instead of an ARN).
Check
aws sts get-caller-identity # the caller actually used aws iam get-role --role-name <role> --query Role.AssumeRolePolicyDocument # if that caller and its account are not in Principal, or it shows AIDA/AROA, this is your causeFix
Name the caller in the trust policy with
aws iam update-assume-role-policy. A recreated user needs to be added again: the old ID will not match.The trust policy has a condition the call does not meet, such as
sts:ExternalIdoraws:MultiFactorAuthPresent.Check
aws iam get-role --role-name <role> --query 'Role.AssumeRolePolicyDocument.Statement[].Condition' # a condition your call does not satisfyFix
Pass what it asks for:
--external-id <id>, or sign in with MFA (--serial-numberand--token-code).The caller's own policy does not allow
sts:AssumeRoleon this role, and the role is in another account or its trust names only the account.Check
aws iam simulate-principal-policy --policy-source-arn <caller-arn> --action-names sts:AssumeRole --resource-arns <role-arn> # implicitDeny: the caller's side is missing (the trust policy is not part of this check)Fix
Allow
sts:AssumeRoleon that one role ARN in the caller's policy.The call passes session tags or a source identity, and the trust policy does not also allow
sts:TagSessionorsts:SetSourceIdentity.Check
aws iam get-role --role-name <role> --query Role.AssumeRolePolicyDocument # Action has only sts:AssumeRole, while your tool passes tagsFix
Add
sts:TagSession(orsts:SetSourceIdentity) to the trust statement's Action.
Prevent it
- Trust roles rather than users, and manage trust policies in code
Now learn it: break and fix it yourself in Role cannot be assumed, then take Level 02 · Roles, STS and trust. Open in the site
Not authorized to perform sts:AssumeRoleWithWebIdentity
Error text
An error occurred (AccessDenied) when calling the AssumeRoleWithWebIdentity operation: Not authorized to perform sts:AssumeRoleWithWebIdentity
Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
What it means
A workload (GitHub Actions, for example) sent STS an OIDC token, a signed statement of who it is, and asked for a role. The role's trust policy did not accept that token: its claims (sub for which repository and branch, aud for the audience) did not match the conditions, or the provider is not the one trusted.
Causes, most likely first
The
subcondition names another repository, branch or environment. A job that uses an environment sendsrepo:owner/repo:environment:prodinstead of the ref, and a pull request sendsrepo:owner/repo:pull_request.Check
aws iam get-role --role-name <role> --query Role.AssumeRolePolicyDocument # the sub value the role accepts aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity --max-results 5 # the failed call's userIdentity shows the subject the token carried; if it differs from sub, this is your causeFix
Set
subto exactly what the job sends, for examplerepo:owner/repo:ref:refs/heads/mainorrepo:owner/repo:environment:prod.A wildcard is used in
subunder StringEquals. Wildcards only work with StringLike, sorepo:owner/repo:*under StringEquals matches nothing.Check
aws iam get-role --role-name <role> --query Role.AssumeRolePolicyDocument # a * inside a StringEquals block is this causeFix
Use StringLike for patterns, scoped to the repository and branch or environment, never
repo:*.The
audis notsts.amazonaws.com, or the Federated principal names an OIDC provider in another account or one that does not exist.Check
aws iam list-open-id-connect-providers # the provider for token.actions.githubusercontent.com must be in this account, and its ARN must match Principal.FederatedFix
Create the provider in this account, and set the
audcondition tosts.amazonaws.com.The workflow lacks
permissions: id-token: write, so it cannot request a token. The action then fails before it calls AWS, with a message about the id-token permission.Check
grep -n -A3 'permissions:' .github/workflows/<workflow>.yml # no id-token: write is this causeFix
Add
permissions: id-token: write(andcontents: read) to the job.
Prevent it
- Never use repo:* or a bare *; scope to repository and branch or environment
Now learn it: break and fix it yourself in GitHub Actions cannot assume its role, then take Level 18 · Infrastructure as code and CI/CD. Open in the site
Explicit deny in a service control policy
Error text
is not authorized to perform: s3:CreateBucket on resource: "arn:aws:s3:::nimbus-kitchen-archive-eu" with an explicit deny in a service control policy
because no service control policy allows the
What it means
Your AWS Organization's service control policies (SCPs) cap what this account can do. SCPs apply to every user and role in member accounts, root included, but not to the management account or to service-linked roles, and nothing inside the account overrides them. with an explicit deny in a service control policy means an SCP denies it; no service control policy allows means an allow-list SCP does not include the action. A resource control policy (RCP) does the same for resources.
Causes, most likely first
A Region restriction: an SCP denies requests outside approved Regions (a Deny on
aws:RequestedRegion).Check
aws organizations list-parents --child-id <account-id> # the OU the account is in; SCPs on every OU up to the root apply aws organizations list-policies-for-target --target-id <account-id> --filter SERVICE_CONTROL_POLICY # from the management account or a delegated administrator; repeat for each OU, and look for aws:RequestedRegionFix
Use an allowed Region, or ask the platform team for an exception.
A guard rail blocks the action itself: disabling CloudTrail, leaving the organization, changing networking.
Check
aws organizations describe-policy --policy-id <policy-id> --query Policy.Content --output text # a Deny naming your actionFix
Work within the guard rail, or request an exception from the team that owns it. If you cannot read the SCPs, ask them which one applies.
An allow-list SCP, on the account or on any OU above it, does not list the service or action. Every level must allow it.
Check
aws organizations list-policies-for-target --target-id <ou-id> --filter SERVICE_CONTROL_POLICY # an OU without FullAWSAccess, whose SCP does not list your serviceFix
Ask for the action to be added to the allow list at that level.
Prevent it
- Publish your organization's guard rails where engineers can read them
Now learn it: break and fix it yourself in Denied by a service control policy, then take Level 03 · How AWS decides: policy evaluation. Open in the site
KMS AccessDeniedException: no resource-based policy allows kms:Decrypt
Error text
An error occurred (AccessDeniedException) when calling the Decrypt operation: User: arn:aws:sts::111122223333:assumed-role/payments-app/you is not authorized to perform: kms:Decrypt on resource: arn:aws:kms:us-east-1:111122223333:key/... because no resource-based policy allows the kms:Decrypt action
What it means
KMS refused to use the key for this caller. The key policy is the starting point for every KMS key: an IAM allow counts only when the key policy lets this account's IAM policies grant access (a statement for arn:aws:iam::<account-id>:root); otherwise the key policy, or a grant, must name the caller. no resource-based policy allows means the key policy is what is missing.
Causes, most likely first
The key policy has no statement for the role, and no root-account statement delegating to IAM. IAM permissions alone then count for nothing.
Check
aws kms get-key-policy --key-id <key-id> --policy-name default --query Policy --output text # neither your role nor arn:aws:iam::<account-id>:root is allowed the action: this is your causeFix
Add a key policy statement for the role, or the account-root statement and then an IAM allow, with
aws kms put-key-policy.The key policy delegates to IAM, but the role's own policy does not allow the action on this key. The message then says
no identity-based policy allows.Check
aws iam simulate-principal-policy --policy-source-arn <role-arn> --action-names kms:Decrypt --resource-arns <key-arn> # implicitDeny is this causeFix
Allow
kms:Decrypt(or the action named) on that key's ARN in the role's policy.The data was encrypted with a different key than the one you allowed. You must be allowed the key that was actually used.
Check
aws s3api head-object --bucket <bucket> --key <key> # SSEKMSKeyId: if it is not the key you allowed, this is your causeFix
Allow the key that was used, or re-encrypt the data with the intended key.
A condition in the key policy, such as
kms:ViaServiceorkms:EncryptionContext, is not met by this request.Check
aws kms get-key-policy --key-id <key-id> --policy-name default --query Policy --output text # your statement has a Condition the call does not satisfyFix
Call through the service the condition names, or correct the condition.
Cross-account use, without both the key policy and the caller's IAM policy allowing it.
Check
aws sts get-caller-identity --query Account # differs from the account in the key ARNFix
Allow the other account (or its role) in the key policy, and allow the key's ARN in the role's IAM policy.
Prevent it
- Keep the administrators' statement in every key policy; KMS's lockout check only stops a policy that locks out the principal setting it
Now learn it: break and fix it yourself in KMS key policy blocks decrypt, then take Level 15 · KMS and encryption. Open in the site
Connection timed out to an EC2 instance
Error text
curl: (28) Failed to connect to 203.0.113.10 port 80 after 75003 ms: Couldn't connect to server
ssh: connect to host 203.0.113.10 port 22: Operation timed out
What it means
Your client sent a connection request and never got any answer, so it gave up (curl error 28, ssh timed out). Packets were dropped silently somewhere on the way. In AWS that is a security group, a network ACL or a missing route, not the application: a running app with a closed port would answer Connection refused.
Causes, most likely first
The security group does not allow the port from your address. Security groups drop what they do not allow, with no reply.
Check
aws ec2 describe-security-groups --group-ids <sg-id> --query 'SecurityGroups[].IpPermissions' # no rule for that port from your IP (or a security group you are in) is this causeFix
Allow that one port from your address or from the client's security group, not from 0.0.0.0/0. For SSH, use Session Manager instead.
The network ACL drops the request, or the reply. NACLs are stateless, so the return traffic to the client's ephemeral ports (1024-65535) needs its own outbound rule.
Check
aws ec2 describe-network-acls --filters Name=association.subnet-id,Values=<subnet-id> # a DENY before your ALLOW, or no outbound rule for 1024-65535, is this causeFix
Allow the port inbound and ephemeral ports outbound in the subnet's NACL.
The subnet has no route to an internet gateway, so replies cannot get back to the internet.
Check
aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=<subnet-id> # no 0.0.0.0/0 route to an igw- is this cause (no row at all means it uses the main route table)Fix
Add a 0.0.0.0/0 route to the VPC's internet gateway, or put the instance in a public subnet.
The instance has no public IP, or is stopped. There is then nothing at the address you are trying.
Check
aws ec2 describe-instances --instance-ids <id> --query 'Reservations[].Instances[].[State.Name,PublicIpAddress,SubnetId]' # stopped, or no PublicIpAddress, is this causeFix
Start the instance, attach an Elastic IP, or reach it privately (Session Manager, a VPN, a load balancer). VPC Reachability Analyzer can trace the whole path for you.
Prevent it
- Build networks from templates, and use Session Manager instead of opening SSH
Now learn it: break and fix it yourself in Website times out: security group · Network ACL drops the replies · No public IP address · Subnet with no route to the internet, then take Level 06 · Security groups and network ACLs. Open in the site
Connection refused
Error text
curl: (7) Failed to connect to 10.0.11.21 port 80 after 3 ms: Couldn't connect to server
ssh: connect to host 10.0.1.30 port 22: Connection refused
What it means
Your connection reached the machine, and the machine answered that nothing is listening on that port (a TCP reset). The network path works: security groups and NACLs drop packets, they never refuse. The problem is on the host.
Causes, most likely first
The service is down or crashed, so nothing listens on the port.
Check
aws ssm start-session --target <instance-id> # get a shell on the instance sudo ss -tlnp # no line for the port means nothing listens: this is your causeFix
Start the service and read its log for why it stopped (
sudo journalctl -u <service> -n 50).It listens on another port (8080, not 80).
Check
sudo ss -tlnp # the process listens, but on a different port than you connect toFix
Connect to the port it uses, or change the load balancer or client to it.
It listens only on 127.0.0.1, so it accepts connections from the instance itself and refuses the rest.
Check
sudo ss -tlnp # 127.0.0.1:80 is this cause; 0.0.0.0:80 or *:80 would accept from outsideFix
Make the app bind to 0.0.0.0 (or the instance's private address).
A host firewall on the instance (iptables or nftables with REJECT) refuses the connection.
Check
sudo iptables -S | grep -i reject; sudo nft list ruleset | grep -i reject # a REJECT rule covering the portFix
Allow the port in the host firewall, rather than turning the firewall off.
Prevent it
- Health checks on the real port
Now learn it: break and fix it yourself in ALB returns 502, then take Level 06 · Security groups and network ACLs. Open in the site
Private instance cannot reach the internet
Error text
curl: (28) Failed to connect to api.github.com port 443 after 130217 ms: Couldn't connect to server
What it means
An instance in a private subnet tried to reach an internet address and timed out. Private subnets have no route to the internet gateway by design; they reach the internet only through a NAT gateway (or a proxy).
Causes, most likely first
The private route table has no 0.0.0.0/0 route to a NAT gateway.
Check
aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=<subnet-id> # no 0.0.0.0/0 to a nat- target is this causeFix
Create a NAT gateway in a public subnet with an Elastic IP, and route 0.0.0.0/0 from the private route table to it.
The NAT gateway exists but sits in a private subnet, or is not available. A NAT gateway needs a route to the internet gateway itself.
Check
aws ec2 describe-nat-gateways --query 'NatGateways[].[NatGatewayId,State,SubnetId]' # State not available, or a SubnetId whose route table has no igw- route, is this causeFix
Create the NAT gateway in a public subnet and point the private route at the new one.
Outbound traffic is blocked by the instance's security group or the subnet's NACL.
Check
aws ec2 describe-security-groups --group-ids <sg-id> --query 'SecurityGroups[].IpPermissionsEgress' # no outbound rule for 443Fix
Allow outbound 443, and the NACL's ephemeral ports inbound for the replies.
Prevent it
- Use gateway endpoints for S3 and DynamoDB, and interface endpoints for AWS APIs, so less traffic needs NAT
Now learn it: break and fix it yourself in Private instance cannot reach the internet, then take Level 05 · Internet access: IGW, NAT and public IPs. Open in the site
TargetNotConnected when starting a Session Manager session
Error text
An error occurred (TargetNotConnected) when calling the StartSession operation: i-0123456789abcdef0 is not connected.
What it means
Session Manager could not start a session because the SSM agent on the instance is not talking to Systems Manager. The agent connects out to the SSM endpoints with the instance's role; until it does, the instance is not a managed node.
Causes, most likely first
The instance role lacks
AmazonSSMManagedInstanceCore, or the instance has no role. The agent cannot register without it.Check
aws ec2 describe-instances --instance-ids <id> --query 'Reservations[].Instances[].IamInstanceProfile' # empty means no role aws iam list-attached-role-policies --role-name <role> # AmazonSSMManagedInstanceCore missing is this causeFix
Attach
AmazonSSMManagedInstanceCoreto the instance role. The agent picks it up within a few minutes.There is no route to the SSM endpoints: a private subnet with no NAT and no interface endpoints.
Check
aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values=<vpc-id> --query 'VpcEndpoints[].ServiceName' # no ssm, ssmmessages and ec2messages, and no NAT route, is this causeFix
Give the subnet a NAT route, or interface endpoints for ssm, ssmmessages and ec2messages (with a security group that allows 443 from the instance).
The instance is stopped, or the agent is not installed or not running.
Check
aws ssm describe-instance-information --filters Key=InstanceIds,Values=<id> # no entry, or PingStatus ConnectionLost, is this causeFix
Start the instance. Use an AMI with the agent (Amazon Linux, recent Ubuntu), or install it from user data.
Prevent it
- Bake the agent and role into every launch template
Now learn it: break and fix it yourself in Private instance cannot reach the internet · SSH open to the world, then take Level 16 · Security operations. Open in the site
VPC peering active, traffic times out
Error text
Ncat: TIMEOUT.
"Status": {"Code": "active", "Message": "Active"}
"Status": {"Code": "failed", "Message": "Overlapping CIDRs"}
What it means
The peering connection is active, but traffic between the VPCs times out. Peering only connects the networks; each side still needs routes pointing at it, and security groups must allow the traffic. Overlapping CIDRs means the peering could not be created at all.
Causes, most likely first
One VPC's route table has no route to the other VPC's CIDR through the peering. Traffic goes one way and the reply has no way back.
Check
aws ec2 describe-route-tables --filters Name=vpc-id,Values=<vpc-id> # in both VPCs, look for the other CIDR with a pcx- target; missing on either side is this causeFix
Add a route to the other VPC's CIDR via the peering connection in both VPCs' route tables.
Security groups do not allow the other VPC's addresses.
Check
aws ec2 describe-security-groups --group-ids <sg-id> # no rule for the other VPC's CIDR (or its security group, same Region)Fix
Allow the other VPC's CIDR, or its security group when the peering is in the same Region.
The VPCs' CIDRs overlap. A peering between overlapping ranges cannot be created.
Check
aws ec2 describe-vpc-peering-connections --query 'VpcPeeringConnections[].Status' # failed, Overlapping CIDRsFix
Re-address one VPC, or connect them through a private NAT or PrivateLink.
You expect traffic to pass through a middle VPC. Peering is not transitive: A to B and B to C does not give A to C.
Check
aws ec2 describe-vpc-peering-connections --query 'VpcPeeringConnections[].[RequesterVpcInfo.VpcId,AccepterVpcInfo.VpcId]' # no direct peering between the two VPCsFix
Peer the two VPCs directly, or use a Transit Gateway.
Prevent it
- Plan non-overlapping CIDRs for every VPC up front; use a Transit Gateway for more than a few VPCs
Now learn it: break and fix it yourself in VPC peering: no route back, then take Level 04 · VPC, subnets and route tables. Open in the site
InsufficientInstanceCapacity
Error text
An error occurred (InsufficientInstanceCapacity) when calling the RunInstances operation (reached max retries: 2): We currently do not have sufficient c7i.8xlarge capacity in the Availability Zone you requested (us-east-1a).
What it means
AWS does not have enough of that instance type free in that Availability Zone right now. It is AWS's capacity, not your account's limit (that would be VcpuLimitExceeded), and it usually passes.
Causes, most likely first
A popular or large type is short in one AZ.
Check
aws ec2 describe-instance-type-offerings --location-type availability-zone --filters Name=instance-type,Values=<type> # other AZs that offer the typeFix
Launch in another AZ, or a similar type (c6i.8xlarge instead of c7i.8xlarge).
A capacity spike across the Region.
Check
aws ec2 describe-instance-type-offerings --location-type availability-zone --filters Name=instance-type,Values=<type> --region <region> # the type is offered, yet every AZ fails: retry laterFix
Retry with backoff, or use an Auto Scaling group with several types and AZs. For critical workloads, buy a capacity reservation.
Prevent it
- Auto Scaling groups across several AZs and instance types; capacity reservations for critical workloads
Now learn it: break and fix it yourself in InsufficientInstanceCapacity, then take Level 07 · EC2: launch, state and limits. Open in the site
VcpuLimitExceeded
Error text
An error occurred (VcpuLimitExceeded) when calling the RunInstances operation: You have requested more vCPU capacity than your current vCPU limit of 32 allows for the instance bucket that the specified instance type belongs to.
What it means
Your running instances in this instance family group already use the account's vCPU quota for the Region. The quota counts vCPUs of running On-Demand instances, not the number of instances.
Causes, most likely first
Forgotten instances are still running and using the quota.
Check
aws ec2 describe-instances --filters Name=instance-state-name,Values=running --query 'Reservations[].Instances[].[InstanceId,InstanceType,Tags]' # instances nobody needsFix
Stop or terminate what you do not need.
The account is new or the quota is low for what you run.
Check
aws service-quotas get-service-quota --service-code ec2 --quota-code L-1216C47A # Value: the vCPU limit for standard (A, C, D, H, I, M, R, T, Z) instancesFix
Request an increase:
aws service-quotas request-service-quota-increase --service-code ec2 --quota-code L-1216C47A --desired-value <vcpus>.
Prevent it
- Tag instances with an owner and expiry, and review running instances weekly
Now learn it: break and fix it yourself in VcpuLimitExceeded, then take Level 07 · EC2: launch, state and limits. Open in the site
Status check failed (1/2 or 0/2 checks passed)
Error text
"SystemStatus": {"Status": "impaired"}
StatusCheckFailed_System
What it means
EC2 runs two checks on every instance. The system status check watches the AWS host and its network; the instance status check watches your operating system. Which one failed tells you whose problem it is.
Causes, most likely first
System check failed: hardware or network trouble on the AWS host.
Check
aws ec2 describe-instance-status --instance-ids <id> # SystemStatus impaired is this causeFix
Stop and start the instance (not reboot): that moves it to a new host. Instance store data is lost.
Instance check failed: the operating system is not healthy (kernel panic, full disk, broken network configuration).
Check
aws ec2 describe-instance-status --instance-ids <id> # InstanceStatus impaired, SystemStatus ok aws ec2 get-console-output --instance-id <id> --latest # the boot log shows the panic, the full disk or the failed networkFix
Fix the OS from what the console shows: free disk, revert the change, or attach the volume to another instance and repair it.
Prevent it
- A CloudWatch alarm on StatusCheckFailed_System with the recover action
Now learn it: break and fix it yourself in System status check failed, then take Level 07 · EC2: launch, state and limits. Open in the site
Auto Scaling group does not scale
Error text
New SetDesiredCapacity value 4 is above max value 2 for the AutoScalingGroup.
"StateValue": "INSUFFICIENT_DATA"
What it means
An Auto Scaling group scales only when three things line up: an alarm or target that sees data, a scaling policy that acts on it, and room between MinSize and MaxSize. The first message says the group is at MaxSize; INSUFFICIENT_DATA says the alarm sees no metric.
Causes, most likely first
DesiredCapacity already equals MaxSize, so there is no room to add instances.
Check
aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names <asg> --query 'AutoScalingGroups[].[MinSize,DesiredCapacity,MaxSize]' # Desired equal to Max is this causeFix
Raise MaxSize:
aws autoscaling update-auto-scaling-group --auto-scaling-group-name <asg> --max-size <n>.The alarm watches the wrong dimension, often an old group name, so it never gets data.
Check
aws cloudwatch describe-alarms --alarm-names <alarm> --query 'MetricAlarms[].[StateValue,Dimensions]' # INSUFFICIENT_DATA, or a dimension that is not this group, is this causeFix
Point the alarm at the right dimensions, or use a target tracking policy, which manages its own alarms.
The alarm has no action, or the policy is disabled.
Check
aws cloudwatch describe-alarms --alarm-names <alarm> --query 'MetricAlarms[].[AlarmActions,ActionsEnabled]' # empty actions or false is this cause aws autoscaling describe-scaling-activities --auto-scaling-group-name <asg> # no recent activity when the alarm firedFix
Attach the scaling policy's ARN as the alarm action and enable actions.
Prevent it
- Target tracking policies, which manage their own alarms
Now learn it: break and fix it yourself in Auto Scaling stuck at its maximum · Scaling alarm on the wrong metric, then take Level 10 · Load balancers and Auto Scaling. Open in the site
IncorrectInstanceState
Error text
An error occurred (IncorrectInstanceState) when calling the RebootInstances operation: The instance 'i-0123456789abcdef0' is not in a state from which it can be rebooted.
What it means
The action does not apply to the instance's current state, for example rebooting a stopped instance or stopping one that is already stopping.
Causes, most likely first
The instance is stopped, stopping or terminated.
Check
aws ec2 describe-instances --instance-ids <id> --query 'Reservations[].Instances[].State.Name' # a state the action cannot start fromFix
Start it first, or wait for the transition to finish:
aws ec2 wait instance-running --instance-ids <id>.
Prevent it
- Script with
aws ec2 waitbetween steps
Now learn it: break and fix it yourself in Instance stopped, then take Level 07 · EC2: launch, state and limits. Open in the site
SSH to EC2: Permission denied (publickey)
Error text
ec2-user@203.0.113.10: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
Permissions 0644 for 'web-key.pem' are too open.
What it means
The network path works: the instance answered. SSH then refused the key you offered for that user name. Permissions ... are too open means your own ssh client refused to use the key file because other users can read it.
Causes, most likely first
The wrong user name for the AMI. Each AMI has its own default user, and the key is only installed for that one.
Check
aws ec2 describe-images --image-ids <ami-id> --query 'Images[].[Name,Description]' # Amazon Linux: ec2-user; Ubuntu: ubuntu; Debian: adminFix
Connect as the AMI's user:
ssh -i web-key.pem ec2-user@<public-ip>.The wrong private key, or the instance was launched with another key pair or none.
Check
aws ec2 describe-instances --instance-ids <instance-id> --query 'Reservations[].Instances[].KeyName' # not the key pair your file belongs to, or None, is this cause ssh -v -i web-key.pem ec2-user@<public-ip> # Offering public key ... then no 'Server accepts key' lineFix
Use the matching key. Lost it? Connect with Session Manager or EC2 Instance Connect and add a new public key to
~/.ssh/authorized_keys.The key file can be read by other users, so ssh ignores it.
Check
ls -l web-key.pem # anything wider than -r-------- (400) or -rw------- (600)Fix
chmod 400 web-key.pem.~/.ssh/authorized_keyson the instance was changed or has wrong permissions.Check
aws ssm start-session --target <instance-id> # then: ls -l ~ec2-user/.ssh (700 for the folder, 600 for the file)Fix
Restore the public key and permissions through Session Manager.
Prevent it
- Use Session Manager instead of SSH keys: no port 22 to open and no key to lose
- Keep one key pair per environment, stored safely
Now learn it: take Level 07 · EC2: launch, state and limits. Open in the site
InvalidKeyPair.NotFound: the key pair does not exist
Error text
An error occurred (InvalidKeyPair.NotFound) when calling the RunInstances operation: The key pair 'web-key' does not exist
What it means
EC2 key pairs belong to one Region. The key pair name you passed is not in the Region this request went to.
Causes, most likely first
The key pair was created in another Region.
Check
aws configure get region # the Region the request went to aws ec2 describe-key-pairs --query 'KeyPairs[].KeyName' --region <other-region> # the key is there, not hereFix
Launch in that Region with
--region <region>, or import your public key into this Region:aws ec2 import-key-pair --key-name web-key --public-key-material fileb://~/.ssh/id_ed25519.pub.A typo in the name, or the key pair was deleted.
Check
aws ec2 describe-key-pairs --query 'KeyPairs[].KeyName' # the names that exist in this RegionFix
Use an existing name, or launch without a key and connect with Session Manager.
Prevent it
- Keep key pairs, AMIs and launch templates in the Region you deploy to, or import the same public key into each Region
Now learn it: take Level 07 · EC2: launch, state and limits. Open in the site
InvalidAMIID.NotFound: the image id does not exist
Error text
An error occurred (InvalidAMIID.NotFound) when calling the RunInstances operation: The image id '[ami-0123456789abcdef0]' does not exist
What it means
AMI IDs are Regional: the same Amazon Linux image has a different ID in each Region. The ID you passed does not exist in this Region, or you are not allowed to see it.
Causes, most likely first
The ID was copied from a tutorial or from another Region.
Check
aws ec2 describe-images --image-ids <ami-id> --region <region> # found in another Region but not this one is this causeFix
Look up this Region's current ID:
aws ssm get-parameters --names /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --query 'Parameters[0].Value'.The AMI was deregistered.
Check
aws ec2 describe-images --owners self --query 'Images[].ImageId' # your own AMI is no longer listedFix
Use a current AMI, or rebuild it.
A private AMI in another account that is not shared with you.
Check
aws ec2 describe-image-attribute --image-id <ami-id> --attribute launchPermission # run in the owning account: your account not listed is this causeFix
Ask the owner to share it, or copy your own AMI into the Region:
aws ec2 copy-image --source-image-id <ami-id> --source-region <region> --name web --region <target-region>.
Prevent it
- In templates, resolve the AMI from that public SSM parameter instead of hard-coding an ID
Now learn it: take Level 07 · EC2: launch, state and limits. Open in the site
NoSuchBucket: the specified bucket does not exist
Error text
An error occurred (NoSuchBucket) when calling the ListObjectsV2 operation: The specified bucket does not exist
What it means
No bucket with that exact name exists anywhere. It is not a permissions error: a bucket that exists but is not yours gives AccessDenied instead.
Causes, most likely first
A typo in code or configuration.
Check
aws s3 ls | grep <part-of-name> # the real name differs from the one in the messageFix
Use the right name in configuration.
The bucket was deleted.
Check
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DeleteBucket # a DeleteBucket for that nameFix
Create it again (the name may be taken by then), or point the app at the new bucket.
An environment variable points at another stage's bucket.
Check
env | grep -i bucket # a name for another stage (dev in prod)Fix
Pass bucket names from infrastructure as code outputs rather than typing them.
Prevent it
- Pass bucket names from infrastructure as code outputs rather than typing them
Now learn it: break and fix it yourself in NoSuchBucket from a Lambda function, then take Level 09 · S3: buckets, access and versioning. Open in the site
BucketAlreadyExists
Error text
An error occurred (BucketAlreadyExists) when calling the CreateBucket operation: The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again.
What it means
Another AWS account already owns a bucket with that name. Bucket names are global across all accounts, so you need another name. (If it were yours, the error would be BucketAlreadyOwnedByYou.)
Causes, most likely first
The name is short or generic, so someone took it first.
Check
aws s3api head-bucket --bucket <name> # 403: it exists in another account; 404: the name is freeFix
Choose a unique name, for example organization, purpose, account and Region:
nimbus-kitchen-reports-111122223333-eu-west-1.
Prevent it
- A naming convention and infrastructure as code
Now learn it: break and fix it yourself in BucketAlreadyExists, then take Level 09 · S3: buckets, access and versioning. Open in the site
403 Forbidden on HeadObject or GetObject
Error text
fatal error: An error occurred (403) when calling the HeadObject operation: Forbidden
An error occurred (AccessDenied) when calling the GetObject operation: Access Denied
What it means
S3 refused the request. HeadObject (which the CLI uses before a download) has no response body, so it can only say 403 Forbidden, not why. Check each layer below in turn.
Causes, most likely first
No identity policy allows
s3:GetObjecton that key, or the call runs as another identity than you think.Check
aws sts get-caller-identity # the identity S3 judged aws iam simulate-principal-policy --policy-source-arn <arn> --action-names s3:GetObject --resource-arns arn:aws:s3:::<bucket>/<key> # implicitDeny is this causeFix
Grant
s3:GetObjectonarn:aws:s3:::<bucket>/*(or the prefix you need).The key does not exist and you lack
s3:ListBucket. S3 then answers 403 instead of 404, so a missing object looks like a permission problem.Check
aws s3 ls s3://<bucket>/<prefix>/ # with an identity that can list: the key is not thereFix
Fix the key name, and grant
s3:ListBucketon the bucket so a missing key shows as 404.The object is owned by another account (Object Ownership is ObjectWriter and another account uploaded it), so the bucket owner's policies do not apply to it.
Check
aws s3api get-bucket-ownership-controls --bucket <bucket> # ObjectWriter or BucketOwnerPreferred, with cross-account uploads, is this causeFix
Set Object Ownership to BucketOwnerEnforced, and copy existing objects over themselves to take ownership.
The object is KMS-encrypted and the key policy does not allow you to decrypt.
Check
aws s3api head-object --bucket <bucket> --key <key> # with an identity that can read it: SSEKMSKeyId names the key to checkFix
Allow
kms:Decryptfor your role in that key's policy.A bucket policy Deny, or a VPC endpoint policy, blocks the request.
Check
aws s3api get-bucket-policy --bucket <bucket> --query Policy --output text # a Deny whose condition you fail (aws:SecureTransport, aws:SourceVpce)Fix
Meet the condition, or narrow the Deny.
Prevent it
- BucketOwnerEnforced on every bucket; one KMS key policy per data set, reviewed
Now learn it: break and fix it yourself in 403 on objects another account uploaded · KMS key policy blocks decrypt, then take Level 09 · S3: buckets, access and versioning. Open in the site
IllegalLocationConstraintException
Error text
An error occurred (IllegalLocationConstraintException) when calling the CreateBucket operation: The unspecified location constraint is incompatible for the region specific endpoint this request was sent to.
What it means
Outside us-east-1, CreateBucket needs the Region repeated in the request as a LocationConstraint. The request reached a Regional endpoint without one, so S3 refused it.
Causes, most likely first
aws s3api create-bucket --region eu-west-1was run without--create-bucket-configuration.Check
aws configure get region # anything other than us-east-1, with no LocationConstraint in your command, is this causeFix
Add
--create-bucket-configuration LocationConstraint=<region>, or useaws s3 mb s3://<bucket> --region <region>, which adds it for you.
Prevent it
- Create buckets from templates
Now learn it: break and fix it yourself in BucketAlreadyExists, then take Level 09 · S3: buckets, access and versioning. Open in the site
NoSuchKey, or an object that vanished
Error text
An error occurred (NoSuchKey) when calling the GetObject operation: The specified key does not exist.
What it means
The bucket exists and you may list it, but no current object has that key. In a versioned bucket, a deleted object still exists behind a delete marker.
Causes, most likely first
A typo or a different prefix. Keys are exact and case-sensitive, and
reports/2026/a.csvis notreports/2026//a.csv.Check
aws s3 ls s3://<bucket>/<prefix>/ # the real key differs from the one requestedFix
Use the exact key.
The object was deleted. In a versioned bucket it is hidden behind a delete marker.
Check
aws s3api list-object-versions --bucket <bucket> --prefix <key> # a DeleteMarkers entry with IsLatest true is this causeFix
Delete the delete marker (
aws s3api delete-object --bucket <bucket> --key <key> --version-id <marker-id>) or copy the previous version back.
Prevent it
- Versioning plus a lifecycle rule for noncurrent versions on important buckets
Now learn it: break and fix it yourself in Recover a deleted object, then take Level 09 · S3: buckets, access and versioning. Open in the site
Blocked by CORS policy: No 'Access-Control-Allow-Origin' header
Error text
Access to fetch at 'https://nimbus-kitchen-media.s3.us-east-1.amazonaws.com/menu.json' from origin 'https://shop.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
CORSResponse: This CORS request is not allowed. This is usually because the evalution of Origin, request method / Access-Control-Request-Method or Access-Control-Request-Headers are not whitelisted by the resource's CORS spec.
An error occurred (NoSuchCORSConfiguration) when calling the GetBucketCors operation: The CORS configuration does not exist
What it means
The browser fetched from a different origin (scheme, host and port) than the page, and the bucket's answer did not say your site may read it. The browser then hides the response from your code. The request may even have succeeded on S3.
Causes, most likely first
The bucket has no CORS configuration, so S3 sends no Access-Control-Allow-Origin header.
Check
aws s3api get-bucket-cors --bucket <bucket> # NoSuchCORSConfiguration is this causeFix
Add one with
aws s3api put-bucket-cors, allowing your site's origin and the methods it uses.AllowedOrigins does not match your site exactly, including scheme and port.
Check
curl -s -D - -o /dev/null -H 'Origin: https://shop.example.com' '<object or presigned URL>' | grep -i access-control # no Access-Control-Allow-Origin line for your origin is this causeFix
Put the exact origin in AllowedOrigins, for example
https://shop.example.com, rather than "*".The method or headers are not allowed, for example a PUT with Content-Type. The browser's preflight (OPTIONS) is then refused.
Check
curl -s -D - -o /dev/null -X OPTIONS -H 'Origin: https://shop.example.com' -H 'Access-Control-Request-Method: PUT' -H 'Access-Control-Request-Headers: content-type' '<url>' # 403, or no allow headers, is this causeFix
Add the method to AllowedMethods and the header to AllowedHeaders.
Prevent it
- Keep CORS rules in infrastructure as code next to the site that needs them; avoid "*" origins
Now learn it: break and fix it yourself in S3 CORS error in the browser, then take Level 09 · S3: buckets, access and versioning. Open in the site
S3: the region is wrong, or PermanentRedirect
Error text
An error occurred (AuthorizationHeaderMalformed) when calling the ListObjectsV2 operation: The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'eu-west-1'
An error occurred (PermanentRedirect) when calling the ListObjects operation: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint.
What it means
The bucket lives in a different Region than the one the request was signed for or sent to. The first message names the Region S3 expected (expecting 'eu-west-1').
Causes, most likely first
The profile's Region differs from the bucket's.
Check
aws s3api get-bucket-location --bucket <bucket> # the bucket's Region; an empty or null LocationConstraint means us-east-1 aws configure get region # differs from the bucket's: this is your causeFix
Send the request to the bucket's Region:
aws s3 ls s3://<bucket> --region eu-west-1.An SDK client was created for a fixed Region, or a hard-coded endpoint URL points at another Region.
Check
grep -rn "region_name\|endpoint_url" . # a Region or endpoint that is not the bucket'sFix
Create the client for the bucket's Region, and keep each bucket's Region in your config next to its name.
Prevent it
- Keep each bucket's Region in your config next to its name
Now learn it: take Level 09 · S3: buckets, access and versioning. Open in the site
ECS ResourceInitializationError: unable to pull secrets or registry auth
Error text
ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to retrieve ecr registry auth: service call has been retried 3 time(s): RequestError: send request failed caused by: Post "https://api.ecr.us-east-1.amazonaws.com/": dial tcp 52.46.157.62:443: i/o timeout. Please check your task network configuration.
CannotPullContainerError: ref pull has been retried 5 time(s): failed to copy: httpReadSeeker: failed open: failed to do request: Get "https://prod-us-east-1-starport-layer-bucket.s3.us-east-1.amazonaws.com/...": dial tcp 52.216.40.12:443: i/o timeout
What it means
Before a Fargate task starts, ECS uses the task's network interface to fetch the image from ECR (layers come from S3) and any secrets. i/o timeout means those calls never got through, so the image never downloaded and the task stopped.
Causes, most likely first
The task runs in private subnets with no NAT gateway and no VPC endpoints, so it has no way to reach ECR.
Check
aws ecs describe-tasks --cluster <cluster> --tasks <task-arn> --query 'tasks[].stoppedReason' # ResourceInitializationError with i/o timeout aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=<subnet-id> # no 0.0.0.0/0 to a nat- gateway, and no endpoints below: this is your causeFix
Route the subnets through a NAT gateway, or add interface endpoints for ecr.api and ecr.dkr (and logs, secretsmanager if used) and a gateway endpoint for S3.
There are endpoints for ecr.api and ecr.dkr, but no S3 gateway endpoint. The image manifest is fetched, then the layer download from S3 times out.
Check
aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values=<vpc-id> --query 'VpcEndpoints[].[ServiceName,VpcEndpointType]' # ecr endpoints present, com.amazonaws.<region>.s3 Gateway missingFix
Add the S3 gateway endpoint and associate it with the subnets' route tables.
The task is in a public subnet but has no public IP, so the internet gateway route cannot be used.
Check
aws ecs describe-services --cluster <cluster> --services <service> --query 'services[].networkConfiguration.awsvpcConfiguration.assignPublicIp' # DISABLED in a public subnet is this causeFix
Prefer private subnets with NAT or endpoints. Otherwise set assignPublicIp to ENABLED.
The interface endpoints' security group does not allow 443 from the tasks.
Check
aws ec2 describe-vpc-endpoints --query 'VpcEndpoints[].Groups' # then describe-security-groups on that group: no inbound 443 from the task's security groupFix
Allow 443 on the endpoint security group from the tasks' security group.
Prevent it
- Build the endpoints into the VPC template for every private workload
Now learn it: break and fix it yourself in ECS task cannot reach ECR, then take Level 19 · Containers: ECS, ECR and EKS. Open in the site
ECS: execution role is not authorized to perform ecr:GetAuthorizationToken
Error text
AccessDeniedException: User: arn:aws:sts::111122223333:assumed-role/ecsTaskExecutionRole/... is not authorized to perform: ecr:GetAuthorizationToken on resource: * because no identity-based policy allows the ecr:GetAuthorizationToken action
What it means
ECS pulls the image and reads secrets using the task execution role, not the task role your code uses. That role lacks the permission named in the message, so the task cannot start.
Causes, most likely first
AmazonECSTaskExecutionRolePolicyis not attached to the execution role, or the task definition has no execution role.Check
aws ecs describe-task-definition --task-definition <family:revision> --query 'taskDefinition.[executionRoleArn,taskRoleArn]' # executionRoleArn empty or not the role you think aws iam list-attached-role-policies --role-name <execution-role> # AmazonECSTaskExecutionRolePolicy missing is this causeFix
Attach
arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicyto the execution role, then force a new deployment.The permissions were added to the task role instead. The task role is for your application's calls, after the container starts.
Check
aws iam list-attached-role-policies --role-name <task-role> # the ECR permissions sit here, not on the execution roleFix
Move them to the execution role.
The image is in another account and its ECR repository policy does not allow this role.
Check
aws ecr get-repository-policy --repository-name <repo> --registry-id <account-id> # run in the owning account: no statement for your execution roleFix
Allow
ecr:BatchGetImageandecr:GetDownloadUrlForLayerfor the role in the repository policy.
Prevent it
- Keep execution and task roles separate and named for what they do
Now learn it: break and fix it yourself in ECS task: execution role cannot pull, then take Level 19 · Containers: ECS, ECR and EKS. Open in the site
kubectl: error: You must be logged in to the server (Unauthorized)
Error text
error: You must be logged in to the server (Unauthorized)
What it means
kubectl sent a token for your IAM identity, and the cluster did not map that identity to any Kubernetes user. So the cluster treats you as nobody: there is no access entry (and no aws-auth mapping) for the role you used.
Causes, most likely first
There is no access entry for your role.
Check
aws sts get-caller-identity # for assumed-role/<role>/<session>, the role is what the access entry must name aws eks list-access-entries --cluster-name <cluster> # that role's ARN missing is this causeFix
aws eks create-access-entry --cluster-name <cluster> --principal-arn <role-arn>, thenaws eks associate-access-policywith only the access it needs.kubeconfig gets its token with a different profile, role, cluster or Region than you think.
Check
kubectl config view --minify -o jsonpath='{.users[0].user.exec.args}' # the cluster, Region and profile or role kubectl gets its token forFix
Run
aws eks update-kubeconfig --name <cluster> --region <region> --profile <profile>with the identity that has access.The access entry or aws-auth mapping names the session ARN (
arn:aws:sts::...:assumed-role/...) instead of the role ARN (arn:aws:iam::...:role/...). The session ARN never matches.Check
aws eks list-access-entries --cluster-name <cluster> # an entry with arn:aws:sts:: is this causeFix
Use the role ARN.
The cluster uses only the aws-auth ConfigMap, and the identity that created it was deleted, so nobody can sign in.
Check
aws eks describe-cluster --name <cluster> --query cluster.accessConfig # authenticationMode CONFIG_MAPFix
Switch the authentication mode to API_AND_CONFIG_MAP (
aws eks update-cluster-config --access-config authenticationMode=API_AND_CONFIG_MAP) and create access entries.
Prevent it
- Use the API authentication mode with access entries managed in code; never depend on the cluster creator's identity
Now learn it: break and fix it yourself in kubectl: You must be logged in (Unauthorized), then take Level 19 · Containers: ECS, ECR and EKS. Open in the site
kubectl: Error from server (Forbidden)
Error text
Error from server (Forbidden): pods is forbidden: User "arn:aws:sts::111122223333:assumed-role/OnCall/you" cannot list resource "pods" in API group "" at the cluster scope
What it means
Kubernetes knows who you are (the message names your role), but no access policy or RBAC binding allows that verb on that resource in that scope.
Causes, most likely first
The access entry exists but has no access policy associated.
Check
aws eks list-associated-access-policies --cluster-name <cluster> --principal-arn <role-arn> # empty is this causeFix
aws eks associate-access-policy --cluster-name <cluster> --principal-arn <role-arn> --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy --access-scope type=cluster.The policy is scoped to some namespaces and you asked about another, or about all of them (
-A).Check
aws eks list-associated-access-policies --cluster-name <cluster> --principal-arn <role-arn> # accessScope type namespace, without the one you used kubectl auth can-i list pods -n <namespace> # yes here, no with -AFix
Add the namespace to the scope, or use cluster scope for read-only on-call access.
The entry maps you to Kubernetes groups, and no RoleBinding or ClusterRoleBinding gives those groups the permission.
Check
kubectl auth can-i --list # what your identity may do; the verb and resource are missingFix
Create a RoleBinding for the group with the least role that works.
Prevent it
- Grant read-only cluster-wide for on-call, edit or admin only where needed
Now learn it: break and fix it yourself in kubectl: You must be logged in (Unauthorized), then take Level 19 · Containers: ECS, ECR and EKS. Open in the site
docker push to ECR: no basic auth credentials
Error text
no basic auth credentials
denied: Your authorization token has expired. Reauthenticate and try again.
What it means
Docker has no valid login for that ECR registry. An ECR login is a token that lasts 12 hours and is for one registry: one account and one Region. no basic auth credentials means no login at all for that registry address.
Causes, most likely first
The login was never run, or was for another account or Region than the image tag.
Check
grep -o '[0-9]\{12\}\.dkr\.ecr\.[a-z0-9-]*\.amazonaws\.com' ~/.docker/config.json # registry addresses you are logged in to: the tag's address missing is this causeFix
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com, with the account ID and Region from the image tag.The login is more than 12 hours old.
Check
docker pull <account-id>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag> # denied: Your authorization token has expiredFix
Log in again, or use the Amazon ECR credential helper so Docker fetches tokens itself.
Docker runs with sudo, which uses root's Docker login, not yours.
Check
sudo ls /root/.docker/config.json # missing, or without the registry, while you logged in as yourselfFix
Log in and push as the same user, or add yourself to the docker group.
Your identity cannot get a token or push to the repository.
Check
aws ecr get-authorization-token --query 'authorizationData[].expiresAt' # AccessDenied: you lack ecr:GetAuthorizationTokenFix
Allow
ecr:GetAuthorizationToken(Resource "*"), plus push permissions on that repository only.
Prevent it
- In CI, log in at the start of every job
- Use the Amazon ECR credential helper so Docker fetches tokens itself
Now learn it: take Level 19 · Containers: ECS, ECR and EKS. Open in the site
InvalidViewerCertificate: certificate isn't in us-east-1
Error text
An error occurred (InvalidViewerCertificate) when calling the UpdateDistribution operation: The specified SSL certificate doesn't exist, isn't in us-east-1 region, isn't valid, or doesn't include a valid certificate chain.
What it means
CloudFront only accepts ACM certificates from us-east-1, and only ones that are issued and cover the distribution's alternate domain names. The certificate ARN you gave fails one of those.
Causes, most likely first
The certificate was requested in the app's Region (eu-west-1, for example). CloudFront cannot see it.
Check
aws acm list-certificates --region us-east-1 # your certificate missing here is this causeFix
Request (or import) the certificate in us-east-1 and use that ARN in ViewerCertificate.
The certificate is still PENDING_VALIDATION.
Check
aws acm describe-certificate --region us-east-1 --certificate-arn <arn> --query Certificate.Status # anything but ISSUEDFix
Create its validation record (see the ACM entry), then update the distribution.
It does not cover every alias on the distribution.
Check
aws acm describe-certificate --region us-east-1 --certificate-arn <arn> --query Certificate.SubjectAlternativeNames # an alias not listedFix
Request a certificate that covers all the aliases.
Prevent it
- Create CloudFront certificates in us-east-1 from the same template as the distribution
Now learn it: break and fix it yourself in CloudFront: certificate in the wrong Region, then take Level 21 · Edge: CloudFront and ACM. Open in the site
ACM certificate stuck in PENDING_VALIDATION
Error text
"Status": "PENDING_VALIDATION"
"ValidationStatus": "PENDING_VALIDATION"
What it means
ACM will not issue the certificate until it can see a specific CNAME record in public DNS, which proves you control the domain. It is still waiting for that record.
Causes, most likely first
The CNAME from DomainValidationOptions was never created.
Check
aws acm describe-certificate --certificate-arn <arn> --query 'Certificate.DomainValidationOptions[].ResourceRecord' # the record ACM wants dig +short <validation record name> CNAME # nothing returned is this causeFix
Create that exact CNAME in the domain's hosted zone (change-resource-record-sets UPSERT). ACM issues within minutes.
It was created in the wrong hosted zone, or with a typo.
Check
dig +short <validation record name> CNAME # a different value, or nothing, though you created itFix
Create it in the zone the domain is delegated to, with the exact name and value.
The domain's delegation is broken, so public DNS cannot see the zone.
Check
dig +short NS <domain> # not the hosted zone's four name serversFix
Fix the registrar's name servers (see the SERVFAIL entry).
Prevent it
- Keep the validation record: ACM uses it again for automatic renewal
Now learn it: break and fix it yourself in CloudFront: certificate in the wrong Region, then take Level 21 · Edge: CloudFront and ACM. Open in the site
CloudFront still serves the old version
Error text
X-Cache: Hit from cloudfront
What it means
CloudFront edge locations serve their cached copy until its TTL expires or you invalidate the path. X-Cache: Hit from cloudfront means this response came from the cache, not from your origin.
Causes, most likely first
The files keep the same name across releases and have long TTLs, so edges keep the old copy.
Check
curl -sI https://<site>/index.html | grep -i '^\(x-cache\|age\|cache-control\)' # Hit with a large Age is this causeFix
aws cloudfront create-invalidation --distribution-id <id> --paths /index.html(or "/*").The deploy ran no invalidation.
Check
aws cloudfront list-invalidations --distribution-id <dist-id> # none since the deployFix
Add an invalidation of the entry points to the deploy step.
Prevent it
- Versioned file names for assets with long TTLs; short TTLs and invalidation for entry points
Now learn it: break and fix it yourself in CloudFront serves the old version, then take Level 21 · Edge: CloudFront and ACM. Open in the site
SSL: no alternative certificate subject name matches target host name
Error text
curl: (60) SSL: no alternative certificate subject name matches target host name 'www.example.com'
NET::ERR_CERT_COMMON_NAME_INVALID
What it means
The server presented a TLS certificate that does not list the host name you asked for, so the client stopped the connection. Nothing was wrong with the network.
Causes, most likely first
The distribution still uses the default
*.cloudfront.netcertificate.Check
aws cloudfront get-distribution-config --id <dist-id> --query DistributionConfig.ViewerCertificate # CloudFrontDefaultCertificate true is this causeFix
Attach a us-east-1 ACM certificate that covers the name.
The name is missing from the distribution's aliases or from the certificate.
Check
aws cloudfront get-distribution-config --id <dist-id> --query DistributionConfig.Aliases # your name not listedFix
Add the name as an alias, with a certificate that covers it.
DNS points at the wrong distribution or load balancer.
Check
dig +short <name> # not your distribution's d123.cloudfront.net addressFix
Point the record (an alias) at the right distribution.
Prevent it
- Monitor certificate coverage and expiry with a synthetic check
Now learn it: break and fix it yourself in CloudFront: certificate in the wrong Region, then take Level 21 · Edge: CloudFront and ACM. Open in the site
502 Bad Gateway from a load balancer
Error text
HTTP/1.1 502 Bad Gateway
Server: awselb/2.0
What it means
The load balancer reached a target but did not get a valid HTTP response: the connection was refused or reset, or the answer was malformed. Server: awselb/2.0 shows the 502 came from the load balancer, not from your app.
Causes, most likely first
Targets are registered on a port where nothing listens.
Check
aws elbv2 describe-target-groups --names <tg> --query 'TargetGroups[].Port' # the port traffic goes to aws elbv2 describe-target-health --target-group-arn <tg-arn> # unhealthy with Target.FailedHealthChecks, and nothing listens on that port on the instanceFix
Register the targets on the port the app uses.
The application crashed or closed the connection mid-request.
Check
aws elbv2 describe-target-health --target-group-arn <tg-arn> # healthy, yet 502s come and goFix
Read the application's logs for crashes at those times and fix the app.
The app's keep-alive timeout is shorter than the load balancer's idle timeout, so the app closes connections the load balancer still reuses.
Check
aws elbv2 describe-load-balancer-attributes --load-balancer-arn <alb-arn> --query "Attributes[?Key=='idle_timeout.timeout_seconds']" # longer than the app's keep-alive (often 5 s in Node.js) is this causeFix
Set the app's keep-alive above the load balancer's idle timeout.
Prevent it
- Health checks on the traffic port, and ELB access logs
Now learn it: break and fix it yourself in ALB returns 502, then take Level 10 · Load balancers and Auto Scaling. Open in the site
503 Service Temporarily Unavailable from a load balancer
Error text
HTTP/1.1 503 Service Temporarily Unavailable
<title>503 Service Temporarily Unavailable</title>
What it means
The load balancer accepted the request but had no target to send it to: the target group is empty, or the rule points at one with nothing registered.
Causes, most likely first
The target group has no registered targets.
Check
aws elbv2 describe-target-health --target-group-arn <tg-arn> # an empty list is this causeFix
Register targets:
aws elbv2 register-targets --target-group-arn <tg-arn> --targets Id=<instance-id>.The listener rule points at a different, empty target group.
Check
aws elbv2 describe-listeners --load-balancer-arn <alb-arn> # the TargetGroupArn in DefaultActions; then describe-rules for each listenerFix
Point the rule at the target group that has targets.
The Auto Scaling group is attached to another target group.
Check
aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names <asg> --query 'AutoScalingGroups[].TargetGroupARNs' # not the listener's target groupFix
Attach the Auto Scaling group to the right target group.
Prevent it
- Attach Auto Scaling groups to target groups in the same template as the listener
Now learn it: break and fix it yourself in ALB returns 503, then take Level 10 · Load balancers and Auto Scaling. Open in the site
504 Gateway Time-out from a load balancer
Error text
HTTP/1.1 504 Gateway Time-out
"Reason": "Target.Timeout"
What it means
The load balancer could not open a connection to the target, or the target did not answer within the idle timeout. Target.Timeout says the health check connection timed out too.
Causes, most likely first
The target's security group does not allow the load balancer's security group.
Check
aws ec2 describe-security-groups --group-ids <target-sg> # no inbound rule for the target port from the load balancer's security group is this causeFix
Allow the target port from the load balancer's security group.
A NACL drops the traffic between the load balancer's subnets and the targets.
Check
aws ec2 describe-network-acls --filters Name=association.subnet-id,Values=<subnet-id> # a DENY or no rule for the port and the ephemeral portsFix
Allow the port and the ephemeral ports in both directions.
The app takes longer than the idle timeout to answer.
Check
aws elbv2 describe-load-balancer-attributes --load-balancer-arn <alb-arn> --query "Attributes[?Key=='idle_timeout.timeout_seconds']" # shorter than your slowest requestFix
Make the request faster, or raise the idle timeout for it.
Prevent it
- Reference the load balancer's security group in the targets' rules
Now learn it: break and fix it yourself in ALB returns 504, then take Level 10 · Load balancers and Auto Scaling. Open in the site
Targets unhealthy: health checks failed
Error text
"Reason": "Target.ResponseCodeMismatch", "Description": "Health checks failed with these codes: [404]"
"Reason": "Target.FailedHealthChecks"
What it means
The load balancer's health check did not get the answer it expects from the target. ResponseCodeMismatch ... [404] means it got an HTTP answer, just not one in the success codes.
Causes, most likely first
The health check path does not exist on the app.
Check
aws elbv2 describe-target-groups --names <tg> --query 'TargetGroups[].[HealthCheckPath,HealthCheckPort,Matcher.HttpCode]' # the path being checked curl -s -o /dev/null -w '%{http_code}\n' http://<target-ip>:<port><path> # 404 is this causeFix
Point the health check at a path that returns 200 when the app can serve.
The app redirects on that path (301 or 302), for example to HTTPS or a login page.
Check
curl -s -o /dev/null -w '%{http_code}\n' http://<target-ip>:<port><path> # 301 or 302 is this causeFix
Use a path that answers 200 without redirecting.
The health check uses the wrong port.
Check
aws elbv2 describe-target-groups --names <tg> --query 'TargetGroups[].HealthCheckPort' # not the port the app listens onFix
Set it to traffic-port or the app's port. Do not widen the matcher to hide failures.
Prevent it
- A dedicated /health endpoint; do not widen the matcher to hide failures
Now learn it: break and fix it yourself in Targets unhealthy: health check path · ALB returns 502, then take Level 10 · Load balancers and Auto Scaling. Open in the site
Domain does not resolve: SERVFAIL
Error text
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL
** server can't find shop.example.com: SERVFAIL
What it means
Resolvers asked the domain's authoritative name servers and got no usable answer. SERVFAIL means the delegation or the zone is broken, not that one record is missing (that would be NXDOMAIN or an empty answer).
Causes, most likely first
The registrar points at the name servers of a deleted or different hosted zone.
Check
aws route53domains get-domain-detail --domain-name <domain> --query Nameservers # the name servers the registrar gives out aws route53 get-hosted-zone --id <zone-id> --query DelegationSet.NameServers # different from the line above: this is your causeFix
Update the registrar's name servers to the hosted zone's four.
DNSSEC is misconfigured: a DS record at the registrar that does not match the zone's signing key.
Check
dig +dnssec <domain> @1.1.1.1 # SERVFAIL here but an answer with +cd (checking disabled) points to DNSSECFix
Fix the DS record, or remove it before you change or disable signing.
Prevent it
- Protect production hosted zones from deletion; reusable delegation sets
Now learn it: break and fix it yourself in Domain does not resolve: name servers, then take Level 12 · Route 53 and DNS. Open in the site
Route 53 record points at the wrong place
Error text
dig +short shop.example.com returns an old IP address
What it means
DNS answers, but with an address where your service no longer runs.
Causes, most likely first
A records hold a load balancer's or an old instance's IP. Load balancer IPs change, and instances get new ones.
Check
aws route53 list-resource-record-sets --hosted-zone-id <zone-id> --query "ResourceRecordSets[?Name=='<name>.']" # an A record with ResourceRecords instead of AliasTarget is this causeFix
Use an alias record to the load balancer (or a CNAME below the apex).
Resolvers still cache the old answer until its TTL ends.
Check
dig <name> @<one-of-the-zone-name-servers> # the new address here, but the old one from your resolver: cachedFix
Wait for the TTL to end. Before a planned change, lower the TTL first.
Prevent it
- Records created by the same template as the load balancer
Now learn it: break and fix it yourself in DNS points at an old server, then take Level 12 · Route 53 and DNS. Open in the site
Connection timed out to RDS
Error text
Error: connect ETIMEDOUT 10.0.11.25:5432
psql: error: connection to server at "orders-db..." (10.0.11.25), port 5432 failed: Connection timed out
ERROR 2003 (HY000): Can't connect to MySQL server on 'orders-db...:3306' (110)
What it means
Your client sent a connection request to the database and got no answer at all. Packets were dropped on the way, which almost always means a security group or the network path, not the database itself.
Causes, most likely first
The database's security group does not allow the client's security group on the database port.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].VpcSecurityGroups' # the database's security group aws ec2 describe-security-groups --group-ids <db-sg> --query 'SecurityGroups[].IpPermissions' # no rule for 5432 or 3306 from the client's security group is this causeFix
Allow the port from the client's security group, never from 0.0.0.0/0.
The client is outside the VPC and the database is private.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].[PubliclyAccessible,Endpoint.Address]' # false, and you connect from a laptop or another networkFix
Connect from inside the VPC (Session Manager port forwarding, a VPN), rather than making the database public.
NACLs between the subnets drop the traffic.
Check
aws ec2 describe-network-acls --filters Name=association.subnet-id,Values=<db-subnet-id> # no ALLOW for the port in, or the ephemeral ports outFix
Allow the database port and the ephemeral ports between the subnets.
Prevent it
- Reference security groups, never 0.0.0.0/0, on database ports
Now learn it: break and fix it yourself in Lambda cannot reach RDS · Database open to the internet, then take Level 11 · RDS. Open in the site
Connection refused to RDS
Error text
psql: error: connection to server at "orders-db..." (10.0.11.25), port 3306 failed: Connection refused
Error: connect ECONNREFUSED 10.0.11.25:3306
ERROR 2003 (HY000): Can't connect to MySQL server on 'orders-db...:3306' (111)
What it means
The connection reached an address and was turned away: nothing listens on that port there. The network path works; you are at the wrong port or the database is not running.
Causes, most likely first
The wrong port: 3306 is MySQL, 5432 is PostgreSQL, and an instance can use a custom port.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].[Engine,Endpoint.Port]' # a different port than the one in the message is this causeFix
Use the endpoint's port.
The instance is rebooting, stopped or failing over.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].DBInstanceStatus' # anything but availableFix
Wait for available, or start it:
aws rds start-db-instance --db-instance-identifier <db>.The host name resolves to something other than the database, such as an old endpoint or localhost.
Check
nslookup <endpoint> # not the instance's current addressFix
Use the endpoint from describe-db-instances in your configuration.
Prevent it
- Read the endpoint and port from configuration, not from memory
Now learn it: break and fix it yourself in RDS connection refused, then take Level 11 · RDS. Open in the site
RDS login fails: Access denied or password authentication failed
Error text
ERROR 1045 (28000): Access denied for user 'admin'@'10.0.12.77' (using password: YES)
psql: error: connection to server at "orders-db..." (10.0.11.25), port 5432 failed: FATAL: password authentication failed for user "app"
What it means
The network is fine: the database received your login and no account accepted it. In MySQL's message, 'admin'@'10.0.12.77' is the user name your client sent and the address the server saw you connect from (often a NAT or proxy address, not your laptop's). using password: YES means a password was sent; NO means none was. Error 1045 does not by itself mean a wrong password: the user name, the host the account allows, or how the account authenticates give the same message. PostgreSQL's password authentication failed is just as vague.
Causes, most likely first
The password is wrong or old: the secret was rotated, the master password was reset, or the app uses a copy from a config file. The server finds the account but the password does not match it.
Check
python3 -c "import os; print(repr(os.environ.get('DB_USER')), bool(os.environ.get('DB_PASSWORD')))" # in the app's environment: the user it sends, and whether a password reaches the process, without printing it aws secretsmanager describe-secret --secret-id <secret-arn> --query '[LastChangedDate,LastRotatedDate]' # changed after the app last read it: this is your causeFix
Use the current password from its secret, read by the app at start-up (and again after a failed login), not a copy in a config file.
The master password is managed in Secrets Manager, and you used the one from creation time. RDS changes it on rotation, so the old one stops working.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].[MasterUsername,MasterUserSecret.SecretArn]' # a SecretArn means Secrets Manager holds the master passwordFix
Read it from that secret. Lost it entirely?
aws secretsmanager rotate-secret --secret-id <secret-arn>. Without a managed secret:aws rds modify-db-instance --db-instance-identifier <db> --master-user-password '<new-password>' --apply-immediately.No account exists for that user at that host. MySQL accounts are user@host, so
appfrom 10.0.12.77 only gets in if one of app's host patterns matches that address.Check
SELECT user, host, plugin, ssl_type FROM mysql.user WHERE user IN ('app', ''); -- as the master user: if no host for app matches the address in the message, this is your causeFix
Create an account for the network the app connects from, not
'%':CREATE USER 'app'@'10.0.%' IDENTIFIED BY '<password>';, then grant only what it needs.Another account matched first. MySQL picks the account by its host-matching rules (the most specific host first), so a more specific host row for the same user, or an anonymous
''account, can win and carry a different password.Check
mysql -h db.example.internal -P 3306 -u app -p -e 'SELECT USER(), CURRENT_USER();' # where a login works: USER() is the identity the client supplied (user@client-host as the server sees it); CURRENT_USER() is the account actually used to authenticate. If they differ, or CURRENT_USER() is ''@..., another account matched SELECT user, host FROM mysql.user WHERE user IN ('app', '') ORDER BY host; -- two or more rows that could match your addressFix
Fix that row's password, or drop it if nothing needs it. Adding yet another
approw does not help while a more specific one still wins.The user name is wrong, or DB_USER or DB_PASSWORD is empty in the app's environment. With an empty password the message says
using password: NO.Check
python3 -c "import os; print(repr(os.environ.get('DB_USER')), bool(os.environ.get('DB_PASSWORD')))" # None or '' for the user, or False for the password, is this causeFix
Set both from the secret in the app's configuration (task definition, function settings), by reference rather than by value.
The account authenticates another way: it was set up for IAM database authentication, or requires TLS. The server rejects a plain password for it with the same error.
Check
SELECT user, host, plugin, ssl_type FROM mysql.user WHERE user = 'app'; -- MySQL: AWSAuthenticationPlugin means IAM tokens only; ssl_type ANY or X509 means TLS is required SELECT pg_has_role('app', 'rds_iam', 'member'); -- PostgreSQL: true means it logs in with IAM tokens onlyFix
IAM user: connect with a token (see RDS IAM database authentication), or use a separate user with a password. TLS required: connect with TLS (see RDS requires TLS).
PostgreSQL: the role does not exist, or its password has passed
VALID UNTIL. PostgreSQL gives the same message for both, so it does not reveal which users exist.Check
SELECT rolname, rolvaliduntil FROM pg_roles WHERE rolname = 'app'; -- no row: no such role; a past rolvaliduntil: the password expiredFix
Create the role (
CREATE ROLE app LOGIN PASSWORD '<password>';), or set a new password and expiry:ALTER ROLE app PASSWORD '<password>' VALID UNTIL '<date>';. Do not switch the app to the master user to get past the error: it hides the cause and gives the app every privilege.
Prevent it
- Read the password from Secrets Manager at start-up, and again when a login fails
- One database user per application, with only the grants it needs
- After each rotation or user change, test a login as the app's own user from the app's subnet, and check that
CURRENT_USER()is the account you meant
Now learn it: break and fix it yourself in Secret rotation broke the app, then take Level 11 · RDS. Open in the site
RDS IAM database authentication: PAM authentication failed
Error text
psql: error: connection to server at "orders-db..." (10.0.11.25), port 5432 failed: FATAL: PAM authentication failed for user "app"
What it means
With IAM database authentication the password is a short-lived token from aws rds generate-db-auth-token. PostgreSQL reports a bad or refused token as PAM authentication failed; MySQL reports it as error 1045, Access denied.
Causes, most likely first
The token is more than 15 minutes old.
Check
grep -n generate-db-auth-token <your-code> # the token made once at start-up and reused is this causeFix
Make a fresh token just before each connection opens.
The token was made for another host name, port, Region or user than the connection uses.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].[Endpoint.Address,Endpoint.Port]' # compare with the --hostname, --port and --username used for the tokenFix
aws rds generate-db-auth-token --hostname <endpoint> --port 5432 --region <region> --username app, with exactly the values the connection uses.The IAM identity has no
rds-db:connectonarn:aws:rds-db:<region>:<account-id>:dbuser:<DbiResourceId>/<user>. The token is signed, but the database rejects it.Check
aws iam simulate-principal-policy --policy-source-arn <role-arn> --action-names rds-db:connect --resource-arns arn:aws:rds-db:<region>:<account-id>:dbuser:<DbiResourceId>/app # implicitDeny is this causeFix
Allow
rds-db:connecton that dbuser ARN (it uses DbiResourceId, not the instance name).IAM database authentication is not turned on for the instance.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].[IAMDatabaseAuthenticationEnabled,DbiResourceId]' # false is this causeFix
aws rds modify-db-instance --db-instance-identifier <db> --enable-iam-database-authentication --apply-immediately.The database user is not set up for IAM authentication.
Check
SELECT pg_has_role('app', 'rds_iam', 'member'); -- PostgreSQL: false is this cause SELECT user, host, plugin FROM mysql.user WHERE user = 'app'; -- MySQL: a plugin other than AWSAuthenticationPlugin is this causeFix
PostgreSQL:
GRANT rds_iam TO app;. MySQL:CREATE USER app IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';.
Prevent it
- Generate the token in code each time a connection opens; never store it
Now learn it: take Level 11 · RDS. Open in the site
RDS requires TLS: no pg_hba.conf entry, no encryption
Error text
psql: error: connection to server at "orders-db..." (10.0.11.25), port 5432 failed: FATAL: no pg_hba.conf entry for host "10.0.12.77", user "app", database "orders", no encryption
ERROR 3159 (HY000): Connections using insecure transport are prohibited while --require_secure_transport=ON.
What it means
The database accepts only encrypted connections, and the client connected without TLS. On RDS for PostgreSQL this is the rds.force_ssl parameter, on by default from PostgreSQL 15 (no encryption in the message); on MySQL it is require_secure_transport.
Causes, most likely first
The client or driver connects without TLS:
sslmode=disable, or a library that does not use TLS unless told to.Check
grep -rn "sslmode\|ssl_mode\|ssl=" <your-config> # disable, or no TLS setting at all, is this causeFix
Download the RDS certificate bundle (
curl -sSO https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem) and connect with TLS:psql "host=<endpoint> dbname=orders user=app sslmode=verify-full sslrootcert=global-bundle.pem", or for MySQLmysql -h <endpoint> -u app -p --ssl-mode=VERIFY_IDENTITY --ssl-ca=global-bundle.pem.An engine upgrade or a new parameter group turned TLS enforcement on, so clients that worked before now fail.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].[EngineVersion,DBParameterGroups]' # a recent upgrade to PostgreSQL 15 or later, or a changed group aws rds describe-db-parameters --db-parameter-group-name <group> --query "Parameters[?ParameterName=='rds.force_ssl'||ParameterName=='require_secure_transport'].[ParameterName,ParameterValue]" # 1 or ON: TLS is requiredFix
Fix the clients to use TLS as above. Keep enforcement on rather than turning it off.
Prevent it
- Keep TLS on, and put
sslmode=verify-full(or the driver's equivalent) in every connection string
Now learn it: take Level 11 · RDS. Open in the site
RDS: database does not exist, or Unknown database
Error text
FATAL: database "orders" does not exist
ERROR 1049 (42000): Unknown database 'orders'
What it means
You reached the server and logged in, but there is no database with that name on it. An RDS instance is a database server; a database inside it is only created for you if you named one at launch.
Causes, most likely first
The instance was created without an initial database name (
--db-name). PostgreSQL then has onlypostgres; MySQL has none of yours.Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].DBName' # null is this causeFix
Create it:
CREATE DATABASE orders;, ideally in the first migration.A typo in the name, or psql was run with no dbname: it then connects to a database named after the user (
app).Check
psql "host=<endpoint> user=<user> dbname=postgres" -c '\l' # PostgreSQL: the names that exist mysql -h <endpoint> -u <user> -p -e 'SHOW DATABASES;' # MySQL: the names that existFix
Use the exact name, and always pass
dbname=in connection strings.You connected to another instance, or to a restored copy.
Check
aws rds describe-db-instances --query 'DBInstances[].[DBInstanceIdentifier,Endpoint.Address]' # the endpoint you used belongs to another instanceFix
Point the app at the right endpoint.
Prevent it
- Create the database in the same code that creates the instance:
--db-name, or the first migration
Now learn it: take Level 11 · RDS. Open in the site
RDS: Too many connections
Error text
ERROR 1040 (HY000): Too many connections
FATAL: remaining connection slots are reserved for non-replication superuser connections
FATAL: sorry, too many clients already
What it means
The database has reached max_connections and turns new connections away. On RDS the default is derived from the instance class's memory, so a small instance allows few connections.
Causes, most likely first
Many Lambda invocations or containers each open their own connections.
Check
aws cloudwatch get-metric-statistics --namespace AWS/RDS --metric-name DatabaseConnections --dimensions Name=DBInstanceIdentifier,Value=<db> --start-time <start> --end-time <end> --period 60 --statistics Maximum # spikes in step with Lambda concurrency or task countFix
Put RDS Proxy in front of the database, and in Lambda open the connection outside the handler.
Connections are opened per request and never closed (a leak).
Check
SELECT state, count(*) FROM pg_stat_activity GROUP BY state; -- PostgreSQL: many idle connections; MySQL: SHOW PROCESSLIST; with many Sleep rowsFix
Close connections when done, and reuse them from a pool.
Each process's pool is larger than the database can take in total.
Check
SHOW VARIABLES LIKE 'max_connections'; -- MySQL (PostgreSQL: SHOW max_connections;); compare with pool size times process countFix
Size every app's pool so that together they stay under
max_connections.The instance class is too small for the number of clients.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].DBInstanceClass' # a micro or small classFix
Move to a larger class, or raise
max_connectionsin the parameter group if memory allows.
Prevent it
- Alarm on DatabaseConnections well below the limit
- Size every app's pool so that together they stay under
max_connections
Now learn it: take Level 11 · RDS. Open in the site
RDS storage-full, No space left on device
Error text
could not extend file "base/16384/16397": No space left on device
DB instance orders-db has run out of storage space.
What it means
The database volume is full, so the engine cannot write. RDS sets the instance to storage-full and writes stop until there is space.
Causes, most likely first
The data grew and storage autoscaling is off, or has reached its maximum.
Check
aws rds describe-db-instances --db-instance-identifier <db> --query 'DBInstances[].[DBInstanceStatus,AllocatedStorage,MaxAllocatedStorage]' # storage-full, and no MaxAllocatedStorage (or AllocatedStorage equal to it), is this causeFix
aws rds modify-db-instance --db-instance-identifier <db> --allocated-storage <+10% or more> --max-allocated-storage <cap> --apply-immediately.Logs or temporary files are filling the disk (binary logs kept long, large sorts, replication slots holding WAL).
Check
aws cloudwatch get-metric-statistics --namespace AWS/RDS --metric-name FreeStorageSpace --dimensions Name=DBInstanceIdentifier,Value=<db> --start-time <start> --end-time <end> --period 3600 --statistics Minimum # a fast drop with no matching data growth SELECT slot_name, active FROM pg_replication_slots; -- PostgreSQL: an inactive slot holds WALFix
Drop unused replication slots, shorten binary log retention, then add storage.
Prevent it
- Storage autoscaling and an alarm on FreeStorageSpace
Now learn it: break and fix it yourself in Database storage full, then take Level 11 · RDS. Open in the site
Lambda: Task timed out after 3.00 seconds
Error text
Task timed out after 3.00 seconds
{"errorType":"Sandbox.Timedout","errorMessage":"RequestId: ... Error: Task timed out after 3.00 seconds"}
What it means
The function ran longer than its timeout setting and Lambda stopped it. The number in the message is the timeout, not how long the work needed.
Causes, most likely first
The default 3-second timeout was never changed.
Check
aws lambda get-function-configuration --function-name <function> --query '[Timeout,MemorySize]' # 3 is this causeFix
Raise the timeout above the slowest real run:
aws lambda update-function-configuration --function-name <function> --timeout 30.A downstream call (an API, a database) is slow or hangs.
Check
aws logs tail /aws/lambda/<function> --since 1h # the last log line before the timeout shows which call it was inFix
Set timeouts on downstream calls shorter than the function's, and fix or retry the slow dependency.
A function in a VPC waits for a network path that does not exist: no NAT gateway and no VPC endpoint for the service it calls.
Check
aws lambda get-function-configuration --function-name <function> --query VpcConfig # subnets set aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=<subnet-id> # no route to a NAT gateway, and no endpoint for the service: this is your causeFix
Add a NAT route or a VPC endpoint for the service, or take the function out of the VPC if it needs nothing there.
Prevent it
- Set timeouts deliberately, and alarm on the Errors and Duration metrics
Now learn it: break and fix it yourself in Lambda task timed out, then take Level 13 · Lambda. Open in the site
Lambda: TooManyRequestsException, Rate Exceeded
Error text
An error occurred (TooManyRequestsException) when calling the Invoke operation (reached max retries: 2): Rate Exceeded.
What it means
Lambda refused to run the function because it is at its concurrency limit: the function's reserved concurrency, or the account's limit for the Region. The CLI retried and gave up.
Causes, most likely first
Reserved concurrency is set to 0 (switched off) or too low.
Check
aws lambda get-function-concurrency --function-name <function> # ReservedConcurrentExecutions 0 or a small number is this causeFix
aws lambda put-function-concurrency --function-name <function> --reserved-concurrent-executions <n>, ordelete-function-concurrencyto remove the cap.The burst is above the account's concurrency limit for the Region.
Check
aws lambda get-account-settings --query 'AccountLimit.ConcurrentExecutions' # compare with the ConcurrentExecutions metric at the timeFix
Smooth the load (a queue in front), or request a higher limit.
Prevent it
- Alarm on the Throttles metric
Now learn it: break and fix it yourself in Lambda throttled: TooManyRequestsException, then take Level 13 · Lambda. Open in the site
Lambda: Runtime.ImportModuleError or Runtime.HandlerNotFound
Error text
{"errorMessage": "Unable to import module 'lambda_function': No module named 'requests'", "errorType": "Runtime.ImportModuleError"}
{"errorType":"Runtime.HandlerNotFound","errorMessage":"index.handler is undefined or not exported"}
What it means
Lambda could not load your code before running it. Either a package it imports is not in the deployment package or a layer, or the handler setting names a file or function that is not there.
Causes, most likely first
The library was installed on your machine but not zipped with the function.
Check
unzip -l function.zip # the package's folder (requests/) missing is this causeFix
pip install -r requirements.txt -t package/, thencd package && zip -r ../function.zip ., and add your code at the zip's root.The files sit inside a folder in the zip, not at its root.
Check
unzip -l function.zip # paths like myapp/lambda_function.py instead of lambda_function.pyFix
Zip from inside the folder, so the files are at the root.
The handler setting does not match the file and function.
Check
aws lambda get-function-configuration --function-name <function> --query Handler # lambda_function.lambda_handler needs lambda_function.py with def lambda_handlerFix
aws lambda update-function-configuration --function-name <function> --handler lambda_function.lambda_handler.A package with compiled code was built for another operating system or architecture.
Check
aws lambda get-function-configuration --function-name <function> --query '[Runtime,Architectures]' # arm64 or x86_64; a wheel built on macOS or for the other architecture is this causeFix
Download wheels built for Lambda:
pip install --platform manylinux2014_x86_64 --only-binary=:all: -t package/ <package>(manylinux2014_aarch64 for arm64).
Prevent it
- Build the package in CI the same way every time, or deploy a container image
- Put shared libraries in a layer
Now learn it: take Level 13 · Lambda. Open in the site
ProvisionedThroughputExceededException
Error text
ProvisionedThroughputExceededException: The level of configured provisioned throughput for the table was exceeded. Consider increasing your provisioning level with the UpdateTable API.
What it means
Requests to a provisioned table (or one of its indexes) asked for more read or write capacity units than it has, so DynamoDB throttled them. The SDK retried and gave up.
Causes, most likely first
Traffic grew past the provisioned WCU or RCU.
Check
aws dynamodb describe-table --table-name <table> --query 'Table.[BillingModeSummary,ProvisionedThroughput]' # PROVISIONED with low units aws cloudwatch get-metric-statistics --namespace AWS/DynamoDB --metric-name WriteThrottleEvents --dimensions Name=TableName,Value=<table> --start-time <t> --end-time <t> --period 300 --statistics Sum # above 0 while traffic is highFix
Switch to on-demand (
aws dynamodb update-table --table-name <table> --billing-mode PAY_PER_REQUEST) or raise capacity with auto scaling.A hot partition key concentrates writes on one partition, which throttles even when the table total is under its limit.
Check
aws cloudwatch get-metric-statistics --namespace AWS/DynamoDB --metric-name ConsumedWriteCapacityUnits --dimensions Name=TableName,Value=<table> --start-time <t> --end-time <t> --period 60 --statistics Sum # throttling while consumption is well below provisionedFix
Spread writes over a better-distributed key, for example by adding a suffix to busy keys.
A global secondary index has too little capacity. When a GSI throttles, writes to the table throttle too.
Check
aws dynamodb describe-table --table-name <table> --query 'Table.GlobalSecondaryIndexes[].[IndexName,ProvisionedThroughput]' # an index with fewer units than the table writesFix
Give the index capacity to match the table's writes.
Prevent it
- Alarm on throttle events; spread writes over well-distributed partition keys
Now learn it: break and fix it yourself in DynamoDB ProvisionedThroughputExceeded, then take Level 20 · Data services: DynamoDB, SQS and Secrets Manager. Open in the site
SQS messages processed more than once
Error text
ApproximateReceiveCount above 1
Queue visibility timeout: 30 seconds is less than Function timeout: 120 seconds
What it means
A received message stays in the queue, hidden, for its visibility timeout. If the consumer does not delete it before that ends, it becomes visible again and is delivered again. Standard queues also deliver at least once by design.
Causes, most likely first
The visibility timeout is shorter than the processing time.
Check
aws sqs get-queue-attributes --queue-url <queue-url> --attribute-names VisibilityTimeout # shorter than your slowest run (for Lambda, the message above says so) is this causeFix
Raise VisibilityTimeout above the slowest processing time; for a Lambda consumer, six times the function timeout.
The consumer deletes the message only after a slow step, or never on some code paths (errors).
Check
aws sqs get-queue-attributes --queue-url <queue-url> --attribute-names ApproximateNumberOfMessagesNotVisible # stays high while the consumer is idleFix
Delete every message you finished, including on handled errors.
Standard queues can deliver a message twice even when everything is right.
Check
aws sqs get-queue-attributes --queue-url <queue-url> --attribute-names FifoQueue # no FifoQueue: a standard queueFix
Make processing idempotent, keyed on a message or business ID.
Prevent it
- A dead-letter queue with maxReceiveCount, and alarms on its depth
Now learn it: break and fix it yourself in SQS: every email sent twice, then take Level 20 · Data services: DynamoDB, SQS and Secrets Manager. Open in the site
Database login fails after secret rotation
Error text
password authentication failed for user "orders_app"
Access denied for user 'orders_app'@'10.0.12.77' (using password: YES)
What it means
Rotation changed the password in the database and in Secrets Manager. The app still sends an older copy, so the database refuses the login.
Causes, most likely first
The password was pasted into an environment variable or config file. Rotation cannot update copies.
Check
aws lambda get-function-configuration --function-name <function> --query 'keys(Environment.Variables)' # names only: a DB_PASSWORD here is a pasted copy, and this is your causeFix
Remove the copy. Read the secret at run time (with a short cache) and grant
secretsmanager:GetSecretValueon that secret only.The app reads the secret once and caches it forever.
Check
aws secretsmanager describe-secret --secret-id <secret-id> --query LastRotatedDate # rotated after the app last startedFix
Re-read the secret when a login fails, and cache it for minutes, not for the life of the process.
The rotation Lambda failed halfway, so the database and the secret's AWSCURRENT disagree.
Check
aws secretsmanager describe-secret --secret-id <secret-id> --query VersionIdsToStages # AWSPENDING still on a version after the rotation window is this cause aws logs tail /aws/lambda/<rotation-function> --since 1d # which step failed: createSecret, setSecret, testSecret or finishSecretFix
Fix the error in the log, then
aws secretsmanager rotate-secret --secret-id <secret-id>to run it again.
Prevent it
- Never copy secrets into configuration; test rotation in staging
Now learn it: break and fix it yourself in Secret rotation broke the app, then take Level 20 · Data services: DynamoDB, SQS and Secrets Manager. Open in the site
AccessDeniedException on secretsmanager:GetSecretValue
Error text
AccessDeniedException: User: arn:aws:sts::111122223333:assumed-role/orders-api-role/orders-api is not authorized to perform: secretsmanager:GetSecretValue on resource: prod/orders/db because no identity-based policy allows the secretsmanager:GetSecretValue action
What it means
The role asked Secrets Manager for a secret's value and IAM refused. no identity-based policy allows means the role's own policies do not allow secretsmanager:GetSecretValue on that secret.
Causes, most likely first
The policy does not cover the secret's ARN. Secret ARNs end in a random six-character suffix (
prod/orders/db-AbCdEf), so a Resource without it does not match.Check
aws iam simulate-principal-policy --policy-source-arn <role-arn> --action-names secretsmanager:GetSecretValue --resource-arns <secret-arn> # implicitDeny is this causeFix
Allow
secretsmanager:GetSecretValueon the full secret ARN, or onarn:aws:secretsmanager:<region>:<account-id>:secret:prod/orders/db-*.The secret uses a customer managed KMS key the role cannot decrypt.
Check
aws secretsmanager describe-secret --secret-id <secret-arn> --query KmsKeyId # empty means the AWS managed key; a key ARN means the role also needs kms:Decrypt on itFix
Allow
kms:Decrypton that key, in the key policy or (if it delegates to IAM) in the role's policy.A resource policy on the secret denies the role. The message then says explicit deny in a resource-based policy.
Check
aws secretsmanager get-resource-policy --secret-id <secret-arn> # a Deny that matches the roleFix
Narrow the Deny, or call from where it allows (a VPC endpoint, a tagged role).
Cross-account access: the secret needs a resource policy and a customer managed KMS key, because the default aws/secretsmanager key cannot be used from another account.
Check
aws sts get-caller-identity --query Account # differs from the account in the secret ARNFix
Add a resource policy for the role, encrypt the secret with a customer managed key that allows the role, and allow both in the role's policy.
Prevent it
- Name secrets by path (prod/orders/...) and scope policies to the path with a wildcard suffix
Now learn it: break and fix it yourself in Secret rotation broke the app, then take Level 20 · Data services: DynamoDB, SQS and Secrets Manager. Open in the site
CloudFormation: stack is in ROLLBACK_COMPLETE state and can not be updated
Error text
An error occurred (ValidationError) when calling the UpdateStack operation: Stack:arn:aws:cloudformation:us-east-1:111122223333:stack/web/... is in ROLLBACK_COMPLETE state and can not be updated.
What it means
The stack's first create failed, and CloudFormation deleted everything it had made. A stack in ROLLBACK_COMPLETE cannot be updated, only deleted and created again.
Causes, most likely first
A resource failed during the first create: a name already taken, a missing permission, a bad parameter.
Check
aws cloudformation describe-stack-events --stack-name <stack> --query "StackEvents[?ResourceStatus=='CREATE_FAILED'].[LogicalResourceId,ResourceStatusReason]" # the oldest CREATE_FAILED is the real cause; later ones are usually cancelledFix
Fix the template or parameters for that reason, then
aws cloudformation delete-stack --stack-name <stack>,aws cloudformation wait stack-delete-complete --stack-name <stack>, and create it again.You deployed again without fixing what failed.
Check
aws cloudformation describe-stacks --stack-name <stack> --query 'Stacks[0].StackStatus' # ROLLBACK_COMPLETE again after the redeployFix
Read the first CREATE_FAILED reason before deploying again.
Prevent it
- Check templates before you deploy:
aws cloudformation validate-template --template-body file://template.yaml - Use change sets to see what will happen before it does
Now learn it: take Level 18 · Infrastructure as code and CI/CD. Open in the site
CloudFormation: Requires capabilities CAPABILITY_IAM
Error text
An error occurred (InsufficientCapabilitiesException) when calling the CreateStack operation: Requires capabilities : [CAPABILITY_NAMED_IAM]
What it means
The template creates IAM resources or uses a macro or transform, and CloudFormation wants you to acknowledge that explicitly. The capability named in the message is the one to pass.
Causes, most likely first
The template creates IAM roles, users, groups or policies (CAPABILITY_IAM), or gives them custom names (CAPABILITY_NAMED_IAM).
Check
grep -nE 'AWS::IAM::|RoleName|PolicyName|UserName|GroupName' template.yaml # IAM resources; a fixed name needs CAPABILITY_NAMED_IAMFix
Read which IAM resources it creates, then deploy with
--capabilities CAPABILITY_NAMED_IAM(or CAPABILITY_IAM).The template uses a transform such as AWS::Serverless (CAPABILITY_AUTO_EXPAND).
Check
grep -n 'Transform' template.yaml # a Transform line is this causeFix
Add
CAPABILITY_AUTO_EXPANDto--capabilities.
Prevent it
- Set the capabilities in the pipeline's deploy step, and review IAM changes in pull requests
Now learn it: take Level 18 · Infrastructure as code and CI/CD. Open in the site
CloudFormation: No updates are to be performed
Error text
An error occurred (ValidationError) when calling the UpdateStack operation: No updates are to be performed.
No changes to deploy. Stack web is up to date
What it means
The template and parameters are the same as what is deployed, so there is nothing to change. update-stack reports that as an error; it is not a failure of the stack.
Causes, most likely first
The pipeline ran again with no template change.
Check
aws cloudformation get-template --stack-name <stack> --query TemplateBody # identical to your templateFix
With
aws cloudformation deploy, add--no-fail-on-empty-changesetso a run with no changes exits 0.You changed a file the template only points at, such as Lambda code in S3 under the same key. CloudFormation compares the template, not the file.
Check
aws cloudformation describe-stacks --stack-name <stack> --query 'Stacks[0].Parameters' # the same S3 key or version as beforeFix
Upload the new code under a new key or object version and pass that to the template.
Prevent it
- Version build artifacts, so a code change is also a template change
Now learn it: take Level 18 · Infrastructure as code and CI/CD. Open in the site
ThrottlingException: Rate exceeded
Error text
An error occurred (ThrottlingException) when calling the DescribeInstances operation (reached max retries: 2): Rate exceeded
An error occurred (RequestLimitExceeded) when calling the DescribeInstances operation (reached max retries: 4): Request limit exceeded.
What it means
Too many API calls in a short time, for this account, Region and API. AWS refused some, and the CLI already retried and gave up. The limit is per API, so other calls may still work.
Causes, most likely first
A script polls in a tight loop.
Check
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=<operation> --max-results 50 # many calls a second from one identity or user agent is this causeFix
Slow down, use waiters (
aws ec2 wait) and events (EventBridge) instead of polling.Many tools share one account and Region, and together exceed the limit.
Check
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=<operation> --max-results 50 # many different identities calling the same APIFix
Set
AWS_RETRY_MODE=adaptiveandAWS_MAX_ATTEMPTS=10, batch calls, and spread heavy tools over time.
Prevent it
- Use waiters and events (EventBridge) instead of polling
Now learn it: break and fix it yourself in API throttling: RequestLimitExceeded, then take Level 14 · CloudWatch and CloudTrail. Open in the site