DynamoDB Item Size Has Exceeded the Maximum: The 400 KB Fix
If a DynamoDB write fails with a size error, the fix is almost never "raise the limit" — you can't. The 400 KB cap on a single item is fixed, and no Service Quotas request touches it. The honest fix is one of three patterns: compress the offending attribute, split the item into several smaller ones under the same partition key, or move the big value out to Amazon S3 and keep only a pointer in DynamoDB. Which one is right depends on whether you need to query the data you're removing.
Jake found this out on a Saturday afternoon, mid-repair-season, when the shop's own ticketing app stopped letting him open new repair tickets.
What actually counts toward the 400 KB
An "item" in DynamoDB is one row, identified by its partition key (plus its sort key, if the table has one). The 400 KB limit applies to the whole item, not to any single attribute inside it. That's the part people get wrong first: they check the size of the one field they think is big and it looks fine, not realizing the limit is a sum across every attribute on the item.
The size counts two things added together, and both are measured in raw UTF-8 bytes, not "characters":
- The attribute names — yes, the field names themselves count. An attribute called
customerFullNameAtTimeOfPurchasecosts more bytes on every single item than one calledname. - The attribute values — every string, number, binary blob, and every element nested inside a list or a map, all the way down.
DynamoDB itself gives a small example: an item with two attributes — one named "shirt-color" holding "R", one named "shirt-size" holding "M" — adds up to 23 bytes total, because the field names are part of that count, not just the values. Scale that idea up: a repair-ticket item with 40 short field names, each averaging 15 characters, is already carrying 600 bytes before a single value is written. It's rarely the field names alone that push you over 400 KB, but they are never free, and long, descriptive names on a table with millions of items also cost you in storage billing, item by item, forever.
🕐 What people assume vs. what's actually true
- Assumption: "my
notesfield is only 50 KB, so I'm fine." Reality: DynamoDB sums every attribute on the item, not just the one you're watching. - Assumption: lists and maps have their own separate size cap. Reality: there is no limit on how many values a list, map, or set can hold — the only ceiling is the 400 KB item total they live inside.
- What that means for you: you can't reason about one field in isolation. You have to reason about the whole item, including things like a growing "history" list that nobody is watching.
Why an item ends up over 400 KB in the first place
Almost nobody designs an item to be 400 KB on purpose. It happens one of three ways, and knowing which one you're in decides which fix actually helps.
1. A list or map that never stops growing
This is the most common one, and it's the pattern behind Jake's outage. A "repair ticket" item started with a small notes list — one entry per technician update. Fine for a two-day repair. But some tickets sit open for months (a part on backorder, a customer who never picks up), and every status change, every customer text, every photo reference kept appending to that same list, on that same item, forever. Nothing about the design said "stop at 50 entries." It just grew until one more append pushed a handful of tickets over the line.
2. A binary blob stored directly on the item
Photos, PDFs, exported reports, base64-encoded images — someone decided it was simpler to put the file straight into the item as a Binary attribute instead of standing up a separate storage step. It works right up until one photo, or three photos stacked in a list, crosses the line. Binary attribute length is constrained by the same 400 KB ceiling as everything else, no exception for "but it's a file, not text."
3. Denormalization that went too far
DynamoDB rewards you for embedding related data into one item to avoid a second read — that's a real, documented best practice, not an anti-pattern. But it has a ceiling. A "customer" item that embeds every order, every support ticket, and every device they've ever traded in is trying to be a whole relational schema flattened into one row. That's not denormalization anymore; that's one item doing the job of a table.
🙋♂️ Jake's Reality Check
"I didn't touch the app in weeks. Why did it break on the busiest Saturday of the month?"
Because nothing about the item's design changed — the data inside it did. A repair ticket that had been quietly open since spring, getting one more status update every couple of weeks, finally crossed 400 KB on the update that happened to land that afternoon. The app didn't break. The data outgrew the shape it was poured into months earlier.
The error you'll actually see
When a write pushes an item past the limit, DynamoDB rejects the whole write and returns a ValidationException. This is the message developers usually search for word-for-word: "Item size has exceeded the maximum allowed size." It's a client-side-visible, synchronous failure — the write simply doesn't happen. Nothing gets partially saved, and nothing silently truncates your data for you. That's the good news buried in a bad afternoon: you find out immediately, not three weeks later when a report comes back wrong.
A couple of nuances worth knowing before you go hunting for the bug:
- The 400 KB figure is the size once the item is stored. The JSON payload you send over the wire to call
PutItemorBatchWriteItemcan be somewhat larger than 400 KB in its wire representation before DynamoDB parses and stores it — so don't be surprised if the request body you're inspecting locally doesn't line up byte-for-byte with the stored size. - If you're inside a
TransactWriteItemscall, the same per-item 400 KB rule applies to every item in the transaction, and on top of that, the whole transaction's items together cannot exceed 4 MB. A transaction can fail on either limit, and the cancellation reason will tell you which. BatchWriteItemrejects the whole batch outright if any individual item inside it is over 400 KB, or if the total request size of the batch exceeds 16 MB — two separate ceilings, both checked before anything is written.
Before you touch code, it's worth being honest with yourself about which of the three growth patterns above you're actually dealing with, because the three real fixes below aren't interchangeable — picking the wrong one buys you a few more months before the exact same failure comes back.
Pattern 1: compress the large attribute
This is the pattern AWS's own documentation lists first, and it's the fastest to bolt on. Run the large text attribute through a compression algorithm — GZIP is the common choice, LZO is mentioned as an alternative — before you write it, and store the compressed output as a Binary attribute instead of a String. Decompress it on read. That's the whole pattern.
Where it earns its place: long strings of free text. AWS's own example is a forum-message table — long paragraphs of user-written text are exactly the kind of thing that compresses well and that nobody needs to run a contains() filter against. A ticket's internal "resolution notes" field, a customer complaint transcript, a scanned receipt's OCR text — these are good compression candidates.
⚠️ What this actually breaks
Once an attribute is compressed into binary, DynamoDB can no longer filter, query, or scan on its content. If any part of your app ever needs to search inside that field — "find every ticket where the notes mention 'screen replacement'" — compression takes that ability away permanently, because the data is no longer readable to DynamoDB itself, only to your application after it decompresses the value.
There's a second, quieter limit to what compression buys you: it shrinks the item, it doesn't cap its growth. If the reason your item is 500 KB is an unbounded list, GZIP might get you back under 400 KB today and past the limit again in six months, on the exact same field, because the underlying list is still growing without a stop. Compression is a size reducer, not a growth stopper — treat it as the right tool for "this one field is just naturally verbose," not for "this field never stops getting bigger."
Pattern 2: split the item — vertical partitioning
This is the fix for an unbounded list or map — and it's the one that actually addresses the "grows forever" cause, rather than just buying a few more months of headroom. Instead of one item holding every status update, you break the data into several smaller items that all share the same partition key, and let a sort key tell them apart. DynamoDB's own documentation calls this vertical partitioning: break a large item down into smaller chunks and associate them by partition key, using the sort key to identify each piece. Because they share a partition key, they form what DynamoDB calls an item collection — and you can fetch the whole collection with a single Query, in one round trip, instead of a separate read per related item.
How this looks for a repair ticket
Instead of one item — partition key TICKET#4471 — carrying a notes list that grows every time a technician touches the ticket, you keep one small "header" item for the ticket's core fields (customer, device, status, opened date), and a separate item per status update, all sharing that same partition key:
- Keep the header item small. Partition key
TICKET#4471, sort key#METADATA, holding only the fields that describe the ticket itself and rarely change. - Give every event its own item. Partition key
TICKET#4471, sort key something sortable likeUPDATE#2026-09-12T14:03:00, holding just that one update. - Query the whole collection when you need the full history. A single
Queryagainst partition keyTICKET#4471returns the header and every update, sorted, in one call. - Write new updates as new items, never as appends to an existing item. This is the part that actually caps the growth — no single item ever accumulates more than one update's worth of data, no matter how long the ticket stays open.
✅ Why this is the one to use for growing lists
It's the only one of the three patterns that fixes the actual cause instead of buying time against the symptom. Compression shrinks what's there today; S3 offload moves the problem sideways; vertical partitioning removes the ceiling entirely, because no individual item is ever asked to hold an unbounded collection again.
Pattern 3: offload the value to Amazon S3
For a binary blob — a photo, a scanned document, a generated PDF report — this is the documented pattern. DynamoDB's own guidance is explicit: store the item as an object in Amazon S3 and store the S3 object identifier in your DynamoDB item instead of the object itself. This is the pattern for Jake's cracked-screen photos: they were never meant to live inside the repair-ticket item at all.
- Upload the large value to an S3 bucket — the photo, the PDF, the export — and get back its object key.
- Store only the pointer in DynamoDB. A short string attribute holding the bucket and key (or a full identifier), not the bytes.
- Tag the S3 object's metadata with the DynamoDB primary key that owns it. AWS specifically recommends this — it makes it far easier to find and clean up orphaned S3 objects later, since there's no built-in link back from S3 to DynamoDB otherwise.
- Read the DynamoDB item, then fetch the S3 object separately whenever you actually need the full file — most reads of a repair ticket don't need the photo, so this also saves you from pulling a multi-megabyte blob on every list view.
⚠️ What this actually breaks
DynamoDB does not support transactions that span S3 and DynamoDB together. If your write to DynamoDB succeeds but the upload to S3 fails (or the reverse), your application has to handle that itself — including cleaning up an orphaned S3 object with no DynamoDB row pointing at it. This isn't a corner case you can design away; it's a permanent property of splitting data across two services, and the metadata-tagging step above exists specifically to make that cleanup possible.
Securing the S3 side once the photo has left DynamoDB
Jake's next question was the right one to ask: "If the customer's cracked-screen photo isn't in DynamoDB anymore, who can see it?" This is where offloading to S3 needs a couple of extra decisions, not just a bucket to dump files into.
Since January 2023, every S3 bucket has server-side encryption switched on by default. New objects are automatically encrypted with Amazon S3 managed keys (SSE-S3, using 256-bit AES) as the baseline, at no extra cost and with no performance impact — you can no longer disable encryption on new uploads, even if you tried. If a repair photo needs a stricter access story — say, only your own account's Lambda functions should ever be able to decrypt it — you can switch the bucket or the individual upload to SSE-KMS instead, which puts the decrypt permission behind a customer-managed AWS KMS key rather than S3's own managed key.
Encryption answers "can someone read the bytes." It doesn't answer "can someone list the bucket and find the file." That's an IAM question, not an encryption one: whatever role writes the photo to S3 (a Lambda function, an app server) should be scoped to PutObject and GetObject on that one bucket's prefix, not broad S3 access across the account. A phone-shop ticketing app has no reason for its upload role to be able to list every bucket in the account — the failure mode of over-broad access isn't hypothetical, it's just invisible until an audit or an incident forces the question.
The fourth option: admit the data model was wrong
None of the three patterns above are "workarounds" in the sense of hacks bolted onto a design that's otherwise sound. Every one of them is really a data-modeling correction wearing an implementation's clothes. An item hitting 400 KB is DynamoDB telling you, in the most direct language it has, that something was embedded that should have been its own item, or something was stored inline that should have been a reference.
That reframing matters because it changes what you look for next. If you fix today's oversized item with compression and move on, you haven't asked the underlying question: is there another item, table-wide, quietly approaching the same ceiling right now? The honest version of this fix is a query across your table (or a scheduled job) that checks item sizes proactively, not a one-off patch on the item that happened to fail first.
🙋♂️ Jake's Reality Check
"Can't I just ask AWS to bump the limit for my account? We pay for support."
No — and this is the one AWS limit that a support ticket won't move. Most DynamoDB quotas you can bump through a Service Quotas request: throughput, the number of global secondary indexes, replica counts. Item size isn't on that list. It's documented as a hard constraint on the item itself, not a per-account throughput number, and every pattern in this post exists because that door genuinely doesn't open.
Which pattern for which situation
Ethan's rule of thumb, worked out over enough of these tickets: "Ask what the data is and what you do with it after you save it. The answer to both questions tells you the pattern — you almost never have to guess."
| Situation | Best pattern | Why |
|---|---|---|
| Long free text (notes, transcripts, resolution text) you never filter on | Compress | Shrinks it fast; you were never querying inside it anyway |
| A list or map that gets one more entry appended over and over (history, events, updates) | Vertical partitioning | Only fix that caps the growth instead of just shrinking today's snapshot |
| Photos, PDFs, exports, any binary file | Store in S3, keep a pointer | This is what S3 is built for; DynamoDB isn't a file store |
| Related records embedded wholesale (every order, every ticket, on a customer item) | Vertical partitioning | Each related record becomes its own item under a shared partition key |
| You need it stored and searchable/filterable later | Don't compress it | Compressed binary can't be filtered or scanned by DynamoDB at all |
Most real items need more than one pattern at once. Jake's ticket item ended up with the update history split off into its own items (vertical partitioning), the photos moved to S3 (pointer only), and the header item left alone — nothing on it needed compressing once the two big offenders were gone.
What item size does to your bill and your throughput
Item size isn't only a hard ceiling — it's also a cost lever you're pulling on every read and write, whether you notice it or not. DynamoDB rounds capacity consumption up in fixed increments: one write capacity unit covers a write of up to 1 KB, and one read unit covers a strongly consistent read of up to 4 KB (or two eventually consistent reads of that size). There's no fractional billing — an item at 3.3 KB is billed as if it were a full 4 KB, because the size rounds up to the nearest unit boundary.
| Item size | Write capacity units used | Strongly consistent read units used |
|---|---|---|
| 0.5 KB | 1 (rounded up from 0.5 KB) | 1 (rounded up to 4 KB) |
| 3.3 KB | 4 (rounded up) | 1 (rounded up to 4 KB) |
| 20 KB | 20 | 5 |
| 350 KB | 350 | 88 |
That last row is the quiet cost of letting an item creep up near 400 KB even before it fails: reading a 350 KB item costs roughly 88 times what reading a 4 KB item costs, every single time, whether or not the caller actually needed all 350 KB of it. This is where splitting a bulky item pays for itself twice — once by removing the risk of a failed write, and once by making the header item small enough that routine reads (a status check, a list view) stop paying to haul the whole history along for the ride. Every partition also has a hard ceiling of 3,000 read units and 1,000 write units per second regardless of table-level provisioning, so a hot, oversized item makes you hit that per-partition wall sooner too.
The secondary-index trap nobody warns you about
If your table has a local secondary index (LSI), the 400 KB limit gets stricter, not looser. DynamoDB documents a combined limit: for every LSI on a table, the size of the base item's data plus the size of that item's corresponding entry in the index (including key values and every projected attribute) together must fit inside 400 KB. Project every attribute onto an LSI and you've effectively halved the room your base item has to grow into, because now two copies of overlapping data are competing for the same ceiling.
The fix here is the same advice DynamoDB gives for index design generally: project only the attributes that index's queries actually need. DynamoDB caps you at 100 projected attributes combined across all of a table's local and global secondary indexes, and every attribute projected costs write capacity every time it changes — so a query that never needs the full notes history shouldn't have it projected at all. Let DynamoDB fetch that field from the base table on the rare occasion it's needed, instead of paying for a second copy of it on every write, permanently, against the same size ceiling.
Global secondary indexes (GSIs) don't share this exact combined-size rule — they're physically separate, with their own item-size accounting — but the same design instinct applies: projecting large or fast-growing attributes onto an index you rarely query multiplies your write cost and your storage cost for no real benefit.
Transactions and batch writes with large items
Two more limits worth knowing if oversized items keep showing up inside multi-item operations:
TransactWriteItemsgroups up to 100 action requests targeting items in one or more tables (same account, same Region). DynamoDB will cancel the whole transaction if any single item inside it becomes larger than 400 KB as a result of the write, or if the aggregate size of every item in the transaction exceeds 4 MB — whichever limit gets hit first. If you're batching several updates to the same ticket's related items into one transaction, both ceilings are live at once.BatchWriteItemdoesn't have that same combined-transaction ceiling, but the same per-item 400 KB constraint still governs every item in the batch individually — one oversized item, or a total request over 16 MB, rejects the whole batch before anything is written.
For Jake's ticketing app, this mattered once the team started writing the header item and its first status-update item together, in a single transaction, on ticket creation — good practice for keeping the two in sync, but a second place the same 400 KB math needed to be respected, this time per item inside the transaction rather than once per call.
DynamoDB Streams and Lambda: where the fix creates a new problem
Jake's team wanted every status update to trigger a text message to the customer, so they wired a Lambda function to a DynamoDB Streams event on the ticket table. This is a standard pattern — and it comes with its own quiet version of the same size problem, one layer up.
A DynamoDB Streams record can be configured to capture KEYS_ONLY (just the primary key), NEW_IMAGE (the whole item after the change), OLD_IMAGE (the whole item before the change), or NEW_AND_OLD_IMAGES — both, in the same record. That last option is the common choice for auditing or diffing what changed, and it's also required for global tables. The catch: if your item is sitting near 400 KB, a NEW_AND_OLD_IMAGES stream record for that write is carrying close to two copies of it — up to roughly 800 KB of item data in one record, before you even get to the rest of the event envelope.
For stream-based triggers, Lambda's event source mapping invokes your function synchronously, and synchronous invocations are capped at a 6 MB payload for the whole event, which can bundle several stream records into one batch. A table of small items never comes close to that number. A table with items regularly brushing up against 400 KB, streamed with both images, can fill that 6 MB batch payload with a surprisingly small number of records — and a Lambda invocation that exceeds the payload limit fails outright, the same abrupt way the original PutItem would have.
⚠️ What this actually breaks
Splitting a large item into smaller pieces (vertical partitioning) doesn't just fix the write — it also shrinks every downstream stream record for that item, which is one more reason vertical partitioning beats compression for anything wired to a Streams consumer. A compressed-but-still-large item is still a large item on the wire to Lambda.
Ethan's advice to the team here was blunt: "Don't set NEW_AND_OLD_IMAGES because it seems safer. Set the view type to whatever your consumer actually needs to compare — if the SMS function only needs to know the new status, NEW_IMAGE alone is half the payload for no loss of information."
What DAX does — and doesn't — fix about large items
Somewhere in the middle of this project, someone suggested putting DAX (DynamoDB Accelerator, an in-memory caching layer) in front of the table "to help with the size problem." It's worth stopping that idea before it costs anyone a weekend.
DAX maintains an item cache, keyed by primary key, that stores the results of GetItem and BatchGetItem calls, plus a separate query cache for Query and Scan results. It sits between your application and DynamoDB and serves repeated reads from memory instead of the table — which is genuinely useful for read-heavy, hot-key workloads. What it does not do is change what DynamoDB will accept on a write. DAX caches items; it doesn't shrink them, and it has no say over the 400 KB write-time validation, which happens on DynamoDB's side regardless of whether a cache sits in front of it.
🙋♂️ Jake's Reality Check
"So DAX is a dead end for this, or is it worth adding anyway?"
It's not a dead end, it's just not this fix. DAX is genuinely worth it later, once the item sizes are under control, for the ticket header items customers re-check constantly (status, ETA). But adding a cache in front of a table that's still failing writes is solving the wrong layer of the problem — Ethan's line to the team was, "You don't put a faster shelf in front of a door that won't open."
There's a second reason large items and DAX are an awkward pair even once writes are healthy: DAX node memory is finite, and cache eviction begins before the node's memory utilization even reaches 100%. A handful of near-400 KB items consume disproportionately more of that cache memory than a table of small items, pushing smaller, more frequently accessed items out sooner. Keeping items small isn't just a write-time concern — it's what lets a cache like DAX actually do its job efficiently once you do add one.
Catching this before it fails a customer, not after
DynamoDB's own best-practice guidance is direct about this: monitor and alert on item sizes that approach the 400 KB ceiling, using the ReturnConsumedCapacity parameter on your writes, so you catch a growing item before it fails rather than after. The consumed-capacity response on every write tells you, indirectly, how big the item you just wrote actually was — a write that suddenly jumps from 4 write units to 40 is telling you something changed on that item, well before it ever reaches 400.
The practical version of this for a small team without a dedicated observability stack: log consumed write capacity on every write to the tables holding your largest, longest-lived items, and set a simple threshold alert — say, anything crossing 300 write units (roughly 300 KB) — so someone gets a warning weeks before a customer gets a failed transaction. That's the difference between "we found this in a postmortem" and "we found this in a dashboard on a quiet Tuesday."
✅ Why this is worth setting up even on a small table
The failure mode here isn't gradual and visible — it's binary. An item sits comfortably under 400 KB for a year of ordinary use, then one more append fails the write outright, at whatever moment that append happens to occur. A size alert converts a customer-facing outage into a maintenance ticket you get to schedule yourself.
Repairing a table that's already got oversized items
Everything above assumes you're designing before the failure. Jake's team wasn't — they had a live table with an unknown number of tickets quietly sitting close to the line, and no inventory of which ones. This is the part most articles skip, and it's the part that actually mattered that Saturday.
- Find the offenders first — don't guess. A
Scanacross the table, reading the sameReturnConsumedCapacityfield mentioned above per item, tells you consumed write capacity per item without inspecting every attribute by hand. Remember that a singleScanrequest reads at most 1 MB of data before returning aLastEvaluatedKeyfor the next page — on any table bigger than 1 MB, you're paginating through the whole scan, not doing it in one call. - Rank by size, not by age. The instinct is to fix the item that just failed. The right move is a short list of every item above, say, 300 KB — the one that failed today is rarely the only one close to the wall.
- Migrate one item shape at a time. If ten different item "types" share a table (a common DynamoDB pattern), fix the one type that's actually growing unbounded before touching anything else. Don't turn a targeted repair into a full-table redesign under deadline pressure.
- Write the new shape alongside the old, then cut over. For the ticket table, that meant writing new status updates as separate items (the vertical-partitioning pattern) while the old, already-oversized
noteslists stayed in place, untouched, on their existing items — nothing forces you to rewrite history to stop it from getting worse. - Backfill only the items that need it. Not every historical ticket needs its notes list split apart immediately; only the ones already near 400 KB, or the ones still open and actively growing, are urgent. A slow, scheduled backfill job beats a rushed table-wide migration.
- Re-run the size scan after the backfill to confirm the offenders list actually shrank, and keep the monitoring from the previous section running so the next one gets caught early instead of live, on a Saturday.
✅ Why this is the one to use once you're already in trouble
It's tempting to treat an oversized-item incident as a reason to redesign the whole table. Resist it. Find the specific items and the specific item shape causing the failure, fix that shape, and leave everything else alone — a panic-driven full redesign under time pressure is how a one-table problem becomes a three-week outage.
The fixes people try first that don't actually work
Ethan has heard every version of these, usually from someone hoping there's a setting to flip.
"Switch to on-demand capacity mode"
On-demand mode changes how you're billed for throughput and removes the need to pre-provision capacity. It does nothing at all to the 400 KB item size limit — that's a constraint on the item itself, not a throughput setting, and it applies identically under provisioned and on-demand billing.
"File a Service Quotas increase request"
Covered above, but worth repeating because it's the single most common wrong move: throughput quotas, the number of global secondary indexes per table, and account-level provisioned capacity are the kinds of things a Service Quotas request can raise — DynamoDB's own quotas documentation lists throughput, table counts, and index counts as "Adjustable." Item size isn't on that page at all; it lives on a separate page called Constraints, and constraints aren't the kind of thing a quota increase touches.
"Just delete old entries from the list to make room"
This "fix" quietly deletes real data — Jake's repair-history entries, in this example — to buy headroom on an item that's going to hit the exact same wall again the next time it grows. It's the compression problem without even the storage-cost benefit: you've shrunk today's number without touching the reason it keeps climbing.
Frequently asked questions
Is 400 KB per item or per table?
Per item, always. A table can hold billions of items, and each one independently gets its own 400 KB budget. There's no table-wide storage limit tied to this number — DynamoDB tables can grow to essentially unlimited total size; it's each individual row that's capped.
Does the 400 KB count include attribute names?
Yes. Both the attribute names and their values count toward the total, measured in UTF-8 bytes. Long, descriptive field names on an item with many attributes do add up, though they're rarely the whole cause of an oversized item on their own.
Can I request a limit increase for item size?
No. Unlike throughput or the number of secondary indexes, the 400 KB item size limit isn't a quota you can request through Service Quotas. It's a fixed constraint on the item itself.
What exact error does DynamoDB return?
A ValidationException, typically with the message "Item size has exceeded the maximum allowed size." The write is rejected outright — nothing is partially saved or silently truncated.
Do lists and maps have their own size limit inside an item?
No. There's no separate cap on how many values a list, map, or set can hold — the only ceiling is the overall 400 KB item size that the list or map lives inside, including everything nested within it.
Should I always compress large attributes as the default fix?
Only if you never need to query or filter on that attribute's content. Compression turns the value into opaque binary, which DynamoDB can no longer scan or filter against. If the growth is caused by an unbounded list rather than one naturally large text field, compression also just delays the same failure rather than fixing it.
What's the difference between compressing and offloading to S3?
Compression keeps the (smaller) value inside the DynamoDB item. Offloading to S3 removes the value from DynamoDB entirely and stores only a pointer. S3 is the right call for binary files like photos and PDFs; compression is the right call for long text you're keeping inside the item but never filter on.
What is vertical partitioning in DynamoDB?
Splitting one large item into several smaller items that share the same partition key, distinguished by different sort key values. Because they share a partition key, they form an item collection you can retrieve together with a single Query, while no individual item ever has to hold the full, ever-growing dataset alone.
Can transactions fail because of item size even if each item is under 400 KB?
Yes. TransactWriteItems has a separate ceiling: the aggregate size of every item in the transaction cannot exceed 4 MB, even when each individual item stays comfortably under 400 KB on its own.
How does a local secondary index affect the item size limit?
It tightens it. For every LSI on a table, the combined size of the base item's data and that item's corresponding LSI entry (keys plus every projected attribute) must together fit within 400 KB — effectively sharing the ceiling between the base item and the index copy.
Does a global secondary index have the same combined limit?
GSIs are physically separate from the base table and have their own item-size accounting rather than the same explicit combined 400 KB rule that applies to LSIs. Even so, projecting large attributes onto a GSI you rarely query still multiplies the write and storage cost of every update to that item.
Does DynamoDB Streams double the size problem for consumers?
It can. A NEW_AND_OLD_IMAGES stream record carries both the pre- and post-write versions of the item, so a near-400 KB item can produce a stream record close to 800 KB. Since stream-triggered Lambda invocations are subject to the 6 MB synchronous payload limit, tables with large items and wide stream views can hit batch-size problems downstream of the original write.
Does DAX fix the 400 KB item size problem?
No. DAX is a read-side, in-memory cache that stores the results of GetItem, BatchGetItem, Query, and Scan calls — it has no role in write-time validation. A write that's too large for DynamoDB is still too large for DynamoDB with DAX sitting in front of it.
Can DynamoDB and S3 be updated together in one transaction?
No. DynamoDB doesn't support transactions that span S3 and DynamoDB together. If you're offloading large values to S3, your application has to handle the case where one write succeeds and the other fails, including cleaning up any orphaned S3 object.
How do I get warned before an item hits the limit instead of finding out from a failed write?
Use the ReturnConsumedCapacity parameter on your writes and alert when consumed write capacity for a table's items climbs past a threshold well below 400 (roughly 400 write units). DynamoDB's own best-practice guidance recommends exactly this kind of proactive monitoring.
Is it ever fine to just delete old data to stay under the limit?
Only if that data is genuinely disposable. Deleting entries from a growing list to buy headroom doesn't address why the item keeps growing, and it destroys real history in the process — it's usually a worse trade than the time it takes to split the item properly.
Revision note. Written September 2026, covering DynamoDB's current 400 KB item size constraint, item collections, local and global secondary index behavior, DynamoDB Streams record composition, DAX caching behavior, and the transaction and batch write limits in effect as of this writing. This is a hard architectural limit, not a version number, so it should hold steady until AWS documents otherwise — if that ever changes, this post will be updated to match. If you're reading this because a write just failed on you mid-afternoon, take a breath: nothing is lost, the data's still there, it just needs a slightly different shape to live in.