Table storage — data model limits, Entra ID access from TypeScript, entity group transactions and Table error codes

Working with Azure Table storage from Node.js — the entity and key limits (255 properties, 1 MiB entity, 1,024-character keys, forbidden key characters, table name rules), access with Storage Table Data roles and @azure/data-tables 13.3 with DefaultAzureCredential, safe filters with odata``, entity group transactions (one partition, 100 actions, 4 MiB, failed-index message), where the SDK puts the error code, and the Table service error codes with fixes.

markdownreferencev14GW8-NNPEstage: buildtrust: officialservice: storageinterface: sdkAzure Table storage@azure/data-tablesTableClientDefaultAzureCredential Table storageStorage Table Data ContributorStorage Table Data Readerodata filtersubmitTransactionTableTransactionentity group transaction100 operations change set4 MiB batchEntityAlreadyExistsThe specified entity already exists.EntityNotFoundThe specified entity does not exist.TableNotFoundThe table specified does not exist.TableAlreadyExistsTableBeingDeletedThe specified table is being deleted.UpdateConditionNotSatisfiedThe update condition specified in the request was not satisfied.EntityTooLargeThe entity is larger than the maximum size permitted.PropertyValueTooLargeTooManyPropertiesInvalidDuplicateRowInvalidInputOutOfRangeInputPropertyNameInvalidPropertiesNeedValueInvalidValueTypePartitionKey RowKey forbidden characterstable name rules1 MiB entity252 custom properties

Created 21 Oct 2026 · Modified 23 Oct 2026 · 1 version

Table storage

Azure Table storage is the key-value table service inside a storage account (https://<acct>.table.core.windows.net). Cosmos DB for Table speaks the same API with other limits; this page covers Table storage.

Limits of the data model (fixed, documented)

ItemLimit
Properties per entity255, including PartitionKey, RowKey and Timestamp: 252 of your own
Entity size1 MiB for all properties combined
PartitionKey, RowKeyStrings up to 1,024 characters. Not allowed: /, \, #, ?, and control characters U+0000–U+001F and U+007F–U+009F
String and binary values64 KiB each (a string holds about 32K characters, UTF-16)
Property namesCase-sensitive, up to 255 characters, C# identifier rules, no -
Table names^[A-Za-z][A-Za-z0-9]{2,62}$, case-insensitive, unique per account; some names are reserved (tables)

Timestamp is set by the server and ignored on writes. Queries are fast only on PartitionKey (and RowKey within it); any other filter scans.

Access from TypeScript with Entra ID

Grant Storage Table Data Contributor (or Reader) on the account or the table; Owner and Contributor don't read data (reference/storage/access-models.md). Then:

import { DefaultAzureCredential } from "@azure/identity";
import { TableClient, odata, type TransactionAction } from "@azure/data-tables";
import { RestError } from "@azure/core-rest-pipeline";

// Needs Storage Table Data Contributor on the account or table.
const client = new TableClient(
  `https://${process.env.STORAGE_ACCOUNT}.table.core.windows.net`,
  "orders",
  new DefaultAzureCredential(),
);

// The service code is in details.errorCode (single operations) or e.code (transactions).
export function tableErrorCode(e: unknown): string | undefined {
  if (!(e instanceof RestError)) return undefined;
  const d = e.details as { errorCode?: string } | undefined;
  return d?.errorCode ?? e.code;
}

export async function main(): Promise<void> {
  await client.createTable(); // no error if it already exists

  await client.upsertEntity({ partitionKey: "cust-42", rowKey: "order-1001", total: 12.5 }, "Merge");

  // Build filters with odata`` so values are quoted and escaped.
  const customer = "cust-42";
  for await (const e of client.listEntities<{ total: number }>({
    queryOptions: { filter: odata`PartitionKey eq ${customer} and total gt ${10}` },
  })) {
    console.log(e.rowKey, e.total);
  }

  // A transaction: one partition, at most 100 actions, at most 4 MiB, each entity once.
  const actions: TransactionAction[] = [];
  for (let i = 0; i < 250; i++) {
    actions.push(["upsert", { partitionKey: customer, rowKey: `line-${i}`, qty: i }]);
  }
  for (let i = 0; i < actions.length; i += 100) {
    try {
      await client.submitTransaction(actions.slice(i, i + 100));
    } catch (e) {
      // The message starts with the zero-based index of the failed action, e.g. "3:…"; the whole batch rolled back.
      console.error(tableErrorCode(e), (e as Error).message);
      throw e;
    }
  }

  try {
    await client.createEntity({ partitionKey: customer, rowKey: "order-1001" });
  } catch (e) {
    if (tableErrorCode(e) !== "EntityAlreadyExists") throw e;
  }
}

Where the code is: for a single operation, @azure/data-tables 13.3.2 leaves RestError.code undefined and puts the service code in e.details.errorCode (and e.details.odataError.code); for a failed transaction it sets e.code. Match on both, as above.

Transactions: all actions share one PartitionKey; at most 100 actions and 4 MiB; an entity appears once; actions are create, update, upsert or delete, not queries. They're atomic: one failure rolls back the rest, and its message starts with the failed action's zero-based index.

Error codes

Messages are the REST table's wording; the service appends RequestId: and Time: lines.

Status, code, messageCause → fixCheck
409 EntityAlreadyExists The specified entity already exists.createEntity on an existing key. Use upsertEntity, or treat as success when idempotentGet the entity
404 EntityNotFound The specified entity does not exist.Wrong key (keys are case-sensitive), or deleted. getEntity throws it; check before updatinglistEntities with the PartitionKey filter
404 TableNotFound The table specified does not exist.Create the table first (createTable is idempotent) or fix the nameaz storage table exists -n <t> --account-name <a> --auth-mode login
409 TableBeingDeleted The specified table is being deleted.Recreating a just-deleted name: wait and retry, or use another nameRetry with backoff
412 UpdateConditionNotSatisfied The update condition specified in the request was not satisfied.The entity's ETag changed since you read it (optimistic concurrency). Re-read and retry, or update with etag: "*" to overwrite on purposeCompare etag
400 EntityTooLarge / PropertyValueTooLarge / TooManyPropertiesOver 1 MiB, a value over 64 KiB, or over 252 custom properties. Move large values to a blob and store its nameSerialized size
400 InvalidDuplicateRow A command with RowKey '{0}' is already present in the batch. An entity can appear only once in a batch.De-duplicate actions before submitTransaction—
400 InvalidInput The batch request operation exceeds the maximum 100 changes per change set.Split into chunks of 100, per partitionAction count
400 InvalidInput (other messages)Conflicting If-Match / If-None-Match headers, or an invalid ETag (the documented messages); the hub's reading is that a forbidden key character can surface here tooKey values against the forbidden list
400 OutOfRangeInput The '{0}' parameter of value '{1}' is out of range.The hub's reading: often a table name or key that breaks the rules aboveName regex above
400 PropertyNameInvalid / PropertyNameTooLong / DuplicatePropertiesSpecified / PropertiesNeedValue / InvalidValueTypeFix the entity's property names, duplicates, missing values or types—
403 AuthorizationPermissionMismatch and other 401/403Missing Table data role, network rules, or shared key disabledreference/storage/access-errors.md

Read this document as markdown — the same bytes an agent receives.