<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Workload Shape | John Nessime</title>
	<atom:link href="https://john-nessime.com/blog/tag/workload-shape/feed/" rel="self" type="application/rss+xml" />
	<link>https://john-nessime.com/blog/tag/workload-shape/</link>
	<description>Cloud, DevOps, Data &#38; AI — Built, Tested, Explained</description>
	<lastBuildDate>Tue, 29 Sep 2026 06:05:28 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.2</generator>

<image>
	<url>https://john-nessime.com/blog/wp-content/uploads/2026/07/cropped-jn-32x32.png</url>
	<title>Workload Shape | John Nessime</title>
	<link>https://john-nessime.com/blog/tag/workload-shape/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>RDS vs DynamoDB: The Benchmark Isn&#8217;t the Decision</title>
		<link>https://john-nessime.com/blog/technical-guides/rds-vs-dynamodb-access-patterns/</link>
		
		<dc:creator><![CDATA[John Nessime]]></dc:creator>
		<pubDate>Tue, 29 Sep 2026 06:05:28 +0000</pubDate>
				<category><![CDATA[Cloud Computing]]></category>
		<category><![CDATA[Databases]]></category>
		<category><![CDATA[Technical Guides]]></category>
		<category><![CDATA[Amazon Aurora]]></category>
		<category><![CDATA[Amazon RDS]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWS Lambda]]></category>
		<category><![CDATA[Capacity Planning]]></category>
		<category><![CDATA[Connection Pooling]]></category>
		<category><![CDATA[Cost Optimization]]></category>
		<category><![CDATA[Data Modeling]]></category>
		<category><![CDATA[DynamoDB]]></category>
		<category><![CDATA[Hot Partition]]></category>
		<category><![CDATA[Over-Engineering]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[Provisioned Throughput]]></category>
		<category><![CDATA[Schema Design]]></category>
		<category><![CDATA[Serverless]]></category>
		<category><![CDATA[Throttling]]></category>
		<category><![CDATA[Workload Shape]]></category>
		<guid isPermaLink="false">https://john-nessime.com/blog/?p=738</guid>

					<description><![CDATA[<p>Latency charts don't decide this. The query nobody wrote down at the start does. How to build an access pattern inventory, where DynamoDB and RDS each genuinely win and hurt, a six-question decision procedure, and the arguments on both sides that fall apart under a real workload.</p>
<p>The post <a href="https://john-nessime.com/blog/technical-guides/rds-vs-dynamodb-access-patterns/">RDS vs DynamoDB: The Benchmark Isn&#8217;t the Decision</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Someone drops a benchmark link into the design review. Flat latency line for DynamoDB, a rising one for Postgres, and the data store is chosen before anybody has described what the application does.</p>



<p class="wp-block-paragraph">That argument settles architecture decisions all the time, and it&#8217;s the wrong argument. Both services return a primary-key lookup faster than your network hop and your ORM overhead. What hurts six months later is the query nobody wrote down at the start, and no benchmark will warn you about it.</p>



<p class="wp-block-paragraph">So this post skips the latency charts. It covers the access pattern inventory you write before picking anything, where each service genuinely wins and genuinely hurts, a decision procedure you can run in an afternoon, and the arguments on both sides that fall apart on contact with a real workload.</p>



<h2 class="wp-block-heading">Why the benchmark answers a question you don&#8217;t have</h2>



<p class="wp-block-paragraph">A benchmark measures a fixed operation under fixed conditions. Your application is not a fixed operation. It&#8217;s a set of reads and writes that grows every time somebody asks a new question about the data.</p>



<p class="wp-block-paragraph">The failure mode that decides RDS vs DynamoDB is almost never throughput. It&#8217;s a product manager asking, eight months in, for every account that hasn&#8217;t logged in this quarter, broken down by plan tier. On Postgres that&#8217;s an awkward query and maybe an index. On DynamoDB, if you didn&#8217;t model for it, it&#8217;s a full table scan, a new global secondary index, a backfill, and a standing write-capacity tax to keep it in sync.</p>



<p class="wp-block-paragraph">That asymmetry is the whole decision. A relational database defers the modeling cost and charges a little on every query for the flexibility. DynamoDB front-loads it: design keys around known patterns, get cheap predictable reads for exactly those, and pay dearly for anything else.</p>



<h2 class="wp-block-heading">Write the access pattern inventory first</h2>



<p class="wp-block-paragraph">Before comparing anything, write the list. Every read and write the application performs, in plain language, with the shape of the operation and how fast it needs to be. This much is enough:</p>



<pre class="wp-block-code"><code>PATTERN                                SHAPE           FREQ    LATENCY
get order by id                        point read      high    p99 &lt; 50ms
list a customer's orders, newest first range read      high    p99 &lt; 100ms
orders by status, across customers     cross-entity    low     seconds fine
revenue by product category, monthly   aggregate       low     minutes fine
find orders by partial address         text match      medium  p99 &lt; 300ms
mark order shipped + write audit row   multi-item write high   p99 &lt; 100ms</code></pre>



<p class="wp-block-paragraph">Now read the SHAPE column. If nearly every row is a point or range read, and the exceptions tolerate latency, DynamoDB is a live option. If the column is full of cross-entity reads, aggregates and text matching, you&#8217;re describing a relational workload and can stop shopping.</p>



<h3 class="wp-block-heading">The rows people leave out</h3>



<p class="wp-block-paragraph">The inventory is only useful if it&#8217;s honest, and the omissions are predictable:</p>



<ul class="wp-block-list">
<li>The admin back office. Support staff search by email, phone, partial name, anything. Ad hoc queries, and they never appear in the first inventory.</li>



<li>Reporting. Someone will want a monthly rollup. Decide now whether it lives in the operational store or streams out to a warehouse.</li>



<li>Deletion. Regulatory deletes cascade across entities. In a normalized schema that&#8217;s a transaction. In a denormalized single table it&#8217;s a fan-out you have to write and test yourself.</li>



<li>Backfills. Every schema change becomes a migration job when no schema is enforced at the storage layer.</li>
</ul>



<p class="wp-block-paragraph">Write them down even if the answer is &#8220;we&#8217;ll handle it elsewhere.&#8221; A pattern you consciously exile to a warehouse is fine. One you forgot is a rewrite.</p>



<h2 class="wp-block-heading">Where DynamoDB wins, and where it doesn&#8217;t</h2>



<p class="wp-block-paragraph">Give it its genuine case first, because it&#8217;s a strong one.</p>



<h3 class="wp-block-heading">Where it wins</h3>



<ul class="wp-block-list">
<li><strong>Connection model.</strong> It&#8217;s an HTTPS API with SigV4 auth, so there&#8217;s no pool to exhaust. That&#8217;s why it pairs cleanly with Lambda. A relational database behind high Lambda concurrency needs RDS Proxy or a pooler, which is another component you operate.</li>



<li><strong>Predictable reads at any size.</strong> A query against a well-chosen partition key costs the same at ten thousand items or ten billion. Nothing in Postgres gives you that for free.</li>



<li><strong>Idle cost.</strong> On-demand mode bills per request unit consumed, so a table with no traffic costs storage and nothing else. Useful for tenant-per-table designs and low-traffic side services.</li>



<li><strong>Operational surface.</strong> No minor version upgrades, no vacuum, no failover window, no parameter groups to argue about.</li>
</ul>



<p class="wp-block-paragraph">The query API is narrow on purpose. Give it an exact partition key plus an optional sort key condition, and it returns an ordered slice of an item collection:</p>



<pre class="wp-block-code"><code>aws dynamodb query 
  --table-name AppData 
  --key-condition-expression "PK = :pk AND begins_with(SK, :sk)" 
  --expression-attribute-values '{":pk":{"S":"CUSTOMER#42"},":sk":{"S":"ORDER#"}}'</code></pre>



<p class="wp-block-paragraph">One customer&#8217;s orders, in sort order, in one request, no join. If that call shape covers most of your inventory, you&#8217;re looking at the right service.</p>



<h3 class="wp-block-heading">Where it doesn&#8217;t</h3>



<ul class="wp-block-list">
<li><strong>Anything you didn&#8217;t model.</strong> New patterns mean new keys or a new index, and a new global secondary index means a backfill plus write capacity on every future write.</li>



<li><strong>Filtering is not querying.</strong> A <code>FilterExpression</code> runs after the read, so you pay capacity for every item examined and only save bandwidth. A filter that discards most of what it reads is a design smell.</li>



<li><strong>Aggregates.</strong> There is no <code>GROUP BY</code> and no <code>COUNT(*)</code> over a table. You maintain aggregates yourself, usually with DynamoDB Streams feeding a Lambda that updates a counter item, which means you own the correctness problem when that consumer fails.</li>



<li><strong>Hard physical limits.</strong> An item caps at 400 KB. A Query or Scan page returns at most 1 MB before you paginate. A single partition sustains roughly 3,000 read and 1,000 write units per second, so a hot key throttles while the table sits well under its quota.</li>



<li><strong>Index budget.</strong> Global secondary indexes default to twenty per table; local secondary indexes cap at five and can only be defined at table creation. You can&#8217;t index your way out of a bad key design forever.</li>
</ul>



<h2 class="wp-block-heading">Where RDS wins, and where it doesn&#8217;t</h2>



<h3 class="wp-block-heading">Where it wins</h3>



<ul class="wp-block-list">
<li><strong>Unknown future queries.</strong> The entire point. Joins, aggregates, window functions, ad hoc analysis, and a new index in one statement when something turns out slow.</li>



<li><strong>Constraints the database enforces.</strong> Foreign keys, uniqueness, check constraints, transactions across arbitrary tables. In DynamoDB each of those is application code you write and maintain.</li>



<li><strong>Extensions.</strong> PostGIS for geospatial, pgvector for embeddings, full-text search built in, all against the same rows in the same transaction.</li>



<li><strong>Portability.</strong> Postgres and MySQL run on RDS, in a container, or on a plain VPS from a provider like Contabo or InterServer, so migrating off is an export and an import. DynamoDB&#8217;s data model doesn&#8217;t port without rewriting your data access layer.</li>



<li><strong>Everyone already knows SQL.</strong> Underrated on a small team. Teaching four developers single-table design costs real review time.</li>
</ul>



<h3 class="wp-block-heading">Where it doesn&#8217;t</h3>



<ul class="wp-block-list">
<li><strong>Connections under serverless concurrency.</strong> Each Lambda execution environment opens its own connection, and the database refuses new ones once <code>max_connections</code> is reached. RDS Proxy multiplexes to fix that, but some operations pin a client to a backend connection and pinned connections aren&#8217;t reused. Watch <code>DatabaseConnectionsCurrentlySessionPinned</code> before trusting the pool.</li>



<li><strong>Instances are always on.</strong> A provisioned instance bills whether or not anyone queries it. Aurora Serverless v2 changes that with a minimum of zero capacity units on a supported engine version, but resuming from paused is slow enough that the first user after an idle period feels it. AWS documents resume as taking roughly fifteen seconds.</li>



<li><strong>Vertical scaling has a ceiling.</strong> Writes go to one primary and read replicas help reads only. Sharding is a project, not a setting.</li>



<li><strong>Maintenance is yours.</strong> Version upgrades, failover testing, long transactions blocking vacuum, connection storms on restart. Managed does not mean absent.</li>
</ul>



<p class="wp-block-paragraph">If you want a database that hibernates properly, it lives in the scaling configuration and the idle timeout is yours to set:</p>



<pre class="wp-block-code"><code>aws rds create-db-cluster 
  --db-cluster-identifier app-cluster 
  --engine aurora-postgresql 
  --serverless-v2-scaling-configuration 
      MinCapacity=0,MaxCapacity=4,SecondsUntilAutoPause=600 
  --manage-master-user-password</code></pre>



<p class="wp-block-paragraph">A minimum of zero only works on engine versions supporting automatic pause and resume, so check yours before planning a budget around it. If the cluster refuses the setting, that&#8217;s usually why.</p>



<h2 class="wp-block-heading">A decision procedure for RDS vs DynamoDB</h2>



<p class="wp-block-paragraph">Run these in order against the inventory you wrote. The first clear answer wins.</p>



<ol class="wp-block-list">
<li><strong>Can you enumerate the access patterns, and are they stable?</strong> If the product is still moving and nobody can say what the admin tools will need, pick the store that tolerates being wrong. That&#8217;s RDS.</li>



<li><strong>Does any pattern need a cross-entity aggregate on the hot path?</strong> A user-facing screen needing a count or sum across entities in under a second, with no way to precompute it, means RDS.</li>



<li><strong>Is there a natural partition key with even distribution?</strong> Tenant ID, user ID, device ID. If traffic concentrates on one key, the per-partition ceiling finds you regardless of capacity mode. No good key means no DynamoDB.</li>



<li><strong>What&#8217;s the concurrency shape of the compute tier?</strong> Thousands of short-lived Lambda invocations argue for DynamoDB. A few long-running containers with a warm pool remove the strongest argument against RDS.</li>



<li><strong>Does the workload go quiet?</strong> Bursty and mostly idle favors on-demand DynamoDB or a scale-to-zero Aurora cluster. Steady traffic makes provisioned capacity or a right-sized instance cheaper.</li>



<li><strong>Who maintains this in a year?</strong> If it&#8217;s one part-time developer, weight familiarity heavily. A design nobody on the team can debug is a liability whatever the benchmark said.</li>
</ol>



<h2 class="wp-block-heading">The cost model nobody actually models</h2>



<p class="wp-block-paragraph">The two bill on axes that don&#8217;t compare. RDS charges instance hours, storage, and depending on engine and storage type, I/O, busy or not. DynamoDB charges request units and storage, so an idle table is nearly free and a chatty one isn&#8217;t.</p>



<p class="wp-block-paragraph">The number that matters is request units per user action, and almost nobody calculates it. Reads bill in 4 KB increments and writes in 1 KB increments, strongly consistent reads cost twice what eventually consistent ones do, and transactional operations double again. Every index you project into consumes write units on every write that touches it. A screen firing six queries plus an audit write can cost many times the naive estimate.</p>



<p class="wp-block-paragraph">Work it out per action, multiply by real traffic, and sanity check the instance side with my <a href="https://john-nessime.com/tools/aws-cost-estimator/">AWS cost estimator</a> before the decision hardens. AWS recommends on-demand as the default capacity mode for most workloads, but steady predictable traffic is still cheaper provisioned, and you can flip between them:</p>



<pre class="wp-block-code"><code># which mode is this table on?
aws dynamodb describe-table --table-name AppData 
  --query "Table.BillingModeSummary.BillingMode"

# what is it actually consuming?
aws cloudwatch get-metric-statistics 
  --namespace AWS/DynamoDB 
  --metric-name ConsumedWriteCapacityUnits 
  --dimensions Name=TableName,Value=AppData 
  --statistics Sum --period 3600 
  --start-time $(date -u -d '1 day ago' +%Y-%m-%dT%H:%M:%SZ) 
  --end-time $(date -u +%Y-%m-%dT%H:%M:%SZ)</code></pre>



<p class="wp-block-paragraph">Switching billing mode is rate limited, so treat it as a planned change rather than an incident-time knob.</p>



<h2 class="wp-block-heading">Arguments that don&#8217;t survive contact</h2>



<ul class="wp-block-list">
<li><strong>&#8220;DynamoDB scales infinitely.&#8221;</strong> The service does. Your partition key might not. Uneven access concentrates load on one partition, and adaptive capacity only helps within limits.</li>



<li><strong>&#8220;Postgres can&#8217;t handle our scale.&#8221;</strong> Usually said before anyone has run <code>EXPLAIN (ANALYZE, BUFFERS)</code> on the slow query. A missing index and an N+1 in the ORM explain most claims of outgrowing relational.</li>



<li><strong>&#8220;We&#8217;ll add the indexes later.&#8221;</strong> True in Postgres, expensive in DynamoDB. A new global secondary index backfills across the whole table and then taxes every subsequent write.</li>



<li><strong>&#8220;Serverless database means no operations.&#8221;</strong> You trade instance operations for capacity and key-design operations. Throttling, hot partitions and stream consumer lag all need dashboards, exactly like a relational instance does.</li>



<li><strong>&#8220;We can just use both.&#8221;</strong> Sometimes right, often a way to avoid deciding. Two stores means two consistency models, two restore procedures, and a sync path that becomes the least reliable component you own.</li>
</ul>



<h2 class="wp-block-heading">How I&#8217;d decide</h2>



<p class="wp-block-paragraph">My default for a new application with an unfinished product definition is managed Postgres. Not because it&#8217;s better, but because being wrong about a relational schema costs an afternoon and being wrong about a key design costs a migration.</p>



<p class="wp-block-paragraph">I&#8217;d reach for DynamoDB first when the workload is a small closed set of patterns keyed off one identifier, with high write volume, tolerance for eventual consistency, and concurrency that would flatten a connection pool. Session stores, telemetry ingestion, per-user feeds, idempotency keys, feature flags. It&#8217;s excellent at all of those.</p>



<p class="wp-block-paragraph">The hybrid worth doing is narrow: relational as the system of record, DynamoDB for one high-volume pattern that joins nothing. The hybrid to avoid splits related entities across both stores and reconciles them in application code.</p>



<p class="wp-block-paragraph">Whichever you land on, instrument it on day one. Throttled requests and consumed capacity on the DynamoDB side, connection counts and slow queries on the relational side, on a CloudWatch or Grafana Cloud dashboard someone actually opens. If you&#8217;re restricting item-level access with IAM condition keys such as <code>dynamodb:LeadingKeys</code>, run the policy through my <a href="https://john-nessime.com/tools/aws-iam-policy-checker/">IAM policy checker</a> before it ships.</p>



<h2 class="wp-block-heading">Frequently asked questions</h2>



<h3 class="wp-block-heading">Is DynamoDB always faster than RDS?</h3>



<p class="wp-block-paragraph">For a primary-key lookup at large scale its latency is more consistent, because that operation&#8217;s cost doesn&#8217;t grow with table size. For a query it wasn&#8217;t modeled for it can be far slower, because the fallback is a scan. Speed depends on the match between query and key design, not on the service.</p>



<h3 class="wp-block-heading">Can I run analytics on DynamoDB?</h3>



<p class="wp-block-paragraph">Not directly, and you shouldn&#8217;t try. Export the table to S3 or stream changes out, then query with Athena or load a warehouse. Treat operational and analytical stores as separate systems.</p>



<h3 class="wp-block-heading">What about Aurora Serverless as the middle ground?</h3>



<p class="wp-block-paragraph">It solves idle cost, not connections or write scaling. If you&#8217;re considering DynamoDB because of Lambda concurrency or single-primary write limits, Aurora Serverless doesn&#8217;t address that. If it&#8217;s because you&#8217;re paying for an idle dev database, it does.</p>



<h3 class="wp-block-heading">How do I migrate from DynamoDB to RDS if I get it wrong?</h3>



<p class="wp-block-paragraph">Export to S3, reshape denormalized items into normalized tables, load, and dual-write during cutover. The data movement is routine. The hard part is that your application code assumed a key-value access shape everywhere, so the rewrite lands in the service layer, not the pipeline.</p>



<h3 class="wp-block-heading">Is DynamoDB cheaper than RDS for a small application?</h3>



<p class="wp-block-paragraph">Usually yes when traffic is low and bursty, because on-demand charges per request against an idle table while an instance bills continuously. The advantage narrows as traffic steadies and reverses once throughput justifies provisioned capacity or a reserved instance.</p>



<h3 class="wp-block-heading">Do I need RDS Proxy if I use Lambda with Postgres?</h3>



<p class="wp-block-paragraph">At meaningful concurrency, yes. Below that, a direct connection reused across invocations is fine, and the proxy adds cost plus a pinning failure mode. Add it when connection counts approach your limit, not preemptively in development.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">The one thing worth remembering about RDS vs DynamoDB: you&#8217;re not choosing a performance profile, you&#8217;re choosing when to pay the modeling cost. DynamoDB takes it up front and pays you back in predictability. A relational database lets you defer it and charges interest later in query complexity.</p>



<p class="wp-block-paragraph">Write the access pattern inventory. Be honest about the admin screens and the reporting nobody has asked for yet. If the list is short, stable and keyed on one identifier, DynamoDB will serve you for years. If it isn&#8217;t, take the relational database and spend the saved effort on indexes and connection handling.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Need a second opinion on the data store decision?</h2>



<p class="wp-block-paragraph">Cheap to review now, expensive to revisit later. Things I help with:</p>



<ul class="wp-block-list">
<li>Building the access pattern inventory with your team and pressure-testing it against the queries your roadmap implies</li>



<li>Reviewing a DynamoDB key and index design before it ships, including partition key distribution and index write amplification</li>



<li>Diagnosing throttling and hot partitions on an existing table, and whether the fix is a key change, a capacity change or a cache</li>



<li>Sizing RDS or Aurora for a serverless compute tier, including connection handling, RDS Proxy configuration and pinning</li>



<li>Modeling the cost of both options against your real request volume instead of a rule of thumb</li>



<li>Planning a migration in either direction, with a dual-write cutover and a rollback that works</li>
</ul>



<p class="wp-block-paragraph">Send me your access pattern list, a slow query plan, or your throttling metrics and I&#8217;ll tell you what I&#8217;d do with it.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://www.upwork.com/freelancers/~01f15a912ad84a6620" target="_blank" rel="noreferrer noopener">Work with me on Upwork</a></div>
</div>
<p>The post <a href="https://john-nessime.com/blog/technical-guides/rds-vs-dynamodb-access-patterns/">RDS vs DynamoDB: The Benchmark Isn&#8217;t the Decision</a> appeared first on <a href="https://john-nessime.com/blog">John Nessime</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
