Amazon DynamoDB provides Time to Live (TTL) to remove items that are no longer needed. It is commonly used for session records, temporary authentication tokens, cached data, short-lived application events, and other records with a limited lifetime.
TTL is easy to configure, but one behavior often surprises developers: an item can pass its expiration time and still appear in a query or scan.
This happens because DynamoDB TTL does not delete expired items immediately. Instead, DynamoDB identifies expired items and removes them asynchronously in the background. Until deletion occurs, an expired item may remain in the table and can still be returned by database operations.
Understanding this behavior is important when building applications that depend on expiration for correctness, access control, or data cleanup. This article explains how DynamoDB TTL works, how to configure it, and how to handle expired items safely in application code.
What Is DynamoDB TTL?
DynamoDB TTL is a feature that automatically deletes items after a specified expiration time.
To use TTL, a table must contain an attribute that stores the expiration timestamp as a Unix epoch time in seconds. You choose the attribute name when enabling TTL.
For example, an item representing a temporary session might look like this:
{
"sessionId": "session-1001",
"userId": "user-501",
"status": "active",
"expiresAt": 1793275200
}
Here, expiresAt contains the expiration timestamp.
DynamoDB checks the configured TTL attribute and identifies items whose expiration time has passed. It then deletes eligible items through its background cleanup process.
The important distinction is that expiration eligibility and physical deletion are separate events.
An item may be expired according to its timestamp but remain stored until DynamoDB processes it.
Common Uses of TTL
TTL is useful when an application needs to remove records after a defined period.
Common examples include:
- Temporary user sessions.
- One-time verification codes.
- Short-lived application state.
- Temporary API response data.
- Expiring event records.
- Temporary workflow information.
However, TTL should not be treated as a precise timer or as the only mechanism enforcing authorization and access restrictions.
Why Expired Items Still Appear in Queries
Suppose an application creates a session that expires after 30 minutes.
The application stores an expiration timestamp, and the session reaches that time. The application may expect the item to disappear immediately, but DynamoDB does not guarantee immediate deletion.
The lifecycle looks like this:
- The application writes an item with a TTL timestamp.
- The expiration timestamp passes.
- DynamoDB identifies the item as eligible for expiration.
- A background process eventually deletes the item.
- Subsequent reads no longer find the deleted item.
Between steps 2 and 4, the expired item can still exist in the table.
A Query or Scan does not automatically exclude every item whose TTL timestamp has passed. If an expired item remains physically present and matches the operation's conditions, it may be returned.
For example, this operation can return expired records:
const result = await documentClient.send(
new ScanCommand({
TableName: "UserSessions"
})
);
The scan retrieves matching items from the table without automatically applying your application's expiration rules.
The same principle applies to queries against a partition key. If expired records still exist and satisfy the key condition, the query can return them.
TTL Is Asynchronous
DynamoDB manages expiration and deletion in the background. The time between expiration and deletion can vary.
Therefore, an application should not assume that an item disappears at the exact timestamp stored in its TTL attribute.
This matters when an application displays active sessions, processes temporary jobs, or checks whether a verification code remains valid.
If application correctness depends on expiration, enforce the expiration condition in application logic or in the database access pattern rather than waiting for physical deletion.
How to Enable TTL on a DynamoDB Table
Before using TTL, create an expiration attribute in your item schema and enable TTL for that attribute on the table.
Consider a table named UserSessions with a TTL attribute called expiresAt.
You can enable TTL using the AWS CLI:
aws dynamodb update-time-to-live \
--table-name UserSessions \
--time-to-live-specification \
"Enabled=true,AttributeName=expiresAt"
This command enables TTL using the expiresAt attribute.
The attribute name is case-sensitive. If your items store the expiration timestamp in expirationTime but TTL is configured for expiresAt, DynamoDB will not use the differently named attribute to expire those items.
TTL configuration also takes time to become active. Verify the table's TTL status before assuming that expiration processing is configured correctly.
Store Expiration as Unix Epoch Seconds
DynamoDB expects the TTL attribute to be a Number containing Unix epoch time in seconds.
The value represents the number of seconds since January 1, 1970, UTC.
For example, this JavaScript code calculates an expiration timestamp 30 minutes in the future:
const expiresAt = Math.floor(Date.now() / 1000) + 30 * 60;
The result can be stored with the item:
const item = {
sessionId: "session-1001",
userId: "user-501",
expiresAt
};
The use of Math.floor(Date.now() / 1000) is important because JavaScript's Date.now() returns milliseconds, while DynamoDB TTL requires seconds.
If you accidentally store milliseconds, the timestamp will be much larger than the intended expiration time, and the item may not expire when expected.
Prevent Expired Items From Appearing in Application Results
The most reliable approach is to apply an expiration condition whenever your application reads records that must still be valid.
There are different ways to implement this, depending on whether the application uses a query, a scan, or a direct item lookup.
Filter Expired Items in a Scan
Suppose the application needs to retrieve session records that have not expired.
With the AWS SDK for JavaScript v3, you can use a filter expression:
import {
DynamoDBDocumentClient,
ScanCommand
} from "@aws-sdk/lib-dynamodb";
const now = Math.floor(Date.now() / 1000);
const result = await documentClient.send(
new ScanCommand({
TableName: "UserSessions",
FilterExpression: "attribute_not_exists(expiresAt) OR expiresAt > :now",
ExpressionAttributeValues: {
":now": now
}
})
);
console.log(result.Items);
This filter keeps items that either do not have an expiration attribute or have an expiration timestamp later than the current time.
If every item must have a TTL value, use the stricter condition:
FilterExpression: "expiresAt > :now"
The choice depends on the table's data model. Items without an expiration attribute are not automatically expired by TTL, so the application must decide whether those items should be treated as permanent or invalid.
One important limitation is that a DynamoDB filter expression does not reduce the amount of data read before filtering. A scan can still consume substantial read capacity even if most items are excluded from the returned results.
For large tables, avoid relying on scans for routine application queries. Prefer a data model and key condition that retrieve only the records the application needs.
Filter Expired Items in a Query
A query is usually more efficient than a scan when the application knows the partition key.
For example:
import { QueryCommand } from "@aws-sdk/lib-dynamodb";
const now = Math.floor(Date.now() / 1000);
const result = await documentClient.send(
new QueryCommand({
TableName: "UserSessions",
KeyConditionExpression: "userId = :userId",
FilterExpression: "expiresAt > :now",
ExpressionAttributeValues: {
":userId": "user-501",
":now": now
}
})
);
console.log(result.Items);
This example assumes that userId is the table's partition key or is otherwise supported by the table's key schema for the query being performed.
The filter excludes expired items from the returned results. However, DynamoDB evaluates the key condition before applying the filter, so the filter does not reduce the read capacity consumed by items evaluated for the query.
If your table has a different key schema, adapt the key condition accordingly.
Also remember that a query can return paginated results. If you need all matching records, continue requesting pages while LastEvaluatedKey is present and pass it as ExclusiveStartKey in the next request.
Handle Direct Item Reads Correctly
Filtering query results is not enough when an application retrieves an individual session by its primary key.
For example, an application may request a session directly:
import { GetCommand } from "@aws-sdk/lib-dynamodb";
const result = await documentClient.send(
new GetCommand({
TableName: "UserSessions",
Key: {
sessionId: "session-1001"
}
})
);
If the item has expired but has not yet been deleted, the operation can still return it.
The application should check the expiration timestamp before treating the session as valid.
const item = result.Item;
const now = Math.floor(Date.now() / 1000);
if (!item) {
console.log("Session not found");
} else if (
typeof item.expiresAt !== "number" ||
item.expiresAt <= now
) {
console.log("Session is expired or invalid");
} else {
console.log("Session is valid");
}
This example treats a missing or invalid expiration attribute as an invalid session. That policy is appropriate for a session table in which every session must expire, but other applications may use different rules.
For authentication and authorization, the validity check must occur before granting access. Deleting an expired session eventually is not a substitute for checking its expiration during the authentication flow.
TTL and Filter Expressions: Important Differences
TTL and filter expressions can both affect which records an application sees, but they serve different purposes.
| Feature | DynamoDB TTL | Filter expression |
|---|
| Primary purpose | Automatically delete expired items | Exclude items from operation results |
| Deletion behavior | Asynchronous background deletion | Does not delete items |
| Applies to stored items | Yes, eventually | No |
| Reduces data returned | After the item is deleted | Yes, for the current operation |
| Reduces data read before filtering | Not a query filter | Generally no |
| Suitable for strict application validity checks | No, not by itself | Useful when applied correctly, but direct reads still need checks |
TTL is a storage lifecycle feature. Filter expressions are a way to control the results returned by supported read operations.
Most production applications benefit from using both: TTL for eventual cleanup and explicit expiration checks for application correctness.
Best Practices for Production Applications
Use a Consistent TTL Attribute
Choose one attribute name, such as expiresAt, and use it consistently across the table. Configure TTL using the exact same name.
Calculate Timestamps in Seconds
Convert milliseconds to seconds before storing the value. Add tests that verify the generated expiration timestamp is within the expected range.
Enforce Expiration in Application Logic
Check expiration before using sessions, tokens, temporary permissions, or other time-sensitive records. Do not assume that the background deletion process has already removed them.
Design Queries Around the Access Pattern
Prefer partition-key queries over full-table scans when possible. Filter expressions can remove expired records from returned results, but they do not eliminate the read cost of the items evaluated before filtering.
Test Missing and Invalid TTL Values
Decide how the application should handle items without an expiration attribute, malformed data, or timestamps that are already in the past.
For temporary credentials and sessions, rejecting records with missing expiration values is often safer than assuming they are permanent.
Monitor TTL Configuration and Cleanup
Verify that TTL is enabled on the intended attribute. During troubleshooting, inspect actual stored values and confirm that they use Unix epoch seconds.
If expired records remain visible, check whether their timestamps have passed, whether the configured attribute name matches, and whether the application explicitly filters or validates expiration.
Common Mistakes to Avoid
| Mistake | Result | Correct approach |
|---|
| Expecting immediate deletion | Expired records may remain visible | Account for asynchronous cleanup |
| Storing milliseconds instead of seconds | Items may expire much later than intended | Convert timestamps to epoch seconds |
| Assuming queries exclude expired records | Expired items may appear in results | Add an expiration condition |
| Using a scan for every request | Unnecessary read capacity consumption | Prefer queries that match the key schema |
| Assuming a filter expression deletes data | Records remain stored until TTL or another deletion process removes them | Use TTL or an explicit delete operation for cleanup |
| Ignoring missing expiration values | Records may never be selected for TTL deletion | Validate the data model and define an explicit policy |
Advantages and Disadvantages of DynamoDB TTL
Advantages
- Automatically removes eligible items without requiring a custom scheduled deletion process.
- Helps manage temporary records and control long-term storage growth.
- Works well for sessions, temporary application state, and expiring event records.
- Separates data lifecycle management from ordinary application write operations.
Disadvantages
- Deletion is asynchronous and does not happen at an exact expiration time.
- Expired records may remain visible until background cleanup removes them.
- Application-level filtering and validation require additional logic.
- Filter expressions do not eliminate the read capacity consumed by items evaluated before filtering.
Summary
DynamoDB TTL is useful for automatically cleaning up records that are no longer needed, but it should not be treated as an immediate deletion mechanism.
An item can pass its expiration timestamp and remain available to queries, scans, and direct reads until DynamoDB deletes it in the background. To handle this correctly, store TTL values as Unix epoch seconds, configure the appropriate expiration attribute, and check expiration explicitly whenever application behavior depends on whether a record is still valid.
Use filter expressions to exclude expired items from query results, but remember that filtering does not generally reduce the read capacity consumed by the items evaluated. For direct reads, validate the expiration timestamp before using the returned item.
The key principle is simple: use TTL for eventual cleanup and application-level expiration checks for correctness.
Join the conversation! Your thoughts help the community grow.