AWS DynamoDB · 2026-09-10 · DynamoDB arc

AWS DynamoDB for MCP Servers: Session Store, TTL, Streams, Transactions, and DAX — Five Production Patterns

DynamoDB is a natural fit for MCP session state — each session maps to a single partition key, reads and writes are fast single-item operations, and TTL handles cleanup automatically. But MCP servers are concurrent by design: parallel tool calls, agent retries, and multi-tab users all write to the same session item simultaneously. Without the right patterns, concurrent writes silently overwrite each other, expired sessions are returned as valid, and Lambda retries create duplicate tool call records. Five patterns address the production failure modes that emerge when you put a real MCP workload on DynamoDB.

TL;DR

Use a version attribute + ConditionExpression: "#v = :expected" on every session metadata update to prevent concurrent corruption. Store TTL as epoch seconds, not milliseconds, and filter expired items in application code. Enable StreamViewType: NEW_AND_OLD_IMAGES and set bisectBatchOnError: true on the Lambda event source mapping. Use ClientRequestToken: toolCallId on TransactWriteItems for 10-minute idempotency. Use DAX for read-heavy sessions; never route TransactWriteItems through DAX (it's not supported).

Pattern 1: Session store with optimistic locking

A DynamoDB session table for MCP servers works best with a single-table design that separates mutable session metadata from immutable tool call records using sort key prefixes:

The critical design decision: every write to the metadata item must use optimistic locking. MCP servers receive parallel tool calls — an agent fires three tools concurrently in the same session. Without locking, all three read version=4, all three write with SET version=5, and two writes silently overwrite the first. The result: lost context messages, dropped tool results, corrupted session state.

Optimistic locking with a version attribute:

For context accumulation (appending new messages), avoid the read-modify-write pattern entirely. Use DynamoDB's list_append function: SET context = list_append(if_not_exists(context, :empty), :newMessages). This is atomic — two concurrent list_append updates to the same list both succeed, each appending its messages independently. Combine with the version attribute for updates that need to also mutate other fields.

Tool call records are written with ConditionExpression: "attribute_not_exists(pk)" to make them idempotent. A Lambda retry writes the same sk=tool#<ts>#<callId> — the condition check catches the duplicate and returns a ConditionalCheckFailedException that you catch and swallow. Use the tool call ID in the sort key, not a random UUID, so retries produce the same key.

Full guide: MCP Server DynamoDB Session Store — versioning, optimistic locking, conditional writes

Pattern 2: TTL for session lifecycle management

DynamoDB TTL deletes items automatically, but the deletion is eventual — expired items can linger for up to 48 hours after their TTL timestamp. For MCP session management, this creates a gap: a user whose session expired 20 minutes ago may still have their session item in the table and get a valid response from a GetItem call.

The TTL attribute must be a Unix epoch timestamp in seconds. This is the most common setup mistake: Date.now() returns milliseconds; DynamoDB interprets a millisecond timestamp as a date in the year 2258 and the item never expires. The correct value: Math.floor(Date.now() / 1000) + sessionDurationSeconds.

Application-level expiry filtering is mandatory. On every GetItem that returns a session:

const nowSec = Math.floor(Date.now() / 1000);
if (item.ttl && item.ttl <= nowSec) {
  return null; // expired — treat as not found
}

This filtering is belt-and-suspenders: DynamoDB will eventually delete the item, but your application must not serve expired sessions in the meantime.

Sliding session windows: call UpdateItem to extend the TTL on every tool call. Use a ConditionExpression that rejects extends on already-expired sessions: ConditionExpression: "attribute_exists(pk) AND #ttl > :now". Without this guard, a race condition between a session expiry event and a tool call extension could briefly resurrect a session.

GSI on the TTL attribute: add a GSI with status as partition key and ttl as sort key. This enables Query operations that find sessions expiring in the next 15 minutes — useful for pre-expiry warnings, warm-up, or graceful drain before your MCP server restarts.

DynamoDB Streams integration: TTL-triggered deletions appear as REMOVE events in the stream with record.userIdentity.principalId === "dynamodb.amazonaws.com". Wire a Lambda stream processor to detect TTL expiry and trigger cleanup side effects (revoking auth tokens, notifying downstream systems, logging session summaries).

Full guide: MCP Server DynamoDB TTL — session cleanup, expiry lifecycle, GSI on TTL attribute

Pattern 3: DynamoDB Streams for session audit logs

Every write to the session table — session creation, tool call completion, status transition, TTL expiry — becomes a stream record. Wire a Lambda to the stream to build a complete audit trail without coupling the write path to the audit path.

StreamViewType must be NEW_AND_OLD_IMAGES for audit purposes. NEW_IMAGE alone cannot reconstruct what changed — you need the before and after to detect field-level diffs. Setting this cannot be changed on an existing stream without disabling and re-enabling streams (which resets the 24-hour retention window). Set it at table creation.

The most common production incident with DynamoDB Streams: a poison-pill record blocks the entire shard. If your Lambda handler throws an unhandled exception, DynamoDB retries the batch. Without BisectBatchOnFunctionError: true on the event source mapping, DynamoDB retries the same failing batch for up to 24 hours — blocking all newer records on the same shard for the duration. Enable bisection: DynamoDB halves the batch, isolates the bad record, and sends it to the dead-letter queue while processing the rest normally.

Critical settings for the Lambda event source mapping:

The Lambda handler must be idempotent. Lambda delivers stream records at-least-once — the same record may arrive twice on retry. Use the stream record's SequenceNumber as an idempotency key: write the audit record with ConditionExpression: "attribute_not_exists(sequenceNumber)" and catch the resulting ConditionalCheckFailedException as a silent success (not an error).

Ordering: records within a shard are in strict write order per partition key. A session's events arrive in the order they were written. Records across shards have no ordering guarantee — if your audit log needs global ordering, use a DynamoDB Streams → Kinesis Data Streams bridge (which supports enhanced fan-out and per-shard ordering).

Full guide: MCP Server DynamoDB Streams — audit trails, session event sourcing, Lambda triggers

Pattern 4: Transactions for idempotent multi-step tool calls

TransactWriteItems lets an MCP tool call atomically write to multiple items — the session metadata, a tool call record, and an audit entry — in a single all-or-nothing operation. If the Lambda times out after the transaction commits but before returning a response, a retry with the same idempotency token gets the original result without re-executing.

The ClientRequestToken parameter is the key to idempotency. Pass the MCP tool call ID: ClientRequestToken: toolCallId. DynamoDB remembers this token for 10 minutes — retries within that window return the original result without re-running the transaction. After 10 minutes, the token expires and a new submission with the same token would execute a new transaction (guard this with a ConditionExpression on the primary item as belt-and-suspenders).

Include a ConditionCheck item in the transaction to assert preconditions:

Error handling: only TransactionConflictException should be retried (two transactions modified the same items concurrently — retry with 50ms exponential backoff up to 3 times). TransactionCanceledException with a ConditionalCheckFailed reason means a business logic precondition failed (session expired, resource already exists, version mismatch) — do not retry, surface the specific failure to the MCP caller by inspecting err.CancellationReasons[i].Code.

Limits: 25 items maximum per transaction across all tables. 2× write capacity units per item compared to individual writes. DAX does not support TransactWriteItems — transactional writes must go directly to DynamoDB, not through the DAX client. Plan your DynamoDB client architecture so the MCP server can use DAX for reads and non-transactional writes, but falls back to the raw DynamoDB client for transactions.

Full guide: MCP Server DynamoDB Transactions — idempotent multi-step tool calls, TransactWriteItems

Pattern 5: DAX caching for read-heavy MCP sessions

DAX adds an in-memory cache in front of DynamoDB. Item reads that hit the cache return in microseconds instead of single-digit milliseconds. For MCP servers where the same session is read on every tool call in a long conversation, DAX can dramatically reduce latency and DynamoDB read costs.

DAX maintains two independent caches: the item cache (5-minute TTL by default) serves individual GetItem results keyed by primary key; the query cache (1-minute TTL) serves Query result sets keyed by the full query parameters. For MCP session management, the item cache is the valuable one — most reads are GetItem pk=session#abc, sk=metadata, and a hot session gets the same key repeatedly.

DAX is write-through: writes via the DAX client update the item cache atomically. The cache stays consistent as long as all writes go through the DAX client. Writes that bypass DAX — the AWS Console, the CLI, migration scripts, the raw DynamoDB client used for transactions — leave stale items in the cache for up to the item cache TTL.

The consistency trap: ConsistentRead: true on a GetItem through the DAX client bypasses the cache and reads directly from DynamoDB. If your MCP server needs post-write read-after-write consistency (common in interactive tool calls), it must either use ConsistentRead: true (losing the DAX benefit) or accept the small window of cache staleness after a write-through update.

DAX is VPC-only — the cluster has no public internet endpoint. Your MCP server Lambda or ECS task must be in the same VPC with outbound TCP 9111 to the DAX cluster security group. The DAX client (npm: amazon-dax-client) is API-compatible with the DynamoDB Document client — swap the constructor, keep all command objects unchanged.

When DAX is not the right choice: write-heavy sessions (every tool call writes a new record — the item cache is continuously invalidated); low traffic volumes (the cluster minimum is ~$0.27/hr for dax.t2.small); workloads dominated by TransactWriteItems (not supported); sessions updated by multiple services outside the DAX client (cache coherency across codebases is operationally complex).

Full guide: MCP Server DynamoDB DAX — in-memory caching, item cache vs query cache, consistency

Combined failure mode reference

PatternSymptomCauseFix
Session storeContext messages disappear under concurrent tool callsRead-modify-write on context list; last write wins overwrites earlier concurrent writesUse list_append in UpdateExpression; combine with version attribute for writes that also mutate other fields
Session storeVersion counter never matches; all updates fail with ConditionalCheckFailedNot re-reading the session before each retry; retrying with stale expected versionAlways re-read with ConsistentRead: true before each retry attempt; use the freshly read version as :expectedVersion
TTLSessions never expire; items accumulate indefinitelyTTL stored in milliseconds (Date.now()) instead of secondsAlways use Math.floor(Date.now() / 1000) + durationSec
TTLExpired sessions returned as validDynamoDB TTL deletion is eventual (up to 48h after expiry)Filter in application: check item.ttl <= Math.floor(Date.now()/1000) on every read
StreamsAudit stream stops processing for hoursPoison-pill record; no bisectBatchOnError; entire shard blockedSet BisectBatchOnFunctionError: true; add DLQ; monitor IteratorAge CloudWatch metric
StreamsAudit records are duplicatedLambda retry delivers the same stream record twiceUse SequenceNumber as idempotency key; condition write on attribute_not_exists(sequenceNumber)
TransactionsTool call creates duplicate resources on Lambda retryNo ClientRequestToken; each retry executes a new transactionPass ClientRequestToken: toolCallId; add ConditionExpression: "attribute_not_exists(pk)" as belt-and-suspenders
TransactionsTransactionCanceledException retried indefinitelyBusiness logic failure (ConditionalCheckFailed) mistakenly treated as retriableOnly retry TransactionConflictException; parse CancellationReasons to surface the correct error to the caller
TransactionsUnsupported operation error from DAX clientTransactWriteItems called through DAX client (not supported)Keep a separate raw DynamoDBClient for transactional writes; use DAX client only for non-transactional reads and writes
DAXStale session data after write from Console or CLIWrite bypassed DAX write-through; cache has old value for up to item cache TTL (5 min)Route all writes through DAX client; for operations that must be immediately consistent post-write, use ConsistentRead: true on the subsequent read
DAXConnection timeout from Lambda to DAXLambda not in same VPC; or SG missing TCP 9111 outbound ruleAdd Lambda VPC config; add outbound TCP 9111 from Lambda SG to DAX SG

Architecture decision checklist

Use this checklist when choosing which patterns to apply for a DynamoDB-backed MCP server: