apereo/cas · warning
DynamoDb batch write returned
Error message
DynamoDb batch write returned [{}] unprocessed item collection(s). Retrying attempt [{}] in [{}]ms What it means
DynamoDb's batchWriteItem API can return 'unprocessed items' when throughput is exceeded; this facilitator retries the unprocessed collections with exponential backoff up to BATCH_WRITE_MAX_ATTEMPTS. This warning is logged on each retry, reporting how many unprocessed item collections remain, the attempt number, and the computed backoff delay. It is not fatal until the max-attempts IllegalStateException is thrown.
Solutions
- Wait/rely on the built-in exponential backoff retry; the operation usually succeeds within max attempts.
- Increase the table's write capacity (WCU or on-demand mode) or enable auto scaling.
- Reduce batch size so fewer items share a partition, and stagger bulk operations.
- Check DynamoDB CloudWatch ThrottledRequests/WriteThrottleEvents metrics to confirm throttling as the root cause.
Example fix
// before: fixed low capacity, big burst batches registry.putAll(tickets); // hundreds at once // after: chunk the batch and let retries absorb throttling val chunkSize = 25; Collectors.partition(tickets, chunkSize).forEach(chunk -> registry.putAll(chunk)); // and increase table WCU / switch billing mode to PAY_PER_REQUEST
Defensive patterns
Strategy: retry
Validate before calling
// Check table capacity before large batch writes
describeTable(tableName).table().billingModeSummary().billingMode() == BillingMode.PAY_PER_REQUEST
|| tableDesc.provisionedThroughput().writeCapacityUnits() >= expectedWcus; Try / catch
try {
registry.putAll(tickets);
} catch (IllegalStateException e) {
if (e.getMessage().contains("batch write failed to process")) {
// re-queue tickets for later write after backoff
retryQueue.addAll(tickets);
} else {
throw e;
}
} Prevention
- Use PAY_PER_REQUEST (on-demand) billing for spiky ticket traffic.
- Keep DynamoDB batch sizes small and uniformly distributed across partition keys.
- Monitor WriteThrottleEvents in CloudWatch and alert before saturation.
- Schedule bulk maintenance writes during low-traffic windows.
When it happens
Trigger: Calling any bulk write path (e.g. putAll / batch persistence via DynamoDbTicketRegistryFacilitator) while DynamoDB responds with UnprocessedItems for part of the batch — triggered by exceeding table or account write capacity, hot partition keys, or DynamoDB throttling during heavy ticket traffic.
Common situations: Large SSO bursts with many service-ticket writes in one batch; DynamoDB table provisioned below required WCU; on-demand table experiencing adaptive capacity issues; batches spanning many partitions under load.
Related errors
- Validation attempt for principal is throttled
- Access Denied for user
- Expired [ ]: number of seconds since ticket usage time [ ]…
- e.getMessage() (ResourceNotFoundException logged during…
- No ticket definition could be found in the catalog to match
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/8da339df8fda1622.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-dynamodb-ticket-registry/src/main/java/org/apereo/cas/ticket/registry/DynamoDbTicketRegistryFacilitator.java:706
}
}
private void submitBatchWriteRequest(final Map<String, List<WriteRequest>> requestItems) {
var unprocessedItems = new HashMap<>(requestItems);
var attempt = 0;
while (!unprocessedItems.isEmpty()) {
val batchRequest = BatchWriteItemRequest.builder().requestItems(unprocessedItems).build();
val response = amazonDynamoDBClient.batchWriteItem(batchRequest);
unprocessedItems = new HashMap<>(response.unprocessedItems());
if (!unprocessedItems.isEmpty()) {
attempt++;
if (attempt >= BATCH_WRITE_MAX_ATTEMPTS) {
throw new IllegalStateException("DynamoDb batch write failed to process [%s] item collection(s) after [%s] attempts"
.formatted(unprocessedItems.size(), attempt));
}
val delay = Math.min(BATCH_WRITE_MAX_RETRY_DELAY_MILLIS,
BATCH_WRITE_RETRY_DELAY_MILLIS * (1L << Math.min(attempt, 8)));
LOGGER.warn("DynamoDb batch write returned [{}] unprocessed item collection(s). Retrying attempt [{}] in [{}]ms",
unprocessedItems.size(), attempt, delay);
FunctionUtils.doUnchecked(__ -> Thread.sleep(delay));
}
}
}
/**
* Column names for tables holding tickets.
*/
@Getter
@RequiredArgsConstructor
public enum ColumnNames {
/**
* id column.
*/
ID("id"),
/**View on GitHub (pinned to e7288fc434)