General
The DynamoDB Scan Filter Illusion: Why Filter Expressions Silently Bankrupt Cloud Budgets
Oct 26, 2026/9 min read
calricon / field note
Developers migrating from SQL to DynamoDB assume FilterExpressions act like WHERE clauses. In reality, DynamoDB bills for every byte read from disk before discarding non-matching items—triggering massive RCU invoices, empty pagination loops, and partition throttling. Here is how to fix it.
Oct 26, 2026/General
Advertisement
<h1>The DynamoDB Scan Filter Illusion: Why Filter Expressions Silently Bankrupt Cloud Budgets</h1>
<p><strong>When software engineers transition from relational databases like PostgreSQL or MySQL to Amazon DynamoDB, their muscle memory betrays them almost immediately.</strong> Accustomed to decades of writing expressive SQL queries with flexible <code>WHERE</code> clauses, developers discover DynamoDB's <code>FilterExpression</code> parameter on the <code>Scan</code> and <code>Query</code> APIs. They read the AWS documentation, see that they can filter items by arbitrary attributes, and implement queries like <code>Scan(FilterExpression="order_status = :status AND created_at > :date")</code>. In staging environments with a few thousand test records, the API returns in under 20 milliseconds, returning the exact two matching items requested. The team ships to production, confident in NoSQL's infinite horizontal scalability. Three weeks later, the FinOps dashboard triggers an emergency billing alarm: a single internal administrative microservice has generated a $14,000 AWS invoice, consuming millions of Read Capacity Units (RCUs) while simultaneously throttling mission-critical customer checkout transactions on the same primary table.</p>
<p>The root cause is the <strong>DynamoDB Scan Filter Illusion</strong>. A <code>FilterExpression</code> does not reduce the volume of data DynamoDB reads from physical disk. It is not an SQL <code>WHERE</code> clause, and it does not leverage indexes. In DynamoDB's internal storage architecture, reading and filtering are two strictly decoupled phases: the storage engine reads up to 1MB of raw data from solid-state storage, <strong>bills you for 100% of the read capacity consumed</strong>, and only then applies your filter in memory—silently discarding 99.9% of the records you just paid for. Here is an architectural deep dive into the hidden economics of DynamoDB filtering, the anatomy of empty-page pagination death loops, and the engineering blueprint required to build high-velocity, cost-effective NoSQL access patterns.</p>
<h2>The Physics of a Read: How DynamoDB Evaluates Filter Expressions</h2>
<p>To understand why your cloud invoice explodes, we must examine the internal execution pipeline of a DynamoDB <code>Scan</code> or <code>Query</code> operation. When a client invokes the DynamoDB API with a <code>FilterExpression</code>, the request executes in two distinct steps across the AWS storage fleet:</p>
<ol>
<li><strong>Phase 1: The Storage Read (The Billing Step):</strong> DynamoDB accesses the physical partition drives (SSDs). For a <code>Scan</code>, it reads items sequentially across physical storage partitions up to a hard limit of <strong>1 Megabyte of raw data</strong>. For a <code>Query</code>, it reads all items matching the <code>KeyConditionExpression</code> (Partition Key and optional Sort Key) up to 1MB. <em>This is the exact moment billing occurs.</em> DynamoDB calculates Read Capacity Units (1 RCU per 4KB for strongly consistent reads, 0.5 RCU per 4KB for eventually consistent reads) based on the total byte size of every item evaluated from disk.</li>
<li><strong>Phase 2: In-Memory Filtering (The Discard Step):</strong> Only after the 1MB of raw data has been read and metered does DynamoDB evaluate the <code>FilterExpression</code>. It evaluates attributes against your filter criteria, strips out non-matching items, and returns only the matching survivors to the client.</li>
</ol>
<p>If your table contains 100GB of historical data, and you run a <code>Scan</code> with a <code>FilterExpression</code> to find the three records matching <code>status = 'FAILED'</code>, DynamoDB reads all 100GB from disk, charges you for <strong>25,000,000 Read Capacity Units</strong>, and returns three items. You paid for a high-performance database to read every byte of an entire enterprise dataset just to run an in-memory <code>if</code>-statement.</p>
<blockquote>
<p>"In SQL, a WHERE clause prevents disk reads. In DynamoDB, a FilterExpression merely hides the bill until your invoice arrives. You pay for everything DynamoDB touches, not what it returns."</p>
</blockquote>
<h2>Trap 1: The Empty-Page Pagination Death Spiral</h2>
<p>The decoupling of physical reads from in-memory filtering creates a devastating behavioral pathology in client applications: <strong>The Empty-Page Pagination Loop</strong>.</p>
<p>DynamoDB enforces a strict 1MB evaluation cap per API invocation. This limit applies to the data <em>read before filtering</em>, not the data returned. Consider what happens when your query matches sparse data across a large table:</p>
<ol>
<li>Your application calls <code>Scan(Limit=20, FilterExpression="tenant_id = :id")</code>.</li>
<li>DynamoDB reads 1MB of items from physical disk. It charges you 250 RCUs.</li>
<li>None of the items in that specific 1MB slice match <code>tenant_id = :id</code>.</li>
<li>DynamoDB returns an HTTP 200 OK with:
<pre><code>{
"Items": [],
"Count": 0,
"ScannedCount": 850,
"LastEvaluatedKey": { "id": "item_850" }
}</code></pre>
</li>
<li>Your client application (using the AWS SDK) sees that <code>LastEvaluatedKey</code> is present and that it hasn't reached its target of 20 items. It immediately issues the next request.</li>
<li>If the matching items reside 500MB deeper in the table, the client SDK executes <strong>500 consecutive synchronous HTTP requests</strong> in a rapid-fire pagination loop!</li>
</ol>
<h3>The Consequences of the Pagination Spiral:</h3>
<ul>
<li><strong>API Gateway & Lambda Timeouts:</strong> The microservice thread is locked in an HTTP polling loop for 30 seconds, eventually hitting AWS Lambda's 15-minute or API Gateway's 29-second integration timeout. The user sees an HTTP 504 Gateway Timeout.</li>
<li><strong>Connection Exhaustion:</strong> Dozens of concurrent requests running empty-page pagination loops exhaust outgoing HTTP connection pools, blocking unrelated threads.</li>
<li><strong>Runaway Costs:</strong> A single user clicking "Next Page" in an internal admin portal burns thousands of RCUs in under ten seconds.</li>
</ul>
<h2>Trap 2: Partition Throttling and Neighbor Impact</h2>
<p>In Amazon DynamoDB, data is horizontally partitioned across multiple physical storage nodes based on the hash of the Partition Key (PK). Each individual physical partition is capped at <strong>1,000 Write Capacity Units (WCUs)</strong> and <strong>3,000 Read Capacity Units (RCUs)</strong>.</p>
<p>When an application executes a table <code>Scan</code>, DynamoDB touches every single physical partition in the table simultaneously:</p>
<ul>
<li>If the table uses Provisioned Capacity, the scan instantly consumes the table's entire provisioned read capacity budget.</li>
<li>Online Transaction Processing (OLTP) point reads (<code>GetItem</code>) serving active end-users on the same partitions are starved of capacity.</li>
<li>Critical customer checkout or login operations begin failing with <code>ProvisionedThroughputExceededException</code> (HTTP 400). SREs scramble to debug a customer-facing outage caused by an internal reporting script scanning a table!</li>
</ul>
<h2>Cost & Performance Breakdown: Scan with Filter vs. Query vs. Sparse GSI</h2>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>Scan + FilterExpression</th>
<th>Query + FilterExpression</th>
<th>Sparse Global Secondary Index</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Disk Read Volume</strong></td>
<td>100% of the entire table</td>
<td>100% of the Partition Key</td>
<td><strong>Only matching indexed items</strong></td>
</tr>
<tr>
<td><strong>RCU Billing Basis</strong></td>
<td>Every byte on the table</td>
<td>Every byte in the partition</td>
<td><strong>Exact payload returned (Zero waste)</strong></td>
</tr>
<tr>
<td><strong>100k Record Cost (0.1% match)</strong></td>
<td>$12.50 per scan pass</td>
<td>$1.25 (if scoped to PK)</td>
<td><strong>$0.012 (1,000x cheaper)</strong></td>
</tr>
<tr>
<td><strong>Empty Page Risk</strong></td>
<td>Severe (Cascading empty pages)</td>
<td>Moderate</td>
<td><strong>Zero (Every page has matching items)</strong></td>
</tr>
<tr>
<td><strong>P99 Latency</strong></td>
<td>5,000ms – 30,000ms+</td>
<td>250ms – 1,200ms</td>
<td><strong>8ms – 15ms</strong></td>
</tr>
</tbody>
</table>
<h2>The Calricon Production Playbook: Eliminating Scan Traps Permanently</h2>
<p>To eliminate runaway RCU bills, banish empty-page pagination loops, and guarantee single-digit millisecond response times, implement this four-pillar NoSQL design model:</p>
<h3>1. Leverage Sparse Global Secondary Indexes (GSIs) for Status Flags</h3>
<p>If your application frequently queries for items in a specific transitional state (e.g., <code>status = 'PENDING'</code> or <code>needs_retry = true</code>), never scan the base table with a filter. Build a <strong>Sparse GSI</strong>:</p>
<p>DynamoDB only populates a Global Secondary Index if the item contains the index's partition key. If 99% of your orders are <code>COMPLETED</code> and only 1% are <code>PENDING</code>, construct your data model so that only pending orders possess the GSI key attribute:</p>
<pre><code>// DATA MODELING: Sparse Index Pattern
// For completed orders, omit the attribute entirely!
const orderRecord = {
order_id: "ord_98412",
customer_id: "cust_123",
total: 89.50,
status: "COMPLETED"
// pending_status attribute is OMITTED!
};
// For pending orders, include the GSI partition key:
const pendingOrderRecord = {
order_id: "ord_98413",
customer_id: "cust_456",
total: 120.00,
status: "PENDING",
pending_status: "PENDING", // GSI Partition Key
created_at: 1729864200 // GSI Sort Key
};
</code></pre>
<p>When you query <code>idx_pending_orders</code>, the index contains only the 1% of active records. A query reads zero completed records, returns instantaneous results, and consumes pennies in read capacity.</p>
<h3>2. Construct Composite Partition Keys for Multi-Attribute Queries</h3>
<p>If you must filter across multiple dimensions (e.g., retrieving orders for a specific tenant within a specific status), encode the query attributes directly into the primary key or secondary index keys:</p>
<pre><code># COMPOSITE KEY PATTERN:
# Partition Key: tenant_id#status (e.g., "corp_alpha#PENDING")
# Sort Key: created_at (e.g., "2026-10-26T12:00:00Z")
response = table.query(
KeyConditionExpression=Key('pk').eq('corp_alpha#PENDING') & Key('sk').begins_with('2026-10')
)
</code></pre>
<p>By transforming in-memory filter attributes into strict Key Condition expressions, DynamoDB isolates disk reads strictly to the matching partition slice. Zero non-matching bytes are read from disk.</p>
<h3>3. Offload Complex Filtering to OpenSearch via DynamoDB Streams</h3>
<p>DynamoDB is a key-value and document store engineered for ultra-high-throughput OLTP workloads with predictable access patterns. It is fundamentally not designed for ad-hoc analytical filtering, free-text search, or arbitrary faceted navigation.</p>
<p>If your application requires flexible multi-field filtering across dozens of optional parameters (e.g., e-commerce product catalogs or compliance auditing portals):</p>
<ul>
<li>Enable <strong>DynamoDB Streams</strong> on the base table.</li>
<li>Deploy the AWS Zero-ETL integration or a lightweight Lambda forwarder to continuously stream table mutations into an <strong>Amazon OpenSearch Service</strong> cluster.</li>
<li>Route OLTP point operations (<code>GetItem</code>, <code>PutItem</code>, <code>UpdateItem</code>) to DynamoDB, and route complex multi-attribute filter searches to OpenSearch.</li>
</ul>
<h3>4. Enforce Static Analysis Rules to Ban Table Scans in CI/CD</h3>
<p>Never rely on developer discipline alone. Add linting rules to your CI/CD pipeline to catch unindexed table scans before code merges to production:</p>
<pre><code># ESLint / AST Rule: Flag DynamoDB Scan operations
# Mandate that every Scan invocation requires an explicit FinOps override tag
rules:
no-dynamodb-unbounded-scan:
selector: "CallExpression[https://www.google.com/url?q=http://callee.property.name&source=gmail&ust=1793096475064000&sa=E='scan']"
message: "Unbounded DynamoDB Scan detected. Use Query against a GSI or obtain FinOps architectural sign-off."
</code></pre>
<h2>Conclusion</h2>
<p>DynamoDB's <code>FilterExpression</code> is one of the most misunderstood parameters in modern cloud architecture. By obscuring massive physical disk reads behind a clean, high-level API abstraction, it lures engineering teams into deploying catastrophic scan patterns that destroy database throughput and trigger massive financial waste. By recognizing that filtering in DynamoDB happens after billing, replacing scans with sparse indexes, designing composite partition keys, and streaming complex workloads to search engines, platform teams can eliminate scan traps permanently and harness the true, cost-efficient power of cloud-native NoSQL.</p>
<p><strong>When software engineers transition from relational databases like PostgreSQL or MySQL to Amazon DynamoDB, their muscle memory betrays them almost immediately.</strong> Accustomed to decades of writing expressive SQL queries with flexible <code>WHERE</code> clauses, developers discover DynamoDB's <code>FilterExpression</code> parameter on the <code>Scan</code> and <code>Query</code> APIs. They read the AWS documentation, see that they can filter items by arbitrary attributes, and implement queries like <code>Scan(FilterExpression="order_status = :status AND created_at > :date")</code>. In staging environments with a few thousand test records, the API returns in under 20 milliseconds, returning the exact two matching items requested. The team ships to production, confident in NoSQL's infinite horizontal scalability. Three weeks later, the FinOps dashboard triggers an emergency billing alarm: a single internal administrative microservice has generated a $14,000 AWS invoice, consuming millions of Read Capacity Units (RCUs) while simultaneously throttling mission-critical customer checkout transactions on the same primary table.</p>
<p>The root cause is the <strong>DynamoDB Scan Filter Illusion</strong>. A <code>FilterExpression</code> does not reduce the volume of data DynamoDB reads from physical disk. It is not an SQL <code>WHERE</code> clause, and it does not leverage indexes. In DynamoDB's internal storage architecture, reading and filtering are two strictly decoupled phases: the storage engine reads up to 1MB of raw data from solid-state storage, <strong>bills you for 100% of the read capacity consumed</strong>, and only then applies your filter in memory—silently discarding 99.9% of the records you just paid for. Here is an architectural deep dive into the hidden economics of DynamoDB filtering, the anatomy of empty-page pagination death loops, and the engineering blueprint required to build high-velocity, cost-effective NoSQL access patterns.</p>
<h2>The Physics of a Read: How DynamoDB Evaluates Filter Expressions</h2>
<p>To understand why your cloud invoice explodes, we must examine the internal execution pipeline of a DynamoDB <code>Scan</code> or <code>Query</code> operation. When a client invokes the DynamoDB API with a <code>FilterExpression</code>, the request executes in two distinct steps across the AWS storage fleet:</p>
<ol>
<li><strong>Phase 1: The Storage Read (The Billing Step):</strong> DynamoDB accesses the physical partition drives (SSDs). For a <code>Scan</code>, it reads items sequentially across physical storage partitions up to a hard limit of <strong>1 Megabyte of raw data</strong>. For a <code>Query</code>, it reads all items matching the <code>KeyConditionExpression</code> (Partition Key and optional Sort Key) up to 1MB. <em>This is the exact moment billing occurs.</em> DynamoDB calculates Read Capacity Units (1 RCU per 4KB for strongly consistent reads, 0.5 RCU per 4KB for eventually consistent reads) based on the total byte size of every item evaluated from disk.</li>
<li><strong>Phase 2: In-Memory Filtering (The Discard Step):</strong> Only after the 1MB of raw data has been read and metered does DynamoDB evaluate the <code>FilterExpression</code>. It evaluates attributes against your filter criteria, strips out non-matching items, and returns only the matching survivors to the client.</li>
</ol>
<p>If your table contains 100GB of historical data, and you run a <code>Scan</code> with a <code>FilterExpression</code> to find the three records matching <code>status = 'FAILED'</code>, DynamoDB reads all 100GB from disk, charges you for <strong>25,000,000 Read Capacity Units</strong>, and returns three items. You paid for a high-performance database to read every byte of an entire enterprise dataset just to run an in-memory <code>if</code>-statement.</p>
<blockquote>
<p>"In SQL, a WHERE clause prevents disk reads. In DynamoDB, a FilterExpression merely hides the bill until your invoice arrives. You pay for everything DynamoDB touches, not what it returns."</p>
</blockquote>
<h2>Trap 1: The Empty-Page Pagination Death Spiral</h2>
<p>The decoupling of physical reads from in-memory filtering creates a devastating behavioral pathology in client applications: <strong>The Empty-Page Pagination Loop</strong>.</p>
<p>DynamoDB enforces a strict 1MB evaluation cap per API invocation. This limit applies to the data <em>read before filtering</em>, not the data returned. Consider what happens when your query matches sparse data across a large table:</p>
<ol>
<li>Your application calls <code>Scan(Limit=20, FilterExpression="tenant_id = :id")</code>.</li>
<li>DynamoDB reads 1MB of items from physical disk. It charges you 250 RCUs.</li>
<li>None of the items in that specific 1MB slice match <code>tenant_id = :id</code>.</li>
<li>DynamoDB returns an HTTP 200 OK with:
<pre><code>{
"Items": [],
"Count": 0,
"ScannedCount": 850,
"LastEvaluatedKey": { "id": "item_850" }
}</code></pre>
</li>
<li>Your client application (using the AWS SDK) sees that <code>LastEvaluatedKey</code> is present and that it hasn't reached its target of 20 items. It immediately issues the next request.</li>
<li>If the matching items reside 500MB deeper in the table, the client SDK executes <strong>500 consecutive synchronous HTTP requests</strong> in a rapid-fire pagination loop!</li>
</ol>
<h3>The Consequences of the Pagination Spiral:</h3>
<ul>
<li><strong>API Gateway & Lambda Timeouts:</strong> The microservice thread is locked in an HTTP polling loop for 30 seconds, eventually hitting AWS Lambda's 15-minute or API Gateway's 29-second integration timeout. The user sees an HTTP 504 Gateway Timeout.</li>
<li><strong>Connection Exhaustion:</strong> Dozens of concurrent requests running empty-page pagination loops exhaust outgoing HTTP connection pools, blocking unrelated threads.</li>
<li><strong>Runaway Costs:</strong> A single user clicking "Next Page" in an internal admin portal burns thousands of RCUs in under ten seconds.</li>
</ul>
<h2>Trap 2: Partition Throttling and Neighbor Impact</h2>
<p>In Amazon DynamoDB, data is horizontally partitioned across multiple physical storage nodes based on the hash of the Partition Key (PK). Each individual physical partition is capped at <strong>1,000 Write Capacity Units (WCUs)</strong> and <strong>3,000 Read Capacity Units (RCUs)</strong>.</p>
<p>When an application executes a table <code>Scan</code>, DynamoDB touches every single physical partition in the table simultaneously:</p>
<ul>
<li>If the table uses Provisioned Capacity, the scan instantly consumes the table's entire provisioned read capacity budget.</li>
<li>Online Transaction Processing (OLTP) point reads (<code>GetItem</code>) serving active end-users on the same partitions are starved of capacity.</li>
<li>Critical customer checkout or login operations begin failing with <code>ProvisionedThroughputExceededException</code> (HTTP 400). SREs scramble to debug a customer-facing outage caused by an internal reporting script scanning a table!</li>
</ul>
<h2>Cost & Performance Breakdown: Scan with Filter vs. Query vs. Sparse GSI</h2>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>Scan + FilterExpression</th>
<th>Query + FilterExpression</th>
<th>Sparse Global Secondary Index</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Disk Read Volume</strong></td>
<td>100% of the entire table</td>
<td>100% of the Partition Key</td>
<td><strong>Only matching indexed items</strong></td>
</tr>
<tr>
<td><strong>RCU Billing Basis</strong></td>
<td>Every byte on the table</td>
<td>Every byte in the partition</td>
<td><strong>Exact payload returned (Zero waste)</strong></td>
</tr>
<tr>
<td><strong>100k Record Cost (0.1% match)</strong></td>
<td>$12.50 per scan pass</td>
<td>$1.25 (if scoped to PK)</td>
<td><strong>$0.012 (1,000x cheaper)</strong></td>
</tr>
<tr>
<td><strong>Empty Page Risk</strong></td>
<td>Severe (Cascading empty pages)</td>
<td>Moderate</td>
<td><strong>Zero (Every page has matching items)</strong></td>
</tr>
<tr>
<td><strong>P99 Latency</strong></td>
<td>5,000ms – 30,000ms+</td>
<td>250ms – 1,200ms</td>
<td><strong>8ms – 15ms</strong></td>
</tr>
</tbody>
</table>
<h2>The Calricon Production Playbook: Eliminating Scan Traps Permanently</h2>
<p>To eliminate runaway RCU bills, banish empty-page pagination loops, and guarantee single-digit millisecond response times, implement this four-pillar NoSQL design model:</p>
<h3>1. Leverage Sparse Global Secondary Indexes (GSIs) for Status Flags</h3>
<p>If your application frequently queries for items in a specific transitional state (e.g., <code>status = 'PENDING'</code> or <code>needs_retry = true</code>), never scan the base table with a filter. Build a <strong>Sparse GSI</strong>:</p>
<p>DynamoDB only populates a Global Secondary Index if the item contains the index's partition key. If 99% of your orders are <code>COMPLETED</code> and only 1% are <code>PENDING</code>, construct your data model so that only pending orders possess the GSI key attribute:</p>
<pre><code>// DATA MODELING: Sparse Index Pattern
// For completed orders, omit the attribute entirely!
const orderRecord = {
order_id: "ord_98412",
customer_id: "cust_123",
total: 89.50,
status: "COMPLETED"
// pending_status attribute is OMITTED!
};
// For pending orders, include the GSI partition key:
const pendingOrderRecord = {
order_id: "ord_98413",
customer_id: "cust_456",
total: 120.00,
status: "PENDING",
pending_status: "PENDING", // GSI Partition Key
created_at: 1729864200 // GSI Sort Key
};
</code></pre>
<p>When you query <code>idx_pending_orders</code>, the index contains only the 1% of active records. A query reads zero completed records, returns instantaneous results, and consumes pennies in read capacity.</p>
<h3>2. Construct Composite Partition Keys for Multi-Attribute Queries</h3>
<p>If you must filter across multiple dimensions (e.g., retrieving orders for a specific tenant within a specific status), encode the query attributes directly into the primary key or secondary index keys:</p>
<pre><code># COMPOSITE KEY PATTERN:
# Partition Key: tenant_id#status (e.g., "corp_alpha#PENDING")
# Sort Key: created_at (e.g., "2026-10-26T12:00:00Z")
response = table.query(
KeyConditionExpression=Key('pk').eq('corp_alpha#PENDING') & Key('sk').begins_with('2026-10')
)
</code></pre>
<p>By transforming in-memory filter attributes into strict Key Condition expressions, DynamoDB isolates disk reads strictly to the matching partition slice. Zero non-matching bytes are read from disk.</p>
<h3>3. Offload Complex Filtering to OpenSearch via DynamoDB Streams</h3>
<p>DynamoDB is a key-value and document store engineered for ultra-high-throughput OLTP workloads with predictable access patterns. It is fundamentally not designed for ad-hoc analytical filtering, free-text search, or arbitrary faceted navigation.</p>
<p>If your application requires flexible multi-field filtering across dozens of optional parameters (e.g., e-commerce product catalogs or compliance auditing portals):</p>
<ul>
<li>Enable <strong>DynamoDB Streams</strong> on the base table.</li>
<li>Deploy the AWS Zero-ETL integration or a lightweight Lambda forwarder to continuously stream table mutations into an <strong>Amazon OpenSearch Service</strong> cluster.</li>
<li>Route OLTP point operations (<code>GetItem</code>, <code>PutItem</code>, <code>UpdateItem</code>) to DynamoDB, and route complex multi-attribute filter searches to OpenSearch.</li>
</ul>
<h3>4. Enforce Static Analysis Rules to Ban Table Scans in CI/CD</h3>
<p>Never rely on developer discipline alone. Add linting rules to your CI/CD pipeline to catch unindexed table scans before code merges to production:</p>
<pre><code># ESLint / AST Rule: Flag DynamoDB Scan operations
# Mandate that every Scan invocation requires an explicit FinOps override tag
rules:
no-dynamodb-unbounded-scan:
selector: "CallExpression[https://www.google.com/url?q=http://callee.property.name&source=gmail&ust=1793096475064000&sa=E='scan']"
message: "Unbounded DynamoDB Scan detected. Use Query against a GSI or obtain FinOps architectural sign-off."
</code></pre>
<h2>Conclusion</h2>
<p>DynamoDB's <code>FilterExpression</code> is one of the most misunderstood parameters in modern cloud architecture. By obscuring massive physical disk reads behind a clean, high-level API abstraction, it lures engineering teams into deploying catastrophic scan patterns that destroy database throughput and trigger massive financial waste. By recognizing that filtering in DynamoDB happens after billing, replacing scans with sparse indexes, designing composite partition keys, and streaming complex workloads to search engines, platform teams can eliminate scan traps permanently and harness the true, cost-efficient power of cloud-native NoSQL.</p>
Advertisement