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

  1. Wait/rely on the built-in exponential backoff retry; the operation usually succeeds within max attempts.
  2. Increase the table's write capacity (WCU or on-demand mode) or enable auto scaling.
  3. Reduce batch size so fewer items share a partition, and stagger bulk operations.
  4. 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

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


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)