---
cuppy_hub: azure
cuppy_path: reference/storage/tables.md
cuppy_section: docs
cuppy_doc: 4GW8-NNPE
cuppy_doc_uuid: 9d904212-2c62-4635-98e6-b584c9550088
cuppy_permalink: https://cuppy.io/azure/d/4GW8-NNPE
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/azure/docs/reference/storage/tables
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: d0e2302672c493353b7a2cc017610fbfa4b52fed6cd7b3c2a4998b9b26642f15
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# 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)

| Item | Limit |
|---|---|
| Properties per entity | 255, including `PartitionKey`, `RowKey` and `Timestamp`: 252 of your own |
| Entity size | 1 MiB for all properties combined |
| `PartitionKey`, `RowKey` | Strings up to 1,024 characters. **Not allowed:** `/`, `\`, `#`, `?`, and control characters U+0000–U+001F and U+007F–U+009F |
| String and binary values | 64 KiB each (a string holds about 32K characters, UTF-16) |
| Property names | Case-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:

```ts
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, message | Cause → fix | Check |
|---|---|---|
| 409 `EntityAlreadyExists` `The specified entity already exists.` | `createEntity` on an existing key. Use `upsertEntity`, or treat as success when idempotent | Get the entity |
| 404 `EntityNotFound` `The specified entity does not exist.` | Wrong key (keys are case-sensitive), or deleted. `getEntity` throws it; check before updating | `listEntities` with the `PartitionKey` filter |
| 404 `TableNotFound` `The table specified does not exist.` | Create the table first (`createTable` is idempotent) or fix the name | `az 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 name | Retry 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 purpose | Compare `etag` |
| 400 `EntityTooLarge` / `PropertyValueTooLarge` / `TooManyProperties` | Over 1 MiB, a value over 64 KiB, or over 252 custom properties. Move large values to a blob and store its name | Serialized 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 partition | Action 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 too | Key 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 above | Name regex above |
| 400 `PropertyNameInvalid` / `PropertyNameTooLong` / `DuplicatePropertiesSpecified` / `PropertiesNeedValue` / `InvalidValueType` | Fix the entity's property names, duplicates, missing values or types | — |
| 403 `AuthorizationPermissionMismatch` and other 401/403 | Missing Table data role, network rules, or shared key disabled | `reference/storage/access-errors.md` |


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 107 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/azure/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
