{"id":18141,"date":"2026-07-13T13:12:51","date_gmt":"2026-07-13T06:12:51","guid":{"rendered":"https:\/\/www.cipher.co.th\/blogs\/cloudflare-storage-services-explained-2\/"},"modified":"2026-07-13T13:12:51","modified_gmt":"2026-07-13T06:12:51","slug":"cloudflare-storage-services-explained","status":"publish","type":"post","link":"https:\/\/www.cipher.co.th\/en\/blogs\/cloudflare-storage-services-explained\/","title":{"rendered":"Storage on Cloudflare: Choosing Between R2, D1, KV and Hyperdrive"},"content":{"rendered":"<p><strong>Quick answer:<\/strong> Use R2 for files and large objects, because egress is always zero. Use D1 when you want real SQL and the dataset per database fits inside its hard 10 GB ceiling. Use KV when the workload is overwhelmingly read-heavy, writes are rare, and eventual consistency is acceptable. Use Hyperdrive when you already have a PostgreSQL or MySQL database and do not want to migrate it, but understand before you switch it on that its cache does not invalidate on write.<\/p>\n<p>The most common and most expensive mistake is treating D1 as Postgres at the edge, and KV as Redis. Both analogies are wrong, and both lead to an architecture that has to be torn up later.<\/p>\n<p>This article has one job: help you pick the right store. Every section ends with a clear use-this-when and do-not-use-this-when. All limits and prices come from the official Cloudflare documentation as of July 2026.<\/p>\n<h2>R2: object storage with no egress fee<\/h2>\n<p>R2 is S3-compatible object storage. Its selling point is one line: egress is zero, always, with no exceptions.<\/p>\n<p>Storage is 0.015 USD per GB per month. The free tier includes 10 GB of storage plus 1 million Class A operations and 10 million Class B operations per month. Beyond that, Class A operations cost 4.50 USD per million and Class B cost 0.36 USD per million. For data you touch rarely, Infrequent Access storage is 0.01 USD per GB per month plus 0.01 USD per GB retrieved, with a 30-day minimum storage duration.<\/p>\n<p>A concrete example: 500 GB stored costs 7.50 USD per month. It does not matter whether your users download that data once or a million times, the egress bill is still zero. That is the reason teams with high download traffic migrate to R2, and we compared the two directly in <a href=\"https:\/\/www.cipher.co.th\/en\/blogs\/cloudflare-r2-vs-s3\/\">Cloudflare R2 vs Amazon S3<\/a>.<\/p>\n<p><strong>Use this when<\/strong> you are storing large files: images, video, user uploads, backups, or a data lake, and especially when that data is served out again.<\/p>\n<p><strong>Do not use this when<\/strong> you need to query or filter the data. R2 is not a database. Fetching an entire object just to read one field inside it is the wrong pattern.<\/p>\n<h2>D1: SQL with a hard 10 GB ceiling<\/h2>\n<p>D1 is serverless SQLite. You write real SQL, you get indexes and transactions, and you call it directly from a Worker.<\/p>\n<p>The limit that must shape your design from day one: a single database holds a maximum of 10 GB, and it cannot be increased. This is not a soft limit you can ask to have raised. It is the ceiling. But this is the part people get backwards. D1 was never designed to be the single monolithic database for your whole system. It was designed to be sharded per tenant or per user. Creating many databases is intentional, and it is how the product is meant to be used.<\/p>\n<h3>Billing is on rows, not queries<\/h3>\n<p>D1 bills rows read and rows written. On Workers Paid, the first 25 billion rows read per month are included, then 0.001 USD per million rows. The first 50 million rows written per month are included, then 1.00 USD per million rows. Storage is 5 GB included, then 0.75 USD per GB-month. There is no egress charge.<\/p>\n<p>The detail that inflates bills is that rows read counts the rows a query scans, not the rows it returns. If a table has 5,000 rows and you run a SELECT filtering on an unindexed column, even if it returns a single row, you are billed for 5,000 rows read. Adding an index is therefore not just a performance decision; it is directly a cost decision. Note also that writing to a column covered by an index adds an extra row written, since the index is updated too, but the reduction in rows read almost always more than pays for it.<\/p>\n<p><strong>Use this when<\/strong> you need real SQL and the data per database is well under 10 GB. It is an excellent fit for multi-tenant systems that shard a database per customer, and for apps where each user has a bounded set of data.<\/p>\n<p><strong>Do not use this when<\/strong> you have one large monolithic dataset over 10 GB, you need complex JOINs across many big tables, or you rely on PostgreSQL features such as PostGIS or advanced full-text search. We covered the trade-offs in <a href=\"https:\/\/www.cipher.co.th\/en\/blogs\/cloudflare-d1-vs-postgresql\/\">Cloudflare D1 vs PostgreSQL<\/a>.<\/p>\n<h2>KV: read a lot, write a little, tolerate staleness<\/h2>\n<p>KV is a key-value store built for reads that are fast from everywhere, and it buys that speed by being eventually consistent.<\/p>\n<p>The pricing tells you exactly what it is for. On Workers Paid, reads include 10 million per month and cost only 0.50 USD per million beyond that. But writes, deletes and list operations each include just 1 million per month and cost 5.00 USD per million beyond that: ten times the price of a read. Storage is 1 GB included, then 0.50 USD per GB-month.<\/p>\n<p>The limits that bite: you can only write to the same key once per second, on both Free and Paid. Keys are capped at 512 bytes, key metadata at 1024 bytes, and values at 25 MiB. The minimum cacheTtl is 30 seconds. A single Worker invocation can make at most 1,000 operations to external services.<\/p>\n<p>One thing people miss: every operation is billable, including a read for a key that does not exist and returns null. Those still traverse the KV infrastructure.<\/p>\n<p><strong>Use this when<\/strong> the workload is read-heavy, writes are rare, and staleness is fine: feature flags, configuration, routing tables, or caching an API response that does not change often.<\/p>\n<p><strong>Do not use this when<\/strong> you need a value written and read back immediately, you need an atomic increment, or you need to write the same key more than once a second. That is Durable Objects territory, not KV.<\/p>\n<h2>Hyperdrive: keep Postgres where it is<\/h2>\n<p>Hyperdrive is not a database. It is connection pooling plus query caching that lets a Worker use the PostgreSQL or MySQL database you already have, without migrating it. It is free on Workers Paid.<\/p>\n<h3>The trap you must understand before enabling it<\/h3>\n<p><strong>Hyperdrive does not invalidate its cache on write.<\/strong> The default max_age is 60 seconds and stale_while_revalidate is 15 seconds. This means that if you point a default-cached configuration at a sessions or permissions table, you have created a 60-second correctness hole. A user whose access you just revoked can still reach data for up to another minute.<\/p>\n<p>The correct approach is to cache only data that changes slowly, such as a product catalogue or a reference table, and to disable caching for anything where correctness matters more than speed. We covered the configuration in detail in <a href=\"https:\/\/www.cipher.co.th\/en\/blogs\/cloudflare-hyperdrive-postgres\/\">Cloudflare Hyperdrive and PostgreSQL<\/a>.<\/p>\n<p><strong>Use this when<\/strong> you have an existing Postgres or MySQL database, you do not want to migrate, and you need Workers to reach it without opening a new connection on every request.<\/p>\n<p><strong>Do not use this when<\/strong> you are building a brand new system with no existing database, or when you would enable caching without understanding which data can safely be up to 60 seconds stale.<\/p>\n<h2>Comparison (as of July 2026)<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Service<\/th>\n<th>Type<\/th>\n<th>Consistency<\/th>\n<th>Key limit<\/th>\n<th>Headline price<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>R2<\/td>\n<td>Object storage<\/td>\n<td>Strong per object<\/td>\n<td>No storage size cap<\/td>\n<td>0.015 USD\/GB\/month, egress 0 USD<\/td>\n<\/tr>\n<tr>\n<td>D1<\/td>\n<td>SQL (SQLite)<\/td>\n<td>Strong<\/td>\n<td>10 GB per database, cannot be increased<\/td>\n<td>0.001 USD per million rows read, 1.00 USD per million rows written<\/td>\n<\/tr>\n<tr>\n<td>KV<\/td>\n<td>Key-value<\/td>\n<td>Eventual<\/td>\n<td>Value 25 MiB, 1 write per second to the same key<\/td>\n<td>Reads 0.50 USD\/million, writes 5.00 USD\/million<\/td>\n<\/tr>\n<tr>\n<td>Hyperdrive<\/td>\n<td>Proxy in front of your own database<\/td>\n<td>Depends on the database; the cache does not invalidate on write<\/td>\n<td>max_age 60 s by default<\/td>\n<td>Free on Workers Paid<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>Decision matrix<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>If you need<\/th>\n<th>Use<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>To store user uploads, images or video<\/td>\n<td>R2<\/td>\n<\/tr>\n<tr>\n<td>To serve a lot of data out (avoiding egress fees)<\/td>\n<td>R2<\/td>\n<\/tr>\n<tr>\n<td>To keep backups or files touched rarely<\/td>\n<td>R2 Infrequent Access<\/td>\n<\/tr>\n<tr>\n<td>SQL per tenant, with data under 10 GB each<\/td>\n<td>D1 (one database per tenant)<\/td>\n<\/tr>\n<tr>\n<td>One relational dataset larger than 10 GB<\/td>\n<td>PostgreSQL via Hyperdrive<\/td>\n<\/tr>\n<tr>\n<td>Feature flags, config, routing tables<\/td>\n<td>KV<\/td>\n<\/tr>\n<tr>\n<td>To cache an API response that changes rarely<\/td>\n<td>KV<\/td>\n<\/tr>\n<tr>\n<td>A counter that increments atomically and must not miss<\/td>\n<td>Durable Objects (not KV)<\/td>\n<\/tr>\n<tr>\n<td>Sessions or permissions that must be up to date instantly<\/td>\n<td>Postgres via Hyperdrive, with caching disabled<\/td>\n<\/tr>\n<tr>\n<td>To use an existing Postgres without migrating it<\/td>\n<td>Hyperdrive<\/td>\n<\/tr>\n<tr>\n<td>Vectors for semantic search<\/td>\n<td>Vectorize<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>A worked example: a multi-tenant SaaS<\/h2>\n<p>Suppose you are building a SaaS for clinics: 300 customers, each with their own patient records. Here is how the data lands.<\/p>\n<p><strong>Attachments, scans and uploaded documents<\/strong> go to R2. This data is read far more often than it is written, and the zero egress fee makes the cost predictable. 500 GB costs 7.50 USD per month whether users download it once or constantly.<\/p>\n<p><strong>The relational records for each clinic<\/strong> go to D1, one database per clinic. The 10 GB ceiling is an advantage here rather than a constraint: it forces you to isolate customer data from customer data, which a large SaaS should be doing anyway on blast-radius grounds. One clinic can never accidentally read another clinic&#8217;s rows, because a query cannot cross the database boundary.<\/p>\n<p><strong>Feature flags and per-clinic configuration<\/strong> go to KV. Read on every request, written occasionally. This is precisely the profile KV is priced for, and a few seconds of propagation delay does no harm.<\/p>\n<p><strong>The customer master table, billing and invoicing<\/strong> stay in the existing PostgreSQL, reached through Hyperdrive. No migration, no rewrite.<\/p>\n<p>The one thing to be careful about is the sessions and permissions tables. Do not point a cached Hyperdrive configuration at them. A max_age of 60 seconds means a revoked permission can still be honoured for up to a minute. Split the configuration: one Hyperdrive config with caching enabled for slow-moving data, and a second with caching disabled for anything that must be correct right now.<\/p>\n<h2>When another option wins<\/h2>\n<p>Cloudflare does not win every case, and this is where it loses.<\/p>\n<p>If your primary dataset exceeds 10 GB and cannot be split per tenant, D1 simply stops at that point. There is no way to route around it. Use PostgreSQL or MySQL properly and let Hyperdrive be the bridge.<\/p>\n<p>If you need advanced PostgreSQL features, PostGIS for geospatial work, complex window functions, or serious full-text search, D1 will make you miserable. SQLite is not Postgres, and pretending otherwise wastes weeks.<\/p>\n<p>If your workload is write-heavy on the same key, more than once per second, KV is not the answer, and 5.00 USD per million writes gets expensive quickly if you use it as a general-purpose store. Consider Durable Objects with SQLite storage instead, where the row-based rates match D1.<\/p>\n<h2>Summary<\/h2>\n<p>Picking a store on Cloudflare comes down to three questions. Is this data a file or is it rows in a table? Does it have to be read back immediately after it is written, or can it be stale for a while? And do you already have a database you do not want to move? Answer those three and the choice is usually obvious.<\/p>\n<p>Cipher designs and implements systems on the Cloudflare Developer Platform for businesses in Thailand, and can assess an existing storage architecture to say which data belongs where.<\/p>\n<h2>Frequently Asked Questions<\/h2>\n<h3>What is the maximum size of a D1 database?<\/h3>\n<p>A single D1 database holds a maximum of 10 GB, and this cannot be increased. It is not a soft limit you can request more of; it is a design constraint. D1 is meant to be sharded per tenant or per user, so the answer is to create many databases rather than one large one.<\/p>\n<h3>Does R2 charge for egress?<\/h3>\n<p>No. R2 egress is zero, always. You pay only for storage at 0.015 USD per GB per month and for operations, at 4.50 USD per million Class A operations and 0.36 USD per million Class B operations. Pricing as of July 2026.<\/p>\n<h3>What is the difference between KV and D1?<\/h3>\n<p>KV is an eventually consistent key-value store built for many reads and few writes. D1 is real SQL, strongly consistent, with indexes and transactions. If you need to query the data, use D1. If you only need to look it up by key, use KV.<\/p>\n<h3>Does Hyperdrive invalidate its cache on write?<\/h3>\n<p>No. Hyperdrive does not invalidate its cache on write. The default max_age is 60 seconds and stale_while_revalidate is 15 seconds. Pointing a default-cached configuration at a sessions or permissions table therefore creates a 60-second correctness hole.<\/p>\n<h3>Should I use D1 or PostgreSQL?<\/h3>\n<p>Use D1 when the data per database is comfortably under 10 GB and you can shard it per tenant, which is the shape D1 was designed for. Use PostgreSQL when you have one relational dataset larger than 10 GB, or when you need advanced features such as PostGIS or full-text search, and let Hyperdrive be the bridge from your Workers.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Quick answer: Use R2 for files and large objects, because egress is always zero. Use D1 when you want real [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_angie_page":false,"content-type":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"page_builder":"","footnotes":""},"categories":[152],"tags":[],"class_list":["post-18141","post","type-post","status-publish","format-standard","hentry","category-technology-en"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.cipher.co.th\/en\/wp-json\/wp\/v2\/posts\/18141","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cipher.co.th\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cipher.co.th\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cipher.co.th\/en\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cipher.co.th\/en\/wp-json\/wp\/v2\/comments?post=18141"}],"version-history":[{"count":0,"href":"https:\/\/www.cipher.co.th\/en\/wp-json\/wp\/v2\/posts\/18141\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.cipher.co.th\/en\/wp-json\/wp\/v2\/media?parent=18141"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cipher.co.th\/en\/wp-json\/wp\/v2\/categories?post=18141"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cipher.co.th\/en\/wp-json\/wp\/v2\/tags?post=18141"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}