DynamoDB TTL attribute must be a Number

TL;DR — DynamoDB's Time to Live only deletes an item when its designated TTL attribute holds a Number representing a Unix epoch timestamp in seconds. A String like "1735689600", a millisecond value, an ISO-8601 date, or a missing attribute is silently ignored — the item never expires. Store the TTL as an epoch-seconds Number and re-write the affected items.

What it means

# A second UpdateTimeToLive call within one hour of the first raises:
ValidationException  (TTL settings can only be modified once per table per hour)

# The quieter failure — no error at all, item just never expires:
TTL attribute "expiresAt" = "2026-01-01T00:00:00Z"   ← String, ignored
TTL attribute "expiresAt" = 1735689600000            ← milliseconds: tens of thousands of years away

Enabling TTL (UpdateTimeToLive) succeeds even when the attribute doesn't yet exist or is the wrong type — DynamoDB does not type-check it up front. The failure shows up later: the background TTL process only deletes an item when the attribute is a Number holding a Unix epoch timestamp in seconds that is in the past (and no more than five years in the past). Anything else is treated as "no expiry".

Why it happens

  • Stored as a String — the value is {"S": "1735689600"} instead of {"N": "1735689600"}. TTL ignores non-N types.
  • Milliseconds instead of secondsDate.now() (JavaScript) returns milliseconds; a 13-digit value is tens of thousands of years in the future, so the item effectively never expires.
  • An ISO-8601 / human date string rather than epoch seconds.
  • A timestamp more than five years in the past — the TTL process ignores it instead of deleting the item.
  • A different attribute name than the one registered with TTL (the name is case-sensitive).
  • Calling UpdateTimeToLive again too soon — the change takes up to one hour to fully process, and any additional UpdateTimeToLive call for the same table during that hour raises a ValidationException.

How to fix it

  1. Write the TTL value as a Number in epoch secondsMath.floor(Date.now() / 1000) + ttlSeconds in JavaScript, int(time.time()) + ttl in Python. Never store milliseconds.
  2. Use the N type, not S. With the low-level client that's {"N": "1735689600"}; the Document Client marshals a native number for you.
  3. Match the registered attribute name exactly, including case. Confirm it with DescribeTimeToLive.
  4. Backfill existing items — items written before the fix still carry the bad value; re-write them with a correct epoch-seconds Number.
  5. Wait an hour between TTL config changesUpdateTimeToLive takes up to one hour to process, and further calls during that window are rejected with a ValidationException.

Want to see the wire type of every attribute while you browse a table? The DynoTable desktop app renders N/S/M type tags inline, so a TTL stored as a String jumps out before it costs you an un-expired item.

FAQ

Why isn't my DynamoDB TTL deleting items? The TTL attribute must be a Number holding a Unix epoch timestamp in seconds. A String value, a milliseconds value, an ISO date, or a name that doesn't match the registered TTL attribute are all silently ignored, so the item never expires. Deletion is also not immediate — DynamoDB typically removes expired items within a few days of their expiration time.

Does DynamoDB validate the TTL attribute type when I enable TTL? No. UpdateTimeToLive succeeds even if the attribute is missing or the wrong type. The type requirement (Number, epoch seconds) is only enforced by the background deletion process, which is why a bad TTL fails quietly.

References

Last verified 2026-07-13 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.