DynamoDB BatchGetItem in Node.js (AWS SDK v3)

BatchGetItem fetches up to 100 items by primary key in one request. In AWS SDK v3 the call has to be a loop, because UnprocessedKeys arrives on a successful response rather than an error. What fills it, and why 16 MB and 1 MB-per-partition are the numbers that matter, is covered in batch operations in DynamoDB. This page is about the v3 call and what it hands back.

Code

import {BatchGetItemCommand, DynamoDBClient} from '@aws-sdk/client-dynamodb';

const client = new DynamoDBClient({region: 'us-east-1'});

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

let requestItems = {
  Music: {
    Keys: [
      {Artist: {S: 'Arturo Sandoval'}, SongTitle: {S: 'Cubano Chant'}},
      {Artist: {S: 'Arturo Sandoval'}, SongTitle: {S: 'A Mis Abuelos'}},
      {Artist: {S: 'Ella Fitzgerald'}, SongTitle: {S: 'Misty'}}
    ]
  }
};

const items = [];
let attempt = 0;

do {
  const response = await client.send(new BatchGetItemCommand({RequestItems: requestItems}));
  items.push(...(response.Responses?.Music ?? []));

  // A partial result is NOT an error: throttling, a >16 MB response, or an
  // internal failure returns the leftovers in UnprocessedKeys. Retry them
  // with exponential backoff.
  requestItems = response.UnprocessedKeys;
  if (requestItems && Object.keys(requestItems).length > 0) {
    attempt += 1;
    await sleep(Math.min(100 * 2 ** attempt, 5000));
  }
} while (requestItems && Object.keys(requestItems).length > 0);

console.log(`Fetched ${items.length} items`);

Explanation

  • The optional chaining is not defensive noise. Responses and UnprocessedKeys are both optional in the v3 types, so response.Responses?.Music ?? [] and the Object.keys() guard are what the compiler asks for. In plain JavaScript they are what stops the first empty response from throwing.
  • Branch on err.name. v3 puts the service error code there, and the two failures this command actually raises are not retryable, so they must never fall into the backoff loop. Over 100 keys gives ValidationException / Too many items requested for the BatchGetItem call; the same key twice gives Provided list of item keys contains duplicates. Both are reproduced verbatim on the Python page.
  • UnprocessedKeys arrives already shaped as RequestItems, which is the only reason the loop can assign it straight back. It is not a pagination cursor and it does not mean the call failed.
  • The backoff is AWS's instruction, not a nicety. The API reference tells you to use "an exponential backoff algorithm" because an immediate retry lands on the same throttled partition.
  • ConsistentRead and ProjectionExpression are per table, set inside each RequestItems entry rather than at the top level. That is easy to miss when the map has one key and looks like a flat request.

What the response actually comes back as

Run the fence above against DynamoDB Local 3.3.0 with all three songs present, add ReturnConsumedCapacity: 'TOTAL', and log the song titles instead of the count:

order:            [ 'A Mis Abuelos', 'Misty', 'Cubano Chant' ]
UnprocessedKeys:  {}
ConsumedCapacity: [ { TableName: 'Music', CapacityUnits: 1.5 } ]

The request listed Cubano Chant, A Mis Abuelos, Misty. The response is in none of those positions, which is why the loop pushes into a flat array instead of indexing by offset. Match items back to requests on their key attributes, and include those keys in any ProjectionExpression so you still have something to match on.

Set ConsistentRead: true on that same Music entry and the same three keys cost 3 units instead of 1.5. Three items under 4 KB each are billed as three separate GetItem reads, at half a unit eventually consistent and a full unit strongly consistent. The pricing calculator converts that per-item arithmetic into a monthly number before you commit to a read pattern.

Now delete Misty and re-run: two items, an empty UnprocessedKeys, and 1.0 units. The missing key was billed nothing. That is a DynamoDB Local artifact and not the contract; the BatchGetItem reference (fetched 2026-07-28) says requests for nonexistent items consume the minimum read capacity for the read type. Do not size a batch of cache misses from a local run.

To pull a set of keys back and look at what actually returned, without writing the loop first, download DynoTable.

References

Last verified 2026-07-28 against the official AWS documentation linked above.

Work with DynamoDB without the Console

A fast DynamoDB desktop client that runs the real SQL DynamoDB can’t — JOINs, GROUP BY, aggregates — with visual editing and an AI agent on your own Bedrock keys.

Free 30-day trial, no credit card — then the Free plan with no time limit.