Errors and retries
The client retries what can pass on a second try and throws the rest. Every exception is in the namespace DynaSharp.
try
{
await orders.Put(order).IfNotExists().SendAsync(ct);
}
catch (ConditionalCheckFailedException)
{
Console.WriteLine("The order exists already.");
}
catch (DynamoDbException exception) when (exception.IsTransient)
{
Console.WriteLine("DynamoDB is busy. Try again later.");
}
A timeout and a network failure are a TimeoutException and an HttpRequestException, not a DynamoDbException.
What throws
Every error DynamoDB returns is a DynamoDbException with ErrorCode (without its namespace), StatusCode, RequestId, OperationName and TableName (as the request named it; null for a batch or transaction). A known code has its own class; any other is a plain DynamoDbException.
| Exception | When | Retried | Carries |
|---|---|---|---|
ConditionalCheckFailedException |
A write's condition did not hold; nothing was written. TrySendAsync returns it as ConditionMet false instead, see Conditions |
No | Item, what the condition saw, with ReturnOldOnFailure(). Key, as stored. Message names the condition sent |
TransactionCanceledException |
A transaction was cancelled; none of it took effect | No | ReasonFor(marker), ItemFor(marker), Failures, CancellationReasons. Message names each failed action by position, verb and table |
TransactionConflictException |
A single-item call met an item a transaction holds | No | |
ValidationException |
DynamoDB rejected the request: an expression that does not parse, a key of the wrong type, a range bound above the other | No | For a command, Expressions, ExpressionNames and ExpressionValueTypes say what #n0 and :v0 stand for, without values |
ResourceNotFoundException |
The table or index does not exist or is not active yet | No | |
ResourceInUseException |
The table is being created, updated or deleted, or already exists | No | |
ProvisionedThroughputExceededException, ThrottlingException, RequestLimitExceededException, LimitExceededException |
A table, the account or the control plane is throttled | Yes, with the throttling backoff | |
TransactionInProgressException |
A transaction with the same token is still running | Yes, as throttling, with the same token | |
ReplicatedWriteConflictException |
A write met a write from another region of a global table | Yes | |
InternalServerErrorException, any HTTP 500, 502, 503 or 504 |
DynamoDB or a proxy in front of it failed. A body that is no DynamoDB response has the code UnrecognizedResponse |
Yes | |
AccessDeniedException |
The credentials lack a permission, see What a call needs | No | |
UnrecognizedClientException |
The access key or session token is not valid | No | |
DynamoDbException with ErrorCode InvalidSignatureException |
The signature was rejected: a secret key not of the access key, a request changed in between, or a local clock off. Message is DynamoDB's |
No, but a clock more than 4 minutes off is corrected and the request resent, see Retries | |
DynamoDbException with ErrorCode MissingAuthenticationTokenException |
The request arrived unsigned: something in between removed the Authorization header. Message is DynamoDB's |
No | |
DynamoDbException with ErrorCode ExpiredTokenException |
The credentials expired | Once, at once, if the provider can fetch new ones: the default chain, the container, instance metadata, web identity, assume role and SSO providers, a profile using one of them or credential_process, and the environment with AWS_CREDENTIAL_EXPIRATION. Otherwise no |
|
IdempotentParameterMismatchException |
A transaction token came back within 10 minutes with other actions | No | |
ItemCollectionSizeLimitExceededException |
A partition of a table with a local index would grow past 10 GB | No | |
ExpiredIteratorException, TrimmedDataAccessException |
A DynamoDB Streams shard iterator is older than 15 minutes, or its records are gone | No |
IsTransient on every DynamoDbException says whether the same call can succeed later unchanged, like DbException.IsTransient. True means the client's attempts ran out: try later, not at once in a loop.
- True: every row retried above,
TransactionConflictException, and aTransactionCanceledExceptionwhose failed actions are allTransactionConflictorThrottled.BatchIncompleteExceptionandBatchGetIncompleteException<T>are true when they gave up on their rounds or on a transient failure. - False: everything else: a failed condition (it cannot pass until the item changes), a rejected request, a missing table, access and token errors, and an unknown code.
The client throws these itself:
| Exception | When | Carries |
|---|---|---|
TimeoutException |
An attempt ran past AttemptTimeout, or a timeout of the HTTP handler cancelled it, such as SocketsHttpHandler.ConnectTimeout. Retried; surfaces after the last attempt. Also a CreateTable() that ran past its Timeout(...), see A table that exists, which is not retried |
The message of the CreateTable() names the table and the step |
HttpRequestException |
The network failed. Retried, except a response above 64 MB, whose HttpRequestError is ConfigurationLimitExceeded |
|
OperationCanceledException |
The caller's CancellationToken was cancelled. Never retried |
|
CredentialsUnavailableException |
No credentials source applies, or one that applies is broken. Never retried. See When credentials fail | |
BatchIncompleteException |
A batch write gave up after its own resends, see Batches | UnprocessedItems, Written, the failure as InnerException. ErrorCode is BatchIncomplete |
BatchGetIncompleteException<T> |
A batch get gave up | UnprocessedKeys and the Items read so far |
TableNotActiveException |
A wait for a table ran out, or CreateTable() found a table missing a declared index. A TimeoutException |
TableName, LastStatus and PendingIndex |
DynamoDbMappingException |
An item does not fit its type: a required attribute is missing, a value cannot be read, or the item is of another type. Never retried; an update that throws it has been applied. A query or scan can skip such items, see Queries |
EntityType, AttributeName and Key |
KeyFormatException |
A value cannot be part of a key, see Generated code. An ArgumentException |
ParamName, the placeholder |
ArgumentException |
A key of Table<Item> is empty, an index is not declared by the entity, a fan-out repeats a partition key |
|
InvalidOperationException |
The chain is wrong: a command used twice, a [Version] rule, Value of a SelectResult without an item. Or CreateTable().IfNotExists() found a table whose key differs from the entities', see A table that exists |
|
FormatException |
ContinuationToken.Parse got text that is no token of this version, or PageAsync got a token that does not fit the query or TokenSigner, see Queries. The message quotes nothing of the token |
No message names a key or a value. Key, Item, UnprocessedItems, UnprocessedKeys and Items hold your data; the client logs none of it.
Retries
One retry loop serves every level: Table<T>, Table<Item>, the request API and SendRawAsync. Each page of a query is retried on its own.
- Attempts.
MaxAttempts, 4 by default, counts the first attempt..MaxAttempts(n)on a command sets it per call. - Waits. A random time between zero and
min(20 s, base * 2^(attempt - 1)), base 50 ms for throttling and 100 ms otherwise: by default at most 50, 100, 200 ms and 100, 200, 400 ms, so a dependency still starting gets at most 700 ms. Cancelling the call ends a wait at once. - Quota. A client has 500 tokens for all its requests. A retry takes 5, or 10 after a timeout; a success returns 1, or what its last retry took. With the quota empty, errors surface at once until successes refill it, and the client logs a warning, event 5 under Observability.
- Resends. If DynamoDB rejects a local clock more than 4 minutes off, the client learns the offset and resends at once. If it rejects expired credentials of a provider that can replace them, the provider drops them and the client resends at once, once per call. Both count as an attempt but take no wait and no quota.
- The end. The last attempt's error surfaces unchanged, and the client logs a warning, event 4.
The loop never retries a failed condition. RetryOnConflictAsync reruns your read-modify-write instead, see Retry on a conflict.
Batches
A batch resends what DynamoDB left unprocessed, with the backoff above; each of its requests has the loop's retries. MaxStalledRounds(n), 8 by default, bounds consecutive requests that get nothing done. Then, on a throttle, a timeout, a lost connection or a server error on the last attempt of a request, or on any other failure after a request went through, it throws BatchIncompleteException or BatchGetIncompleteException<T>. Their UnprocessedItems or UnprocessedKeys go straight into client.BatchWriteItemAsync or client.BatchGetItemAsync. DynamoDB may have applied part of a request that got no answer, so its writes count as unprocessed, and sending them again is safe: a put or a delete ends where it would have. A failure of the first request that repeating cannot fix, such as a ValidationException, applied nothing and surfaces as it is.
Where a retry applies a write twice
When a response is lost (a timeout, a dropped connection), the write was applied and the retry sends it again.
- Safe to repeat. A
Putwithout a condition, aDelete, aSetof a value,SetIfNotExists,Remove, a setAddorDelete, and the puts and deletes of a batch. - Applied twice.
Addon a number,list_appendand arithmetic inSET, such as.Set($"{Attr("n")} = {Attr("n")} + {1}"). A counter counts one too many. - Fails against itself. A conditional write (
IfNotExists(),IfExists(),IfVersion(n), aPutof a[Version]entity, your ownIf(...)) meets its own first attempt and throwsConditionalCheckFailedExceptionfor a write that was applied. Read the item to tell, or letReturnOldOnFailure()hand it over and compare it with what the write sent. A second delivery of the same change fails the same way, and nothing on the wire tells the two apart;TrySendAsyncdoes not either, it only returns the answer without an exception, see Events that arrive out of order. - Applied once. A transaction: every attempt of one send carries the same
ClientRequestToken, and DynamoDB answers a repeat within 10 minutes with the first outcome..Token(id)makes your own retry safe too, across a restart. - Applied again by the helper. Inside
RetryOnConflictAsync, a conditional write that fails against itself counts as a conflict, so the delegate runs and writes again. A write sent withTrySendAsyncthrows no conflict, so the helper does not run it again for a failed condition.
.MaxAttempts(1) makes a write happen at most once, not exactly once: a lost response surfaces as a TimeoutException or an HttpRequestException after the write was applied. A condition makes it exact, since the second attempt cannot pass it:
await customers.Update(Customer.Keys.Of("alice"))
.Add(Customer.Attrs.OrderCount, 1)
.MaxAttempts(1)
.SendAsync(ct);
var seen = await customers.Get(Customer.Keys.Of("alice")).ItemAsync(ct);
await customers.Update(Customer.Keys.Of("alice"))
.Add(Customer.Attrs.OrderCount, 1)
.If(Customer.Attrs.OrderCount.Eq(seen!.OrderCount))
.SendAsync(ct);
.MaxAttempts(n) is on every command and transaction step, before Return(attr) and Select(handle), which end the chain. A parallel scan and CreateTable() have none. For the request API, use a second client: new DynamoDbClient(client.Options with { MaxAttempts = 1 }) shares the credentials and a handler that was given.
Timeouts and cancellation
AttemptTimeout, 30 seconds by default, bounds one attempt, from looking up the credentials to reading the whole response. It surfaces asTimeoutException, neverOperationCanceledException, and is retried with a fresh timeout per attempt, so a call can takeMaxAttempts * AttemptTimeoutplus the waits.Timeout.InfiniteTimeSpanturns it off.- A timeout of the HTTP handler, such as
ConnectTimeoutof aSocketsHttpHandlergiven asHttpHandler, surfaces asTimeoutExceptiontoo, with the handler'sTaskCanceledExceptioninside, and is retried likeAttemptTimeout. Every cancellation that the caller's token did not cause counts as such a timeout. - A
CancellationTokenbounds the whole call, retries and waits included, and surfaces asOperationCanceledException, never retried. To bound a call in time, cancel the token after that time.
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(ct);
deadline.CancelAfter(TimeSpan.FromSeconds(2));
try
{
var cart = await carts.Get(Cart.Keys.Of("bob")).ItemAsync(deadline.Token);
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
Console.WriteLine("No answer within 2 seconds.");
}
Neither tells whether a write was applied, see above. A wait for a table has its own timeout, see A table that exists.
What a call needs
A role needs the IAM action of each operation a call sends, on the ARN of each table it names (arn:aws:dynamodb:<region>:<account>:table/Shop) unless the row says otherwise.
| Call | Actions |
|---|---|
Get, a Get with Select |
dynamodb:GetItem |
Put |
dynamodb:PutItem |
Update |
dynamodb:UpdateItem |
Delete |
dynamodb:DeleteItem |
Query, its ItemAsync, CountAsync and pages, a fan-out, a mixed read |
dynamodb:Query, on the table, or table/Shop/index/ByStatus for an index |
Scan, Segment, Parallel |
dynamodb:Scan, on the table or the index |
BatchGet() |
dynamodb:BatchGetItem |
BatchWrite() |
dynamodb:BatchWriteItem |
TransactWrite() |
Each step's action: dynamodb:PutItem for Put and Unique, dynamodb:UpdateItem, dynamodb:DeleteItem, dynamodb:ConditionCheckItem for Check. None for the transaction itself |
TransactGet() |
dynamodb:GetItem |
CreateTable() |
dynamodb:CreateTable and dynamodb:DescribeTable. With IfNotExists(), an existing table takes dynamodb:DescribeTable alone |
CreateTable().TimeToLive(...), or CreateTable() of an entity with [TimeToLive] |
Also dynamodb:DescribeTimeToLive, and dynamodb:UpdateTimeToLive while time to live is not on yet |
WaitUntilActive(), WaitUntilActiveAsync, DescribeTableAsync, client.DescribeTableAsync |
dynamodb:DescribeTable |
client.UpdateTableAsync, for an index or a stream on an existing table |
dynamodb:UpdateTable |
client.DeleteTableAsync |
dynamodb:DeleteTable |
ToRequest() of CreateTable() with Tags, sent by client.CreateTableAsync |
Also dynamodb:TagResource |
ListStreamsAsync of DynamoDbStreamsClient |
dynamodb:ListStreams, on * |
DescribeStreamAsync, GetShardIteratorAsync, GetRecordsAsync, ReadShardAsync, ReadStreamAsync |
dynamodb:DescribeStream, dynamodb:GetShardIterator, dynamodb:GetRecords, on table/Shop/stream/* |
ReadStreamAsync and ReadChangesAsync of a table |
Also dynamodb:DescribeTable, on the table |
A LockTable of DynaSharp.Coordination |
dynamodb:GetItem and dynamodb:UpdateItem, and dynamodb:ConditionCheckItem for IsHeld in a transaction |
Any other request API method, SendRawAsync |
dynamodb: plus the operation name, on what it names |
An index has its own ARN, so a policy on the table alone lets no query read an index. A shop role that reads and writes items and indexes and creates the table at start:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem",
"dynamodb:ConditionCheckItem", "dynamodb:Query", "dynamodb:Scan",
"dynamodb:BatchGetItem", "dynamodb:BatchWriteItem",
"dynamodb:DescribeTable", "dynamodb:CreateTable",
"dynamodb:DescribeTimeToLive", "dynamodb:UpdateTimeToLive"
],
"Resource": [
"arn:aws:dynamodb:eu-central-1:123456789012:table/Shop",
"arn:aws:dynamodb:eu-central-1:123456789012:table/Shop/index/*"
]
}
]
}
Without CreateTable(), leave out dynamodb:CreateTable, dynamodb:DescribeTimeToLive and dynamodb:UpdateTimeToLive, and keep dynamodb:DescribeTable only for WaitUntilActiveAsync or DescribeTableAsync.
IRSA on EKS. The web identity provider exchanges the pod's token with AssumeRoleWithWebIdentity, an unsigned call, so no pod policy grants it. The role's trust policy does: it allows sts:AssumeRoleWithWebIdentity to the cluster's OIDC provider, with conditions on the service account in sub and on sts.amazonaws.com in aud. EKS Pod Identity uses the container endpoint instead; its role trusts pods.eks.amazonaws.com with sts:AssumeRole and sts:TagSession. Use IRSA on EKS says how the client finds the token.