DynamoDBのTTL(Time to Live)は、項目に有効期限の属性を持たせ、期限を過ぎた項目をDynamoDBが書き込みキャパシティを使わずに消してくれる機能です。ただし期限が来た瞬間には消えません。公式の記述は「通常数日以内」で、それまで項目は読み取りに返り続けます。この記事では、TTL属性の型と計算、有効化の手順と1時間の変更制限、削除タイミング、期限切れ項目の除外、Streamsでの後処理、消えないときの確認手順を、公式ドキュメントとDynamoDB Local 3.3.2での実測(2026年10月)をもとに整理します。
まとめ:DynamoDB TTLの要点
- 属性の型:Number型のUNIX epoch秒。文字列・5年より過去の値は削除対象外。ミリ秒をNumber型で保存すると秒として解釈され、意図した日時には削除されない。
- 削除タイミング:開発者ガイドは「通常数日以内」、APIリファレンスは「通常2日以内」。どちらも保証値ではない。
- 期限切れの項目:削除されるまで読み取りに返り、ストレージと読み取りの料金もかかる。
FilterExpressionで除外する。 - 有効化:全パーティションへの反映に約1時間。同じテーブルへの
UpdateTimeToLiveは1時間以内の再実行がValidationExceptionになる。属性名を変えるには一度無効化する。 - 料金:削除が起きたリージョンでは書き込みキャパシティを消費しない。グローバルテーブルのレプリカへ複製される削除は課金される。MRSC構成のグローバルテーブルはTTL非対応。
- 後処理:TTL削除はStreamsに
userIdentity.type=Service付きで流れるので、Lambdaのイベントフィルターでこれだけを拾える。DynamoDB Localではこのフィールドが付かず、フィルターの検証はできない。
DynamoDB TTLの仕組みとDeleteItemとの違い
TTLは、テーブルごとに1つの属性名を「有効期限」として登録し、現在時刻がその値を過ぎた項目をバックグラウンドの処理が削除する仕組みです。属性名は自由に決められ、大文字と小文字を区別します(expireAt・expire_at・ttlなどがよく使われます)。
アプリケーションからDeleteItemで消す場合との違いは料金です。公式の開発者ガイドは、TTLが期限切れの項目を書き込みスループットを消費せずに削除すると書いています。古いデータをScanで探してDeleteItemするバッチを自前で回すと、読み取りと書き込みの両方が課金されます。TTLに任せればどちらも発生しません。削除された項目は通常の削除と同じく、ローカルセカンダリインデックス(LSI)とグローバルセカンダリインデックス(GSI)からも消えます。
DynamoDBの全体像やRDSとの違いはAmazon DynamoDBとは:RDSとの違い・料金の仕組み・使い方と設計の要点で扱っています。
TTL属性の型と有効期限の計算方法
TTLが読むのはNumber型で保存したUNIX epoch時刻の秒だけです。有効期限の計算に関する公式ページには、ほかの形式の値はTTLの処理が無視すること、そして「5年より過去の値は削除の対象にならない」ことが書かれています。0や1970年に近い値を入れても消えないのはこのためです。
| 保存した値 | 例 | TTLの扱い |
|---|---|---|
| Number・秒(正しい) | 1793145600 | その時刻を過ぎたら削除対象 |
| Number・ミリ秒 | 1793145600000 | 遠い未来と解釈され削除されない |
| String | “1793145600” | Number型でないため無視 |
| Number・5年より過去 | 現在から6年前の秒 | 削除対象外 |
有効期限は「作成時刻または最終更新時刻 + 保持したい秒数」で計算します。90日なら7,776,000秒です。Python(boto3のresource)では、time.time()の小数をint()で切り捨ててから足します。
import time
import boto3
table = boto3.resource("dynamodb").Table("Sessions")
# 作成時刻から90日後を有効期限にする(UNIX epoch秒・整数)
expire_at = int(time.time()) + 90 * 24 * 60 * 60
table.put_item(Item={
"pk": "session#abc123",
"expireAt": expire_at, # intはNumber型として保存される
})
JavaScriptのDate.now()やJavaのSystem.currentTimeMillis()はミリ秒を返すので、1000で割って切り捨てないと13桁になり、項目は消えません。期限を延ばしたいときは属性の値を書き換え、TTLの対象から外したいときは属性ごと削除します。
DynamoDBでTTLを有効化する手順
TTLの有効化はテーブル単位の設定で、項目に期限属性を書き込む作業とは別です。有効化しただけでは何も消えず、登録した属性名の値を持つ項目だけが対象になります。有効化の公式手順によれば、全パーティションに反映されるまで約1時間かかります。
マネジメントコンソールでのTTL有効化
対象テーブルの「追加設定(Additional settings)」タブにあるTime to Live (TTL)で「オンにする(Turn on)」を選び、属性名を入力します。同じ画面で日時を指定すると、その時点で期限切れになる項目のサンプルを表示できるので、属性名と値の単位が合っているかを有効化前に確かめられます。
AWS CLIでのTTL有効化と状態確認
update-time-to-liveで有効化し、describe-time-to-liveで状態を確認します。
aws dynamodb update-time-to-live \
--table-name Sessions \
--time-to-live-specification "Enabled=true, AttributeName=expireAt"
aws dynamodb describe-time-to-live --table-name Sessions
# {"TimeToLiveDescription": {"TimeToLiveStatus": "ENABLED", "AttributeName": "expireAt"}}
CloudFormationでのテーブルとTTLの定義
CloudFormation(およびServerless FrameworkやAWS SAM)ではTimeToLiveSpecificationに書きます。CloudFormationのリファレンスには、AttributeNameを変えるには先にTTLを無効化し、新しい属性名で有効化し直す必要があると書かれています。テンプレートの属性名だけを書き換えてデプロイしても、1回では切り替わりません。
Resources:
SessionsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Sessions
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
TimeToLiveSpecification:
AttributeName: expireAt
Enabled: true
TTL再変更の1時間制限とValidationException
UpdateTimeToLiveのAPIリファレンスは、変更の反映に最大1時間かかり、その間に同じテーブルへUpdateTimeToLiveを呼ぶとValidationExceptionになると定めています。有効化の公式手順はさらに、この操作が冪等ではないこと、すでに有効なテーブルへの有効化や、別の属性名での有効化もエラーになることを明記しています。
実務で問題になるのは2つの場面です。1つは、デプロイのたびにTTLを有効化するスクリプトで、2回目以降は必ず失敗します。「already enabled」系のエラーは成功として扱うか、describe-time-to-liveで状態を見てから呼びます。もう1つは属性名の変更で、無効化してから有効化するまでの間に1時間の制限がかかります。属性名は最初に決めたものを使い続けるのが安全です。
期限切れから削除までの時間と未削除の項目の扱い
TTL削除時間の公式記述:「数日以内」と「2日以内」
削除までの時間について、AWSのドキュメントには2つの記述があります。開発者ガイドは「通常は期限から数日以内(typically within a few days)」、APIリファレンスは「通常は期限から2日以内(typically deletes expired items within two days)」です。2026年10月時点で両方が公開されています。どちらも「typically」で、APIリファレンスは削除までの時間がワークロードによって変わるとも書いています。「数日」も削除期限の上限ではありません。削除時刻の保証が必要な要件にはTTLだけを使わず、明示的な削除処理と完了確認を設計してください。
Query・Scanでの期限切れ項目の除外
削除されるまで、期限切れの項目はGetItem・Query・Scanに返ります。期限切れ項目の扱いに関する公式ページによれば、削除されるまではストレージと読み取りの料金にも数えられます。Query・Scanでは、現在時刻と比べるFilterExpressionで除外します。GetItemにはフィルター式がないため、取得した項目の有効期限をアプリケーション側で確認し、期限切れなら使用しません。
import time
import boto3
from boto3.dynamodb.conditions import Key, Attr
table = boto3.resource("dynamodb").Table("Sessions")
# 期限が現在時刻より後の項目だけを返す
resp = table.query(
KeyConditionExpression=Key("pk").eq("session#abc123"),
FilterExpression=Attr("expireAt").gt(int(time.time())),
)
items = resp["Items"]
FilterExpressionは読み取った後に絞り込むので、除外した項目の分の読み取り料金は減りません。後述の実測では、ScanでScannedCountが4件、返ったCountが2件でした。期限属性を持たない項目はgtの比較で除外されるので、TTLを使わない項目が混在するテーブルではAttr("expireAt").not_exists() | Attr("expireAt").gt(...)のように条件を足します。
TTL項目の更新条件と期限切れの判定
期限切れでも削除前の項目は更新でき、期限属性を未来の値にすると削除の対象から外れます。公式ページは、期限切れの項目への書き込みを避けるため、有効期限が現在時刻より後かを条件式で確かめてから更新する例を示しています。
# 期限が切れていない場合だけ有効期限を現在から30日後に設定する
table.update_item(
Key={"pk": "session#abc123"},
UpdateExpression="SET expireAt = :new",
ConditionExpression="expireAt > :now",
ExpressionAttributeValues={
":new": int(time.time()) + 30 * 24 * 60 * 60,
":now": int(time.time()),
},
)
条件を満たさないとConditionalCheckFailedExceptionが返るので、呼び出し側で「期限切れ」として扱います。
DynamoDB LocalでTTLの挙動を確かめた結果
AWSが配布するDynamoDB Local 3.3.2(2026年10月15日リリース)をDockerで動かし、boto3 1.43.120からTTLの挙動を確かめました(2026年10月28日・イメージamazon/dynamodb-local:latestを-inMemoryで起動し、Python 3.12のコンテナから接続)。テーブルに5件の項目を入れ、TTLを有効化して経過を見た結果です。
| 項目の期限属性 | 30秒後 | 180秒後 | FilterExpressionの結果 |
|---|---|---|---|
| Number・秒(60秒前) | 削除 | 削除 | ― |
| Number・ミリ秒(60秒前) | 残る | 残る | 期限内として返る |
| String・秒(60秒前) | 残る | 残る | 除外 |
| Number・6年前の秒 | 残る | 残る | 除外 |
| Number・1時間後 | 残る | 残る | 期限内として返る |
ミリ秒の項目はTTLに消されないうえ、FilterExpressionでも「まだ期限内」と判定されて返ってきます。ミリ秒の混入は、削除漏れと読み取りの両方で気づきにくい不具合になります。
本番のDynamoDBと違った点が3つあります。
- 削除の速さ:1秒前に期限が切れた項目は約1秒後に消えた。本番の「数日以内」とは桁が違うので、Localで消えたことは本番の削除時間の目安にならない。
- 1時間の変更制限:有効化の直後に無効化し、さらに有効化してもエラーにならなかった。一方、有効なテーブルへの有効化(
TimeToLive is already enabled)と別の属性名での有効化(TimeToLive is active on a different AttributeName)はValidationExceptionになった。 - Streamsのレコード:TTLで消えた項目のレコードは、生のレスポンスで
eventNameがUNKNOWN_TO_SDK_VERSION、userIdentityが空だった。手動のDeleteItemはREMOVEで出る。本番のuserIdentityを前提にしたLambdaのフィルターは、Localでは確かめられない。
LocalはTTL属性の型や単位のテストには使えますが、削除の時間・変更の制限・Streamsの判定は本番の小さなテーブルで確かめるべきです。Localへの接続やテーブルの中身の確認にはNoSQL Workbenchの使い方:DynamoDBの設計・ローカル接続・操作手順が使えます。
StreamsとLambdaによるTTL削除項目のアーカイブ
「削除された項目の内容をS3に保存したい」「削除を監査ログに残したい」ときは、テーブルのDynamoDB Streamsを有効にし、TTL削除のレコードだけをLambdaで処理します。StreamsとTTLの公式ページによると、TTLで削除された項目のレコードには次のuserIdentityが付きます。
"userIdentity": {
"type": "Service",
"principalId": "dynamodb.amazonaws.com"
}
Lambdaのイベントソースマッピングにこの値のフィルターを付けると、通常の書き込みや手動の削除ではLambdaが起動しません。削除前の値を受け取るため、ストリームの表示形式はOLD_IMAGEかNEW_AND_OLD_IMAGESにします。
aws lambda create-event-source-mapping \
--function-name archive-expired \
--event-source-arn arn:aws:dynamodb:ap-northeast-1:123456789012:table/Sessions/stream/2026-10-28T00:00:00.000 \
--starting-position LATEST \
--filter-criteria '{"Filters": [{"Pattern": "{\"userIdentity\":{\"type\":[\"Service\"],\"principalId\":[\"dynamodb.amazonaws.com\"]}}"}]}'
グローバルテーブルでは、このuserIdentityが付くのは削除が実行されたリージョンのレコードだけで、複製先のリージョンのレコードには付きません。アーカイブ処理を全リージョンに置いても、動くのは削除が起きたリージョンだけです。LambdaでなくEventBridge Pipesでフィルターをかける方法はEventBridge Pipesの使い方:DynamoDB Streamsのフィルタとエンリッチ実装にまとめています。
グローバルテーブルでのTTL:MRECは複製の削除が課金・MRSCは非対応
グローバルテーブルの仕組みに関する公式ページによると、TTLに対応するのは結果整合性のMREC構成だけで、強整合性のMRSC構成ではTTLは使えません。MRECでは、TTLの設定は全レプリカで同期され、あるリージョンで起きた削除が全レプリカへ複製されます。
料金は、削除が起きたリージョンでは書き込みキャパシティを消費しない一方、複製先では複製書き込み(プロビジョンドなら複製WCU、オンデマンドなら複製書き込みユニット)として課金されます。プロビジョンドのレプリカでは、通常の書き込みとTTL削除の複製を合わせた量が上限を超えるとスロットリングが起きます。大量の項目が同じ時刻に期限切れになる設計では、複製先の容量を見積もりに入れてください。MRECとMRSCの違いはDynamoDB Global Tables(グローバルテーブル)とは:MRSC・マルチアカウントと料金・作成手順、単価はDynamoDBの料金を東京リージョンの実額で試算する:請求が跳ねる設計と単価の下げ方で扱っています。
TTLが向くデータと向かない要件
TTLが向くのは、数日の遅れがあっても困らず、消すこと自体が目的のデータです。有効期限をアプリケーション側で判定するセッションやワンタイムトークンの後片付け、一時的な計算結果、保持期間を過ぎたイベントログが典型です。
次の要件では、TTLだけに頼らないでください。
- 期限の時刻ちょうどに失効させたい:トークンの有効期限やクーポンの締め切りは、読み取り側で
FilterExpressionか条件式を使って判定する。TTLは後片付けに使う。 - 削除を法的な義務として証明したい:TTLの削除時刻は数日の幅があり、正確な時刻を約束できない。明示的な
DeleteItemと監査ログで残す。 - TTL属性に意味のある値を入れたい:公式手順の注記どおり、TTL属性の値はDynamoDBの診断ログに平文で記録される。期限の時刻以外の情報を同じ属性に持たせない。
DynamoDBやLambdaを組み合わせたAWS環境の設計・構築を外部に相談したい場合は、インフラ構築(AWS・Google Cloud・Azure)の支援内容をご覧ください。
TTLで項目が削除されないときの確認手順
「TTLを設定したのに消えない」ときは、上から順に確認します。
| 確認すること | 見る場所 |
|---|---|
| TTLが有効か・属性名が一致しているか | describe-time-to-live(大文字小文字も区別) |
| Number型か | コンソールの項目表示・GetItemの型(N/S) |
| 秒か(10桁か) | 13桁ならミリ秒 |
| 5年より過去の値でないか | テストデータの固定値・0や小さい値 |
| 有効化から1時間経ったか | 反映には約1時間 |
| 期限から数日経ったか | CloudWatchのTimeToLiveDeletedItemCount |
削除が動いているかは、CloudWatchのメトリクスTimeToLiveDeletedItemCount(TTLが削除した項目数)で見られます。このメトリクスはテーブル全体のTTL削除件数を示すため、0でも削除待ちの場合があり、増えていても特定の項目に設定・値の問題が残る場合があります。未削除の項目の属性名・型・値も個別に確認してください。
見落としやすいのが、Lambdaを通さずAPI GatewayのAWSサービス統合から直接DynamoDBに書くケースです。マッピングテンプレートで使える$context.requestTimeEpochはミリ秒なので、そのまま期限の計算に使うとTTLが効きません。
よくある質問
DynamoDBのTTLの単位は秒ですか、ミリ秒ですか?
秒です。Number型のUNIX epoch秒で保存します。ミリ秒(13桁)は遠い未来と解釈され、削除されません。
DynamoDBのTTLは48時間で削除されますか?
保証されません。APIリファレンスは「通常2日以内」、開発者ガイドは「通常数日以内」と書いており、どちらも目安です。期限ちょうどに隠したい場合は読み取り時にFilterExpressionで除外します。
DynamoDBのTTLの属性名は何にすればよいですか?
自由に決められます。expireAt・expire_at・ttlなどが使われます。大文字と小文字を区別し、後から変えるには一度無効化して1時間の制限を待つ必要があるので、最初に決めた名前を使い続けます。
TTLの有効化や無効化を続けて実行するとどうなりますか?
同じテーブルへのUpdateTimeToLiveは、1時間以内に再実行するとValidationExceptionになります。すでに有効なテーブルへの有効化もエラーです。デプロイ用のスクリプトでは、事前にdescribe-time-to-liveで状態を確認します。
TTLで削除された項目をDynamoDB Streamsで区別できますか?
できます。TTL削除のレコードにはuserIdentity.typeがService、principalIdがdynamodb.amazonaws.comで付きます。グローバルテーブルの複製先と、DynamoDB Localのレコードには付きません。