Skip to content

Common errors

An error response is part of the contract. Callers branch on error codes, so a code whose meaning changes breaks working software as surely as a removed endpoint.

Read this guide to handle errors correctly, and to know which are worth retrying.

  • None

Errors are returned as XML in the Amazon EC2 shape:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Errors>
<Error>
<Code>InvalidInstanceID.NotFound</Code>
<Message>The instance ID 'i-0a1b2c3d4e5f60718' does not exist</Message>
</Error>
</Errors>
<RequestID>b1e2c3d4-5678-90ab-cdef-1234567890ab</RequestID>
</Response>

Note the element names: <Response>, <Errors>, <Error>, and <RequestID> with that exact casing. There is no <Type> element; that belongs to the standard query protocol used by IAM, not to EC2. A client parsing EC2 errors expects this shape and no other.

The <Code> is stable and is what you branch on. The <Message> is for a human and its wording may change between releases. Never parse the message.

StatusMeaning
400The request was malformed, invalid, or expired
403The credentials are unknown, or policy denies the action
404Reserved; EC2 signals absence with a .NotFound code and a 400
409The request conflicts with the state of the resource
500An error on our side
503Temporarily unavailable, or the request rate was exceeded

We return no 401. Amazon EC2’s own common-error list does document NotAuthorized at 401, and real EC2 returns 401 for some AuthFailure cases — so this is a deliberate divergence rather than parity, and it is stated as one.

We return 403 for both. Two reasons: the AWS SDKs’ automatic clock-skew correction is keyed on a 403 or 400 carrying a recognised code, and a 401 without a WWW-Authenticate header violates HTTP and confuses intermediaries that special-case it.

OPEN (Michael): confirm the 403 choice against the SDK clock-skew handlers before shipping, since that behaviour is the stated reason for the whole table.

CodeStatusCauseRetry
AuthFailure403The credentials could not be validatedNo
IncompleteSignature400The Authorization header is malformedNo
SignatureDoesNotMatch403The signature does not match the requestNo
InvalidClientTokenId403The access key id does not exist or is inactiveNo
MissingAuthenticationToken403The request was not signedNo
RequestExpired400The timestamp is outside the permitted skewAfter fixing the clock
UnauthorizedOperation403Policy does not allow this actionNo
InvalidAction400The action does not exist in this versionNo
InvalidParameterValue400A parameter has an unacceptable valueNo
InvalidParameterCombination400Two parameters cannot be used togetherNo
MissingParameter400A required parameter is absentNo
InvalidPaginationToken400The token is invalid or has expiredNo
IdempotentParameterMismatch400A client token was reused with different parametersNo
InvalidID400An identifier is malformedNo
*.NotFound400The named resource does not existOnly if just created
*.Duplicate400A resource with that name already existsNo
*.InUse400The resource is in use and cannot be changedNo
DependencyViolation400Another resource depends on this oneNo
InstanceLimitExceeded400An account quota would be exceededNo
InsufficientInstanceCapacity500No capacity for that type right nowYes, with backoff
DryRunOperation400DryRun was set; the call would have succeededNo
Blocked403The account is suspendedNo
MalformedQueryString404The query string is not well-formedNo

InvalidParameterCombination is returned, among other cases, when a describe call is given both a list of ids and MaxResults. That combination is rejected by Amazon EC2 and is rejected here.

CodeStatusCauseRetry
InternalError500An unexpected failure on our sideYes
Unavailable503The service is temporarily unavailableYes
RequestLimitExceeded503The request rate was exceededYes, with backoff

Throttling is RequestLimitExceeded with a 503, which is what Amazon EC2 returns and what the Terraform AWS provider matches on for its backoff. A 429 would not be recognised by the installed base of older clients.

Absence, denial, and what an error must not reveal

Section titled “Absence, denial, and what an error must not reveal”

A resource belonging to another account returns the same .NotFound as a resource that has never existed. Existence is not confirmed to a caller who is not entitled to know it.

Within an account, a resource that exists but that policy forbids returns UnauthorizedOperation. The rule is therefore:

  • another account, or no such resource → .NotFound
  • this account, denied by policy → UnauthorizedOperation

No error contains internal detail: no stack traces, no hostnames, no query text.

An error code is never given a new meaning. A behaviour needing a different meaning gets a new code, and the old one keeps returning what it always did until it is removed with notice.