
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Sun, 05 Apr 2026 17:24:05 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Announcing support for GROUP BY, SUM, and other aggregation queries in R2 SQL]]></title>
            <link>https://blog.cloudflare.com/r2-sql-aggregations/</link>
            <pubDate>Thu, 18 Dec 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ Cloudflare’s R2 SQL, a distributed query engine, now supports aggregations. Explore how we built distributed GROUP BY execution, using scatter-gather and shuffling strategies to run analytics directly over your R2 Data Catalog. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>When you’re dealing with large amounts of data, it’s helpful to get a quick overview — which is exactly what aggregations provide in SQL. Aggregations, known as “GROUP BY queries”, provide a bird’s eye view, so you can quickly gain insights from vast volumes of data.</p><p>That’s why we are excited to announce support for aggregations in <a href="https://blog.cloudflare.com/r2-sql-deep-dive/"><u>R2 SQL</u></a>, Cloudflare's serverless, distributed, analytics query engine, which is capable of running SQL queries over data stored in <a href="https://developers.cloudflare.com/r2/data-catalog/"><u>R2 Data Catalog</u></a>. Aggregations will allow users of <a href="https://developers.cloudflare.com/r2-sql/"><u>R2 SQL</u></a> to spot important trends and changes in the data, generate reports and find anomalies in logs.</p><p>This release builds on the already supported filter queries, which are foundational for analytical workloads, and allow users to find needles in haystacks of <a href="https://parquet.apache.org/"><u>Apache Parquet</u></a> files.</p><p>In this post, we’ll unpack the utility and quirks of aggregations, and then dive into how we extended R2 SQL to support running such queries over vast amounts of data stored in R2 Data Catalog.</p>
    <div>
      <h2>The importance of aggregations in analytics</h2>
      <a href="#the-importance-of-aggregations-in-analytics">
        
      </a>
    </div>
    <p>Aggregations, or “GROUP BY queries”, generate a short summary of the underlying data.</p><p>A common use case for aggregations is generating reports. Consider a table called “sales”, which contains historical data of all sales across various countries and departments of some organisation. One could easily generate a report on the volume of sales by department using this aggregation query:</p>
            <pre><code>SELECT department, sum(value)
FROM sales
GROUP BY department</code></pre>
            <p>
The “GROUP BY” statement allows us to split table rows into buckets. Each bucket has a label corresponding to a particular department. Once the buckets are full, we can then calculate “sum(value)” for all rows in each bucket, giving us the total volume of sales performed by the corresponding department.</p><p>For some reports, we might only be interested in departments that had the largest volume. That’s where an “ORDER BY” statement comes in handy:</p>
            <pre><code>SELECT department, sum(value)
FROM sales
GROUP BY department
ORDER BY sum(value) DESC
LIMIT 10</code></pre>
            <p>Here we instruct the query engine to sort all department buckets by their total sales volume in the descending order and only return the top 10 largest.</p><p>Finally, we might be interested in filtering out anomalies. For example, we might want to only include departments that had more than five sales total in our report. We can easily do that with a “HAVING” statement:</p>
            <pre><code>SELECT department, sum(value), count(*)
FROM sales
GROUP BY department
HAVING count(*) &gt; 5
ORDER BY sum(value) DESC
LIMIT 10</code></pre>
            <p>Here we added a new aggregate function to our query — “count(*)” — which calculates how many rows ended up in each bucket. This directly corresponds to the number of sales in each department, so we have also added a predicate in the “HAVING” statement to make sure that we only leave buckets with more than five rows in them.</p>
    <div>
      <h2>Two approaches to aggregation: compute sooner or later</h2>
      <a href="#two-approaches-to-aggregation-compute-sooner-or-later">
        
      </a>
    </div>
    <p>Aggregation queries have a curious property: they can reference columns that are not stored anywhere. Consider “sum(value)”: this column is computed by the query engine on the fly, unlike the “department” column, which is fetched from Parquet files stored on R2. This subtle difference means that any query that references aggregates like “sum”, “count” and others needs to be split into two phases.</p><p>The first phase is computing new columns. If we are to sort the data by “count(*)” column using “ORDER BY” statement or filter rows based on it using “HAVING” statement, we need to know the values of this column. Once the values of columns like “count(*)” are known, we can proceed with the rest of the query execution.</p><p>Note that if the query does not reference aggregate functions in “HAVING” or “ORDER BY”, but still uses them in “SELECT”, we can make use of a trick. Since we do not need the values of aggregate functions until the very end, we can compute them partially and merge results just before we are about to return them to the user.</p><p>The key difference between the two approaches is when we compute aggregate functions: in advance, to perform some additional computations on them later; or on the fly, to iteratively build results the user needs.</p><p>First, we will dive into building results on the fly — a technique we call “scatter-gather aggregations.” We will then build on top of that to introduce “shuffling aggregations” capable of running extra computations like “HAVING” and “ORDER BY” on top of aggregate functions.</p>
    <div>
      <h2>Scatter-gather aggregations</h2>
      <a href="#scatter-gather-aggregations">
        
      </a>
    </div>
    <p>Aggregate queries without “HAVING” and “ORDER BY” can be executed in a fashion similar to filter queries. For filter queries, R2 SQL picks one node to be the coordinator in query execution. This node analyzes the query and consults R2 Data Catalog to figure out which Parquet row groups may contain data relevant to the query. Each Parquet row group represents a relatively small piece of work that a single compute node can handle. Coordinator node distributes the work across many worker nodes and collects results to return them to the user.</p><p>In order to execute aggregate queries, we follow all the same steps and distribute small pieces of work between worker nodes. However, this time instead of just filtering rows based on the predicate in the “WHERE” statement, worker nodes also compute <b>pre-aggregates</b>.</p><p>Pre-aggregates represent an intermediary state of an aggregation. This is an incomplete piece of data representing a partially computed aggregate function on a subset of data. Multiple pre-aggregates can be merged together to compute the final value of an aggregate function. Splitting aggregate functions into pre-aggregates allows us to horizontally scale computation of aggregation, making use of vast compute resources available in Cloudflare’s network.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2Vh0x4qHkjOuQTrxSzkVKx/84c05ebf590cb4949b188f5856a4e951/image2.png" />
          </figure><p>For example, pre-aggregate for “count(*)” is simply a number representing the count of rows in a subset of data. Computing the final “count(*)” is as easy as adding these numbers together. Pre-aggregate for “avg(value)” consists of two numbers: “sum(value)” and “count(*)”. The value of “avg(value)” can then be computed by adding together all “sum(value)” values, adding together all “count(*)” values and finally dividing one number by the other.</p><p>Once worker nodes have finished computing the pre-aggregates, they stream results to the coordinator node. The coordinator node collects all results, computes final values of aggregate functions from pre-aggregates, and returns the result to the user.</p>
    <div>
      <h2>Shuffling, beyond the limits of scatter-gather</h2>
      <a href="#shuffling-beyond-the-limits-of-scatter-gather">
        
      </a>
    </div>
    <p>Scatter-gather is highly efficient when the coordinator can compute the final result by merging small, partial states from workers. If you run a query like <code>SELECT sum(sales) FROM orders</code>, the coordinator receives a single number from each worker and adds them up. The memory footprint on the coordinator is negligible regardless of how much data resides in R2.</p><p>However, this approach becomes inefficient when the query requires sorting or filtering based on the <i>result</i> of an aggregation. Consider this query, which finds the top two departments by sales volume:</p>
            <pre><code>SELECT department, sum(sales)
FROM sales
GROUP BY department
ORDER BY sum(sales) DESC
LIMIT 2</code></pre>
            <p>Correctly determining the global Top 2 requires knowing the total sales for every department across the entire dataset. Because the data is spread effectively at random across the underlying Parquet files, sales for a specific department are likely split across many different workers. A department might have low sales on every individual worker, excluding it from any local Top 2 list, yet have the highest sales volume globally when summed together.</p><p>The diagram below illustrates how a scatter-gather approach would not work for this query. "Dept A" is the global sales leader, but because its sales are evenly spread across workers, it doesn’t make to some local Top 2 lists, and ends up being discarded by the coordinator.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3ZJ6AfXzepKtJhiL6DcjiJ/07f4f523d871b25dcf444ee2ada546bd/image4.png" />
          </figure><p>Consequently, when the query orders results by their global aggregation, the coordinator cannot rely on pre-filtered results from workers. It must request the total count for <i>every</i> department from <i>every</i> worker to calculate the global totals before it can sort them. If you are grouping by a high-cardinality column like IP addresses or User IDs, this forces the coordinator to ingest and merge millions of rows, creating a resource bottleneck on a single node.</p><p>To solve this, we need <b>shuffling</b>, a way to colocate data for specific groups before the final aggregation occurs.</p>
    <div>
      <h3>Shuffling of aggregation data</h3>
      <a href="#shuffling-of-aggregation-data">
        
      </a>
    </div>
    <p>To address the challenges of random data distribution, we introduce a <b>shuffling stage</b>. Instead of sending results to the coordinator, workers exchange data directly with each other to colocate rows based on their grouping key.</p><p>This routing relies on <b>deterministic hash partitioning</b>. When a worker processes a row, it hashes the <code>GROUP BY</code> column to identify the destination worker. Because this hash is deterministic, every worker in the cluster independently agrees on where to send specific data. If "Engineering" hashes to Worker 5, every worker knows to route "Engineering" rows to Worker 5. No central registry is required.</p><p>The diagram below illustrates this flow. Notice how "Dept A" starts on Workers 1, 2 and 3. Because the hash function maps "Dept A" to Worker 1, all workers route those rows to that same destination.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3Mw7FvL7ZJgDZqnh3ygkZM/9cfb493b5889d7efe43e4719d9523c93/image1.png" />
          </figure><p>Shuffling aggregates produces the correct results. However, this all-to-all exchange creates a timing dependency. If Worker 1 begins calculating the final total for "Dept A" before Worker 3 has finished sending its share of the data, the result will be incomplete.</p><p>To address this, we enforce a strict <b>synchronization barrier</b>. The coordinator tracks the progress of the entire cluster while workers buffer their outgoing data and flush it via <a href="https://grpc.io/"><u>gRPC</u></a> streams to their peers. Only when every worker confirms that it has finished processing its input files and flushing its shuffle buffers does the coordinator issue the command to proceed. This barrier guarantees that when the next stage begins, the dataset on each worker is complete and accurate.</p>
    <div>
      <h3>Local finalization</h3>
      <a href="#local-finalization">
        
      </a>
    </div>
    <p>Once the synchronization barrier is lifted, every worker holds the complete dataset for its assigned groups. Worker 1 now has 100% of the sales records for "Dept A" and can calculate the final total with certainty.</p><p>This allows us to push computational logic like filtering and sorting down to the worker rather than burdening the coordinator. For example, if the query includes <code>HAVING count(*) &gt; 5</code>, the worker can filter out groups that do not meet this criteria immediately after aggregation.</p><p>At the end of this stage, each worker produces a sorted, finalized stream of results for the groups it owns.</p>
    <div>
      <h3>The streaming merge</h3>
      <a href="#the-streaming-merge">
        
      </a>
    </div>
    <p>The final piece of the puzzle is the coordinator. In the scatter-gather model, the coordinator was responsible for the expensive task of aggregating and sorting the entire dataset. In the shuffling model, its role changes.</p><p>Because the workers have already computed the final aggregates and sorted them locally, the coordinator only needs to perform a <b>k-way merge</b>. It opens a stream to every worker and reads the results row by row. It compares the current row from each worker, picks the "winner" based on the sort order, and adds it to the query results that will be sent to the user.</p><p>This approach is particularly powerful for <code>LIMIT</code> queries. If a user asks for the top 10 departments, the coordinator merges the streams until it has found the top 10 items and then immediately stops processing. It does not need to load or merge the millions of remaining rows, allowing for greater scale of operation without over-consumption of compute resources.</p>
    <div>
      <h2>A powerful engine for processing massive datasets</h2>
      <a href="#a-powerful-engine-for-processing-massive-datasets">
        
      </a>
    </div>
    <p>With the addition of aggregations, <a href="https://developers.cloudflare.com/r2-sql/?cf_target_id=84F4CFDF79EFE12291D34EF36907F300"><u>R2 SQL</u></a> transforms from a tool great for filtering data into a powerful engine capable of data processing on massive datasets. This is made possible by implementing distributed execution strategies like scatter-gather and shuffling, where we are able to push the compute to where the data lives, using the scale of Cloudflare’s global compute and network. </p><p>Whether you are generating reports, monitoring high-volume logs for anomalies, or simply trying to spot trends in your data, you can now easily do it all within Cloudflare’s Developer Platform without the overhead of managing complex OLAP infrastructure or moving data out of R2.</p>
    <div>
      <h2>Try it now</h2>
      <a href="#try-it-now">
        
      </a>
    </div>
    <p>Support for aggregations in R2 SQL is available today. We are excited to see how you use these new functions with data in R2 Data Catalog.</p><ul><li><p><b>Get Started:</b> Check out our <a href="https://developers.cloudflare.com/r2-sql/sql-reference/"><u>documentation</u></a> for examples and syntax guides on running aggregation queries.</p></li><li><p><b>Join the Conversation:</b> If you have questions, feedback, or want to share what you’re building, join us in the Cloudflare <a href="https://discord.com/invite/cloudflaredev"><u>Developer Discord</u></a>.</p></li></ul><p></p> ]]></content:encoded>
            <category><![CDATA[R2]]></category>
            <category><![CDATA[Data]]></category>
            <category><![CDATA[Edge Computing]]></category>
            <category><![CDATA[Rust]]></category>
            <category><![CDATA[Serverless]]></category>
            <category><![CDATA[SQL]]></category>
            <guid isPermaLink="false">1qWQCp4QfhsZAs27s7fEc0</guid>
            <dc:creator>Jérôme Schneider</dc:creator>
            <dc:creator>Nikita Lapkov</dc:creator>
            <dc:creator>Marc Selwan</dc:creator>
        </item>
        <item>
            <title><![CDATA[R2 SQL: a deep dive into our new distributed query engine]]></title>
            <link>https://blog.cloudflare.com/r2-sql-deep-dive/</link>
            <pubDate>Thu, 25 Sep 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ R2 SQL provides a built-in, serverless way to run ad-hoc analytic queries against your R2 Data Catalog. This post dives deep under the Iceberg into how we built this distributed engine. ]]></description>
            <content:encoded><![CDATA[ <p>How do you run SQL queries over petabytes of data… without a server?</p><p>We have an answer for that: <a href="https://developers.cloudflare.com/r2-sql/"><u>R2 SQL</u></a>, a serverless query engine that can sift through enormous datasets and return results in seconds.</p><p>This post details the architecture and techniques that make this possible. We'll walk through our Query Planner, which uses <a href="https://developers.cloudflare.com/r2/data-catalog/"><u>R2 Data Catalog</u></a> to prune terabytes of data before reading a single byte, and explain how we distribute the work across Cloudflare’s <a href="https://www.cloudflare.com/network"><u>global network</u></a>, <a href="https://developers.cloudflare.com/workers/"><u>Workers</u></a> and <a href="https://www.cloudflare.com/developer-platform/products/r2/"><u>R2</u></a> for massively parallel execution.</p>
    <div>
      <h3>From catalog to query</h3>
      <a href="#from-catalog-to-query">
        
      </a>
    </div>
    <p>During Developer Week 2025, we <a href="https://blog.cloudflare.com/r2-data-catalog-public-beta/"><u>launched</u></a> R2 Data Catalog, a managed <a href="https://iceberg.apache.org/"><u>Apache Iceberg</u></a> catalog built directly into your Cloudflare R2 bucket. Iceberg is an open table format that provides critical database features like transactions and schema evolution for petabyte-scale <a href="https://www.cloudflare.com/learning/cloud/what-is-object-storage/">object storage</a>. It gives you a reliable catalog of your data, but it doesn’t provide a way to query it.</p><p>Until now, reading your R2 Data Catalog required setting up a separate service like <a href="https://spark.apache.org/"><u>Apache Spark</u></a> or <a href="https://trino.io/"><u>Trino</u></a>. Operating these engines at scale is not easy: you need to provision clusters, manage resource usage, and be responsible for their availability, none of which contributes to the primary goal of getting value from your data.</p><p><a href="https://developers.cloudflare.com/r2-sql/"><u>R2 SQL</u></a> removes that step entirely. It’s a serverless query engine that executes retrieval SQL queries against your Iceberg tables, right where your data lives.</p>
    <div>
      <h3>Designing a query engine for petabytes</h3>
      <a href="#designing-a-query-engine-for-petabytes">
        
      </a>
    </div>
    <p>Object storage is fundamentally different from a traditional database’s storage. A database is structured by design; R2 is an ocean of objects, where a single logical table can be composed of potentially millions of individual files, large and small, with more arriving every second.</p><p>Apache Iceberg provides a powerful layer of logical organization on top of this reality. It works by managing the table's state as an immutable series of snapshots, creating a reliable, structured view of the table by manipulating lightweight metadata files instead of rewriting the data files themselves.</p><p>However, this logical structure doesn't change the underlying physical challenge: an efficient query engine must still find the specific data it needs within that vast collection of files, and this requires overcoming two major technical hurdles:</p><p><b>The I/O problem</b>: A core challenge for query efficiency is minimizing the amount of data read from storage. A brute-force approach of reading every object is simply not viable. The primary goal is to read only the data that is absolutely necessary.</p><p><b>The Compute problem</b>: The amount of data that does need to be read can still be enormous. We need a way to give the right amount of compute power to a query, which might be massive, for just a few seconds, and then scale it down to zero instantly to avoid waste.</p><p>Our architecture for R2 SQL is designed to solve these two problems with a two-phase approach: a <b>Query Planner</b> that uses metadata to intelligently prune the search space, and a <b>Query Execution</b> system that distributes the work across Cloudflare's global network to process the data in parallel.</p>
    <div>
      <h2>Query Planner</h2>
      <a href="#query-planner">
        
      </a>
    </div>
    <p>The most efficient way to process data is to avoid reading it in the first place. This is the core strategy of the R2 SQL Query Planner. Instead of exhaustively scanning every file, the planner makes use of the metadata structure provided by R2 Data Catalog to prune the search space, that is, to avoid reading huge swathes of data irrelevant to a query.</p><p>This is a top-down investigation where the planner navigates the hierarchy of Iceberg metadata layers, using <b>stats</b> at each level to build a fast plan, specifying exactly which byte ranges the query engine needs to read.</p>
    <div>
      <h3>What do we mean by “stats”?</h3>
      <a href="#what-do-we-mean-by-stats">
        
      </a>
    </div>
    <p>When we say the planner uses "stats" we are referring to summary metadata that Iceberg stores about the contents of the data files. These statistics create a coarse map of the data, allowing the planner to make decisions about which files to read, and which to ignore, without opening them.</p><p>There are two primary levels of statistics the planner uses for pruning:</p><p><b>Partition-level stats</b>: Stored in the Iceberg manifest list, these stats describe the range of partition values for all the data in a given Iceberg manifest file. For a partition on <code>day(event_timestamp)</code>, this would be the earliest and latest day present in the files tracked by that manifest.</p><p><b>Column-level stats</b>: Stored in the manifest files, these are more granular stats about each individual data file. Data files in R2 Data Catalog are formatted using the <a href="https://parquet.apache.org/"><u>Apache Parquet</u></a>. For every column of a Parquet file, the manifest stores key information like:</p><ul><li><p>The minimum and maximum values. If a query asks for <code>http_status = 500</code>, and a file’s stats show its <code>http_status</code> column has a min of 200 and a max of 404, that entire file can be skipped.</p></li><li><p>A count of null values. This allows the planner to skip files when a query specifically looks for non-null values (e.g.,<code> WHERE error_code IS NOT NULL</code>) and the file's metadata reports that all values for <code>error_code</code> are null.</p></li></ul><p>Now, let's see how the planner uses these stats as it walks through the metadata layers.</p>
    <div>
      <h3>Pruning the search space</h3>
      <a href="#pruning-the-search-space">
        
      </a>
    </div>
    <p>The pruning process is a top-down investigation that happens in three main steps:</p><ol><li><p><b>Table metadata and the current snapshot</b></p></li></ol><p>The planner begins by asking the catalog for the location of the current table metadata. This is a JSON file containing the table's current schema, partition specs, and a log of all historical snapshots. The planner then fetches the latest snapshot to work with.</p><p>2. <b>Manifest list and partition pruning</b></p><p>The current snapshot points to a single Iceberg manifest list. The planner reads this file and uses the partition-level stats for each entry to perform the first, most powerful pruning step, discarding any manifests whose partition value ranges don't satisfy the query. For a table partitioned by <code>day(event_timestamp</code>), the planner can use the min/max values in the manifest list to immediately discard any manifests that don't contain data for the days relevant to the query.</p><p>3.<b> Manifests and file-level pruning</b></p><p>For the remaining manifests, the planner reads each one to get a list of the actual Parquet data files. These manifest files contain more granular, column-level stats for each individual data file they track. This allows for a second pruning step, discarding entire data files that cannot possibly contain rows matching the query's filters.</p><p>4. <b>File row-group pruning</b></p><p>Finally, for the specific data files that are still candidates, the Query Planner uses statistics stored inside Parquet file's footers to skip over entire row groups.</p><p>The result of this multi-layer pruning is a precise list of Parquet files, and of row groups within those Parquet files. These become the query work units that are dispatched to the Query Execution system for processing.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7GKvgbex2vhIBqQ1G5UFjQ/2a99db7ae786b8e22a326bac0c9037d9/1.png" />
          </figure>
    <div>
      <h3>The Planning pipeline</h3>
      <a href="#the-planning-pipeline">
        
      </a>
    </div>
    <p>In R2 SQL, the multi-layer pruning we've described so far isn't a monolithic process. For a table with millions of files, the metadata can be too large to process before starting any real work. Waiting for a complete plan would introduce significant latency.</p><p>Instead, R2 SQL treats planning and execution together as a concurrent pipeline. The planner's job is to produce a stream of work units for the executor to consume as soon as they are available.</p><p>The planner’s investigation begins with two fetches to get a map of the table's structure: one for the table’s snapshot and another for the manifest list.</p>
    <div>
      <h4>Starting execution as early as possible</h4>
      <a href="#starting-execution-as-early-as-possible">
        
      </a>
    </div>
    <p>From that point on, the query is processed in a streaming fashion. As the Query Planner reads through the manifest files and subsequently the data files they point to and prunes them, it immediately emits any matching data files/row groups as work units to the execution queue.</p><p>This pipeline structure ensures the compute nodes can begin the expensive work of data I/O almost instantly, long before the planner has finished its full investigation.</p><p>On top of this pipeline model, the planner adds a crucial optimization: <b>deliberate ordering</b>. The manifest files are not streamed in an arbitrary sequence. Instead, the planner processes them in an order matching by the query's <code>ORDER BY</code> clause, guided by the metadata stats. This ensures that the data most likely to contain the desired results is processed first.</p><p>These two concepts work together to address query latency from both ends of the query pipeline.</p><p>The streamed planning pipeline lets us start crunching data as soon as possible, minimizing the delay before the first byte is processed. At the other end of the pipeline, the deliberate ordering of that work lets us finish early by finding a definitive result without scanning the entire dataset.</p><p>The next section explains the mechanics behind this "finish early" strategy.</p>
    <div>
      <h4>Stopping early: how to finish without reading everything</h4>
      <a href="#stopping-early-how-to-finish-without-reading-everything">
        
      </a>
    </div>
    <p>Thanks to the Query Planner streaming work units in an order matching the <code>ORDER BY </code>clause, the Query Execution system first processes the data that is most likely to be in the final result set.</p><p>This prioritization happens at two levels of the metadata hierarchy:</p><p><b>Manifest ordering</b>: The planner first inspects the manifest list. Using the partition stats for each manifest (e.g., the latest timestamp in that group of files), it decides which entire manifest files to stream first.</p><p><b>Parquet file ordering</b>: As it reads each manifest, it then uses the more granular column-level stats to decide the processing order of the individual Parquet files within that manifest.</p><p>This ensures a constantly prioritized stream of work units is sent to the execution engine. This prioritized stream is what allows us to stop the query early.</p><p>For instance, with a query like ... <code>ORDER BY timestamp DESC LIMIT 5</code>, as the execution engine processes work units and sends back results, the planner does two things concurrently:</p><p>It maintains a bounded heap of the best 5 results seen so far, constantly comparing new results to the oldest timestamp in the heap.</p><p>It keeps a "high-water mark" on the stream itself. Thanks to the metadata, it always knows the absolute latest timestamp of any data file that has not yet been processed.</p><p>The planner is constantly comparing the state of the heap to the water mark of the remaining stream. The moment the oldest timestamp in our Top 5 heap is newer than the high-water mark of the remaining stream, the entire query can be stopped.</p><p>At that point, we can prove no remaining work unit could possibly contain a result that would make it into the top 5. The pipeline is halted, and a complete, correct result is returned to the user, often after reading only a fraction of the potentially matching data.</p><p>Currently, R2 SQL supports ordering on columns that are part of the table's partition key only. This is a limitation we are working on lifting in the future.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5qN9TeEuRZJIidYXFictG/8a55cc6088be3abdc3b27878daa76e40/image4.png" />
          </figure>
    <div>
      <h3>Architecture</h3>
      <a href="#architecture">
        
      </a>
    </div>
    
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3wkvnT24y5E0k5064cqu0T/939402d16583647986eec87617379900/image3.png" />
          </figure>
    <div>
      <h2>Query Execution</h2>
      <a href="#query-execution">
        
      </a>
    </div>
    <p>Query Planner streams the query work in bite-sized pieces called row groups. A single Parquet file usually contains multiple row groups, but most of the time only a few of them contain relevant data. Splitting query work into row groups allows R2 SQL to only read small parts of potentially multi-GB Parquet files.</p><p>The server that receives the user’s request and performs query planning assumes the role of query coordinator. It distributes the work across query workers and aggregates results before returning them to the user.</p><p>Cloudflare’s network is vast, and many servers can be in maintenance at the same time. The query coordinator contacts Cloudflare’s internal API to make sure only healthy, fully functioning servers are picked for query execution. Connections between coordinator and query worker go through <a href="https://www.cloudflare.com/en-gb/application-services/products/argo-smart-routing/"><u>Cloudflare Argo Smart Routing</u></a> to ensure fast, reliable connectivity.</p><p>Servers that receive query execution requests from the coordinator assume the role of query workers. Query workers serve as a point of horizontal scalability in R2 SQL. With a higher number of query workers, R2 SQL can process queries faster by distributing the work among many servers. That’s especially true for queries covering large amounts of files.</p><p>Both the coordinator and query workers run on Cloudflare’s distributed network, ensuring R2 SQL has plenty of compute power and I/O throughput to handle analytical workloads.</p><p>Each query worker receives a batch of row groups from the coordinator as well as an SQL query to run on it. Additionally, the coordinator sends serialized metadata about Parquet files containing the row groups. Thanks to that, query workers know exact byte offsets where each row group is located in the Parquet file without the need to read this information from R2.</p>
    <div>
      <h3>Apache DataFusion</h3>
      <a href="#apache-datafusion">
        
      </a>
    </div>
    <p>Internally, each query worker uses <a href="https://github.com/apache/datafusion"><u>Apache DataFusion</u></a> to run SQL queries against row groups. DataFusion is an open-source analytical query engine written in Rust. It is built around the concept of partitions. A query is split into multiple concurrent independent streams, each working on its own partition of data.</p><p>Partitions in DataFusion are similar to partitions in Iceberg, but serve a different purpose. In Iceberg, partitions are a way to physically organize data on object storage. In DataFusion, partitions organize in-memory data for query processing. While logically they are similar – rows grouped together based on some logic – in practice, a partition in Iceberg doesn’t always correspond to a partition in DataFusion.</p><p>DataFusion partitions map perfectly to the R2 SQL query worker’s data model because each row group can be considered its own independent partition. Thanks to that, each row group is processed in parallel.</p><p>At the same time, since row groups usually contain at least 1000 rows, R2 SQL benefits from vectorized execution. Each DataFusion partition stream can execute the SQL query on multiple rows in one go, amortizing the overhead of query interpretation.</p><p>There are two ends of the spectrum when it comes to query execution: processing all rows sequentially in one big batch and processing each individual row in parallel. Sequential processing creates a so-called “tight loop”, which is usually more CPU cache friendly. In addition to that, we can significantly reduce interpretation overhead, as processing a large number of rows at a time in batches means that we go through the query plan less often. Completely parallel processing doesn’t allow us to do these things, but makes use of multiple CPU cores to finish the query faster.</p><p>DataFusion’s architecture allows us to achieve a balance on this scale, reaping benefits from both ends. For each data partition, we gain better CPU cache locality and amortized interpretation overhead. At the same time, since many partitions are processed in parallel, we distribute the workload between multiple CPUs, cutting the execution time further.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2Tis1F5C1x3x6sIyJLL8ju/aae094818b1b7f6f8d6f857305948fbd/image1.png" />
          </figure><p>In addition to the smart query execution model, DataFusion also provides first-class Parquet support.</p><p>As a file format, Parquet has multiple optimizations designed specifically for query engines. Parquet is a column-based format, meaning that each column is physically separated from others. This separation allows better compression ratios, but it also allows the query engine to read columns selectively. If the query only ever uses five columns, we can only read them and skip reading the remaining fifty. This massively reduces the amount of data we need to read from R2 and the CPU time spent on decompression.</p><p>DataFusion does exactly that. Using R2 ranged reads, it is able to read parts of the Parquet files containing the requested columns, skipping the rest.</p><p>DataFusion’s optimizer also allows us to push down any filters to the lowest levels of the query plan. In other words, we can apply filters right as we are reading values from Parquet files. This allows us to skip materialization of results we know for sure won’t be returned to the user, cutting the query execution time further.</p>
    <div>
      <h3>Returning query results</h3>
      <a href="#returning-query-results">
        
      </a>
    </div>
    <p>Once the query worker finishes computing results, it returns them to the coordinator through <a href="https://grpc.io/"><u>the gRPC protocol</u></a>.</p><p>R2 SQL uses <a href="https://arrow.apache.org/"><u>Apache Arrow</u></a> for internal representation of query results. Arrow is an in-memory format that efficiently represents arrays of structured data. It is also used by DataFusion during query execution to represent partitions of data.</p><p>In addition to being an in-memory format, Arrow also defines the <a href="https://arrow.apache.org/docs/format/Columnar.html#format-ipc"><u>Arrow IPC</u></a> serialization format. Arrow IPC isn’t designed for long-term storage of the data, but for inter-process communication, which is exactly what query workers and the coordinator do over the network. The query worker serializes all the results into the Arrow IPC format and embeds them into the gRPC response. The coordinator in turn deserializes results and can return to working on Arrow arrays.</p>
    <div>
      <h2>Future plans</h2>
      <a href="#future-plans">
        
      </a>
    </div>
    <p>While R2 SQL is currently quite good at executing filter queries, we also plan to rapidly add new capabilities over the coming months. This includes, but is not limited to, adding:</p><ul><li><p>Support for complex aggregations in a distributed and scalable fashion;</p></li><li><p>Tools to help provide visibility in query execution to help developers improve performance;</p></li><li><p>Support for many of the configuration options Apache Iceberg supports.</p></li></ul><p>In addition to that, we have plans to improve our developer experience by allowing users to query their R2 Data Catalogs using R2 SQL from the Cloudflare Dashboard.</p><p>Given Cloudflare’s distributed compute, network capabilities, and ecosystem of developer tools, we have the opportunity to build something truly unique here. We are exploring different kinds of indexes to make R2 SQL queries even faster and provide more functionality such as full text search, geospatial queries, and more. </p>
    <div>
      <h2>Try it now!</h2>
      <a href="#try-it-now">
        
      </a>
    </div>
    <p>It’s early days for R2 SQL, but we’re excited for users to get their hands on it. R2 SQL is available in open beta today! Head over to our<a href="https://developers.cloudflare.com/r2-sql/get-started/"> <u>getting started guide</u></a> to learn how to create an end-to-end data pipeline that processes and delivers events to an R2 Data Catalog table, which can then be queried with R2 SQL.</p><p>
We’re excited to see what you build! Come share your feedback with us on our<a href="http://discord.cloudflare.com/"> <u>Developer Discord</u></a>.</p><div>
  
</div><p></p> ]]></content:encoded>
            <category><![CDATA[R2]]></category>
            <category><![CDATA[Birthday Week]]></category>
            <category><![CDATA[Data]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <category><![CDATA[Edge Computing]]></category>
            <category><![CDATA[Rust]]></category>
            <category><![CDATA[Serverless]]></category>
            <category><![CDATA[SQL]]></category>
            <guid isPermaLink="false">7znvjodLkg1AxYlR992it2</guid>
            <dc:creator>Yevgen Safronov</dc:creator>
            <dc:creator>Nikita Lapkov</dc:creator>
            <dc:creator>Jérôme Schneider</dc:creator>
        </item>
        <item>
            <title><![CDATA[Sequential consistency without borders: how D1 implements global read replication]]></title>
            <link>https://blog.cloudflare.com/d1-read-replication-beta/</link>
            <pubDate>Thu, 10 Apr 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ D1, Cloudflare’s managed SQL database, announces read replication beta. Here's a deep dive of the read replication implementation and how your queries can remain consistent across all regions. ]]></description>
            <content:encoded><![CDATA[ <p>Read replication of <a href="https://www.cloudflare.com/developer-platform/products/d1/">D1 databases</a> is in public beta!</p><p>D1 read replication makes read-only copies of your database available in multiple regions across Cloudflare’s network.  For busy, read-heavy applications like e-commerce websites, content management tools, and mobile apps:</p><ul><li><p>D1 read replication lowers average latency by routing user requests to read replicas in nearby regions.</p></li><li><p>D1 read replication increases overall throughput by offloading read queries to read replicas, allowing the primary database to handle more write queries.</p></li></ul><p>The main copy of your database is called the primary database and the read-only copies are called read replicas.  When you enable replication for a D1 database, the D1 service automatically creates and maintains read replicas of your primary database.  As your users make requests, D1 routes those requests to an appropriate copy of the database (either the primary or a replica) based on performance heuristics, the type of queries made in those requests, and the query consistency needs as expressed by your application.</p><p>All of this global replica creation and request routing is handled by Cloudflare at no additional cost.</p><p>To take advantage of read replication, your Worker needs to use the new D1 <a href="https://developers.cloudflare.com/d1/best-practices/read-replication/"><u>Sessions API</u></a>. Click the button below to run a Worker using D1 read replication with this <a href="https://github.com/cloudflare/templates/tree/main/d1-starter-sessions-api-template"><u>code example</u></a> to see for yourself!</p><a href="https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/main/d1-starter-sessions-api-template"><img src="https://deploy.workers.cloudflare.com/button" /></a>
<p></p>
    <div>
      <h2>D1 Sessions API</h2>
      <a href="#d1-sessions-api">
        
      </a>
    </div>
    <p>D1’s read replication feature is built around the concept of database <i>sessions</i>.  A session encapsulates all the queries representing one logical session for your application. For example, a session might represent all requests coming from a particular web browser or all requests coming from a mobile app used by one of your users. If you use sessions, your queries will use the appropriate copy of the D1 database that makes the most sense for your request, be that the primary database or a nearby replica.</p><p>The sessions implementation ensures <a href="https://jepsen.io/consistency/models/sequential"><u>sequential consistency</u></a> for all queries in the session, no matter what copy of the database each query is routed to.  The sequential consistency model has important properties like "<a href="https://jepsen.io/consistency/models/read-your-writes"><u>read my own writes</u></a>" and "<a href="https://jepsen.io/consistency/models/writes-follow-reads"><u>writes follow reads</u></a>," as well as a total ordering of writes. The total ordering of writes means that every replica will see transactions committed in the same order, which is exactly the behavior we want in a transactional system.  Said another way, sequential consistency guarantees that the reads and writes are executed in the order in which you write them in your code.</p><p>Some examples of consistency implications in real-world applications:</p><ul><li><p>You are using an online store and just placed an order (write query), followed by a visit to the account page to list all your orders (read query handled by a replica). You want the newly placed order to be listed there as well.</p></li><li><p>You are using your bank’s web application and make a transfer to your electricity provider (write query), and then immediately navigate to the account balance page (read query handled by a replica) to check the latest balance of your account, including that last payment.</p></li></ul><p>Why do we need the Sessions API? Why can we not just query replicas directly?</p><p>Applications using D1 read replication need the Sessions API because D1 runs on Cloudflare’s global network and there’s no way to ensure that requests from the same client get routed to the same replica for every request. For example, the client may switch from WiFi to a mobile network in a way that changes how their requests are routed to Cloudflare. Or the data center that handled previous requests could be down because of an outage or maintenance.</p><p>D1’s read replication is asynchronous, so it’s possible that when you switch between replicas, the replica you switch to lags behind the replica you were using. This could mean that, for example, the new replica hasn’t learned of the writes you just completed.  We could no longer guarantee useful properties like “read your own writes”.  In fact, in the presence of shifty routing, the only consistency property we could guarantee is that what you read had been committed at some point in the past (<a href="https://jepsen.io/consistency/models/read-committed"><u>read committed</u></a> consistency), which isn’t very useful at all!</p><p>Since we can’t guarantee routing to the same replica, we flip the script and use the information we get from the Sessions API to make sure whatever replica we land on can handle the request in a sequentially-consistent manner.</p><p>Here’s what the Sessions API looks like in a Worker:</p>
            <pre><code>export default {
  async fetch(request: Request, env: Env) {
    // A. Create the session.
    // When we create a D1 session, we can continue where we left off from a previous    
    // session if we have that session's last bookmark or use a constraint.
    const bookmark = request.headers.get('x-d1-bookmark') ?? 'first-unconstrained'
    const session = env.DB.withSession(bookmark)

    // Use this session for all our Workers' routes.
    const response = await handleRequest(request, session)

    // B. Return the bookmark so we can continue the session in another request.
    response.headers.set('x-d1-bookmark', session.getBookmark())

    return response
  }
}

async function handleRequest(request: Request, session: D1DatabaseSession) {
  const { pathname } = new URL(request.url)

  if (request.method === "GET" &amp;&amp; pathname === '/api/orders') {
    // C. Session read query.
    const { results } = await session.prepare('SELECT * FROM Orders').all()
    return Response.json(results)

  } else if (request.method === "POST" &amp;&amp; pathname === '/api/orders') {
    const order = await request.json&lt;Order&gt;()

    // D. Session write query.
    // Since this is a write query, D1 will transparently forward it to the primary.
    await session
      .prepare('INSERT INTO Orders VALUES (?, ?, ?)')
      .bind(order.orderId, order.customerId, order.quantity)
      .run()

    // E. Session read-after-write query.
    // In order for the application to be correct, this SELECT statement must see
    // the results of the INSERT statement above.
    const { results } = await session
      .prepare('SELECT * FROM Orders')
      .all()

    return Response.json(results)
  }

  return new Response('Not found', { status: 404 })
}</code></pre>
            <p>To use the Session API, you first need to create a session using the <code>withSession</code> method (<b><i>step A</i></b>).  The <code>withSession</code> method takes a bookmark as a parameter, or a constraint.  The provided constraint instructs D1 where to forward the first query of the session. Using <code>first-unconstrained</code> allows the first query to be processed by any replica without any restriction on how up-to-date it is. Using <code>first-primary</code> ensures that the first query of the session will be forwarded to the primary.</p>
            <pre><code>// A. Create the session.
const bookmark = request.headers.get('x-d1-bookmark') ?? 'first-unconstrained'
const session = env.DB.withSession(bookmark)</code></pre>
            <p>Providing an explicit bookmark instructs D1 that whichever database instance processes the query has to be at least as up-to-date as the provided bookmark (in case of a replica; the primary database is always up-to-date by definition).  Explicit bookmarks are how we can continue from previously-created sessions and maintain sequential consistency across user requests.</p><p>Once you’ve created the session, make queries like you normally would with D1.  The session object ensures that the queries you make are sequentially consistent with regards to each other.</p>
            <pre><code>// C. Session read query.
const { results } = await session.prepare('SELECT * FROM Orders').all()</code></pre>
            <p>For example, in the code example above, the session read query for listing the orders (<b><i>step C</i></b>) will return results that are at least as up-to-date as the bookmark used to create the session (<b><i>step A</i></b><i>)</i>.</p><p>More interesting is the write query to add a new order (<b><i>step D</i></b>) followed by the read query to list all orders (<b><i>step E</i></b>). Because both queries are executed on the same session, it is guaranteed that the read query will observe a database copy that includes the write query, thus maintaining sequential consistency.</p>
            <pre><code>// D. Session write query.
await session
  .prepare('INSERT INTO Orders VALUES (?, ?, ?)')
  .bind(order.orderId, order.customerId, order.quantity)
  .run()

// E. Session read-after-write query.
const { results } = await session
  .prepare('SELECT * FROM Orders')
  .all()</code></pre>
            <p>Note that we could make a single batch query to the primary including both the write and the list, but the benefit of using the new Sessions API is that you can use the extra read replica databases for your read queries and allow the primary database to handle more write queries.</p><p>The session object does the necessary bookkeeping to maintain the latest bookmark observed across all queries executed using that specific session, and always includes that latest bookmark in requests to D1. Note that any query executed without using the session object is not guaranteed to be sequentially consistent with the queries executed in the session.</p><p>When possible, we suggest continuing sessions across requests by including bookmarks in your responses to clients (<b><i>step B</i></b>), and having clients passing previously received bookmarks in their future requests.</p>
            <pre><code>// B. Return the bookmark so we can continue the session in another request.
response.headers.set('x-d1-bookmark', session.getBookmark())</code></pre>
            <p>This allows <i>all</i> of a client’s requests to be in the same session. You can do this by grabbing the session’s current bookmark at the end of the request (<code>session.getBookmark()</code>) and sending the bookmark in the response back to the client in HTTP headers, in HTTP cookies, or in the response body itself.</p>
    <div>
      <h3>Consistency with and without Sessions API</h3>
      <a href="#consistency-with-and-without-sessions-api">
        
      </a>
    </div>
    <p>In this section, we will explore the classic scenario of a read-after-write query to showcase how using the new D1 Sessions API ensures that we get sequential consistency and avoid any issues with inconsistent results in our application.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1zIBf3V1YIogYJKeWm1kDn/f484faf38cc0f8d7227f9db1fa386354/1.png" />
          </figure><p>The Client, a user Worker, sends a D1 write query that gets processed by the database primary and gets the results back. However, the subsequent read query ends up being processed by a database replica. If the database replica is lagging far enough behind the database primary, such that it does not yet include the first write query, then the returned results will be inconsistent, and probably incorrect for your application business logic.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1w81ec5tNGWJ7sQyFBZQ6l/d487ccf225a097a0e48054d88df0ba1f/2.png" />
          </figure><p>Using the Sessions API fixes the inconsistency issue. The first write query is again processed by the database primary, and this time the response includes “<b>Bookmark 100</b>”. The session object will store this bookmark for you transparently.</p><p>The subsequent read query is processed by database replica as before, but now since the query includes the previously received “<b>Bookmark 100</b>”, the database replica will wait until its database copy is at least up-to-date as “<b>Bookmark 100</b>”. Only once it’s up-to-date, the read query will be processed and the results returned, including the replica’s latest bookmark “<b>Bookmark 104</b>”.</p><p>Notice that the returned bookmark for the read query is “<b>Bookmark 104</b>”, which is different from the one passed in the query request. This can happen if there were other writes from other client requests that also got replicated to the database replica in-between the two queries our own client executed.</p>
    <div>
      <h2>Enabling read replication</h2>
      <a href="#enabling-read-replication">
        
      </a>
    </div>
    <p>To start using D1 read replication:</p><ol><li><p>Update your Worker to use the D1 Sessions API to tell D1 what queries are part of the same database session. The Sessions API works with databases that do not have read replication enabled as well, so it’s safe to ship this code even before you enable replicas. Here’s <a href="http://developers.cloudflare.com/d1/best-practices/read-replication/"><u>an example</u></a>.</p></li><li><p><a href="https://developers.cloudflare.com/d1/best-practices/read-replication/#enable-read-replication"><u>Enable replicas</u></a> for your database via <a href="https://dash.cloudflare.com/?to=/:account/workers/d1"><u>Cloudflare dashboard</u></a> &gt; Select D1 database &gt; Settings.</p></li></ol><p>D1 read replication is built into D1, and you don’t pay extra storage or compute costs for replicas. You incur the exact same D1 usage with or without replicas, based on <code>rows_read</code> and <code>rows_written</code> by your queries. Unlike other traditional database systems with replication, you don’t have to manually create replicas, including where they run, or decide how to route requests between the primary database and read replicas. Cloudflare handles this when using the Sessions API while ensuring sequential consistency.</p><p>Since D1 read replication is in beta, we recommend trying D1 read replication on a non-production database first, and migrate to your production workloads after validating read replication works for your use case.</p><p>If you don’t have a D1 database and want to try out D1 read replication, <a href="https://dash.cloudflare.com/?to=/:account/workers/d1/create"><u>create a test database</u></a> in the Cloudflare dashboard.</p>
    <div>
      <h3>Observing your replicas</h3>
      <a href="#observing-your-replicas">
        
      </a>
    </div>
    <p>Once you’ve enabled D1 read replication, read queries will start to be processed by replica database instances. The response of each query includes information in the nested <code>meta</code> object relevant to read replication, like <code>served_by_region</code> and <code>served_by_primary</code>. The first denotes the region of the database instance that processed the query, and the latter will be <code>true</code> if-and-only-if your query was processed by the primary database instance.</p><p>In addition, the <a href="https://dash.cloudflare.com/?to=/:account/workers/d1/"><u>D1 dashboard overview</u></a> for a database now includes information about the database instances handling your queries. You can see how many queries are handled by the primary instance or by a replica, and a breakdown of the queries processed by region. The example screenshots below show graphs displaying the number of queries executed  and number of rows read by each region.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1ChIlqQ5xgJfiftOHw9Egg/b583d00d22dcea60e7439dfbfa1761df/image10.png" />
          </figure>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4Zze5y22759fOIYPOqrK1Y/6cd3c684006ca8234db20924cae8b960/image1.png" />
          </figure>
    <div>
      <h2>Under the hood: how D1 read replication is implemented</h2>
      <a href="#under-the-hood-how-d1-read-replication-is-implemented">
        
      </a>
    </div>
    <p>D1 is implemented on top of SQLite-backed Durable Objects running on top of Cloudflare’s <a href="https://blog.cloudflare.com/sqlite-in-durable-objects/#under-the-hood-storage-relay-service"><u>Storage Relay Service</u></a>.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3GWWL8goIzrGTmkH54O416/aabd47fcd94bfc73492556b19ac6069f/5.png" />
          </figure><p>D1 is structured with a 3-layer architecture.  First is the binding API layer that runs in the customer’s Worker.  Next is a stateless Worker layer that routes requests based on database ID to a layer of Durable Objects that handle the actual SQL operations behind D1.  This is similar to how <a href="https://developers.cloudflare.com/durable-objects/what-are-durable-objects/#durable-objects-in-cloudflare"><u>most applications using Cloudflare Workers and Durable Objects are structured</u></a>.</p><p>For a non-replicated database, there is exactly one Durable Object per database.  When a user’s Worker makes a request with the D1 binding for the database, that request is first routed to a D1 Worker running in the same location as the user’s Worker.  The D1 Worker figures out which D1 Durable Object backs the user’s D1 database and fetches an RPC stub to that Durable Object.  The Durable Objects routing layer figures out where the Durable Object is located, and opens an RPC connection to it.  Finally, the D1 Durable Object then handles the query on behalf of the user’s Worker using the Durable Objects SQL API.</p><p>In the Durable Objects SQL API, all queries go to a SQLite database on the local disk of the server where the Durable Object is running.  Durable Objects run <a href="https://www.sqlite.org/wal.html"><u>SQLite in WAL mode</u></a>.  In WAL mode, every write query appends to a write-ahead log (the WAL).  As SQLite appends entries to the end of the WAL file, a database-specific component called the Storage Relay Service <i>leader</i> synchronously replicates the entries to 5 <i>durability followers</i> on servers in different datacenters.  When a quorum (at least 3 out of 5) of the durability followers acknowledge that they have safely stored the data, the leader allows SQLite’s write queries to commit and opens the Durable Object’s output gate, so that the Durable Object can respond to requests.</p><p>Our implementation of WAL mode allows us to have a complete log of all of the committed changes to the database. This enables a couple of important features in SQLite-backed Durable Objects and D1:</p><ul><li><p>We identify each write with a <a href="https://en.wikipedia.org/wiki/Lamport_timestamp"><u>Lamport timestamp</u></a> we call a <a href="https://developers.cloudflare.com/d1/reference/time-travel/#bookmarks"><u>bookmark</u></a>.</p></li><li><p>We construct databases anywhere in the world by downloading all of the WAL entries from cold storage and replaying each WAL entry in order.</p></li><li><p>We implement <a href="https://developers.cloudflare.com/d1/reference/time-travel/"><u>Point-in-time recovery (PITR)</u></a> by replaying WAL entries up to a specific bookmark rather than to the end of the log.</p></li></ul><p>Unfortunately, having the main data structure of the database be a log is not ideal.  WAL entries are in write order, which is often neither convenient nor fast.  In order to cut down on the overheads of the log, SQLite <i>checkpoints</i> the log by copying the WAL entries back into the main database file.  Read queries are serviced directly by SQLite using files on disk — either the main database file for checkpointed queries, or the WAL file for writes more recent than the last checkpoint.  Similarly, the Storage Relay Service snapshots the database to cold storage so that we can replay a database by downloading the most recent snapshot and replaying the WAL from there, rather than having to download an enormous number of individual WAL entries.</p><p>WAL mode is the foundation for implementing read replication, since we can stream writes to locations other than cold storage in real time.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/ezp8gcf3gXqkvumzufGfP/1a54fc6f434290968c7e695c2e5bb0c9/6.png" />
          </figure><p>We implemented read replication in 5 major steps.</p><p>First, we made it possible to make replica Durable Objects with a read-only copy of the database.  These replica objects boot by fetching the latest snapshot and replaying the log from cold storage to whatever bookmark primary database’s leader last committed. This basically gave us point-in-time replicas, since without continuous updates, the replicas never updated until the Durable Object restarted.</p><p>Second, we registered the replica leader with the primary’s leader so that the primary leader sends the replicas every entry written to the WAL at the same time that it sends the WAL entries to the durability followers.  Each of the WAL entries is marked with a bookmark that uniquely identifies the WAL entry in the sequence of WAL entries.  We’ll use the bookmark later.</p><p>Note that since these writes are sent to the replicas <i>before</i> a quorum of durability followers have confirmed them, the writes are actually unconfirmed writes, and the replica leader must be careful to keep the writes hidden from the replica Durable Object until they are confirmed.  The replica leader in the Storage Relay Service does this by implementing enough of SQLite’s <a href="https://www.sqlite.org/walformat.html#the_wal_index_file_format"><u>WAL-index protocol</u></a>, so that the unconfirmed writes coming from the primary leader look to SQLite as though it’s just another SQLite client doing unconfirmed writes.  SQLite knows to ignore the writes until they are confirmed in the log.  The upshot of this is that the replica leader can write WAL entries to the SQLite WAL <i>immediately,</i> and then “commit” them when the primary leader tells the replica that the entries have been confirmed by durability followers.</p><p>One neat thing about this approach is that writes are sent from the primary to the replica as quickly as they are generated by the primary, helping to minimize lag between replicas.  In theory, if the write query was proxied through a replica to the primary, the response back to the replica will arrive at almost the same time as the message that updates the replica.  In such a case, it looks like there’s no replica lag at all!</p><p>In practice, we find that replication is really fast.  Internally, we measure <i>confirm lag</i>, defined as the time from when a primary confirms a change to when the replica confirms a change.  The table below shows the confirm lag for two D1 databases whose primaries are in different regions.</p><div>
    <figure>
        <table>
            <colgroup>
                <col></col>
                <col></col>
                <col></col>
            </colgroup>
            <tbody>
                <tr>
                    <td>
                        <p><br /><span><span>Replica Region</span></span></p>
                    </td>
                    <td>
                        <p><span><span>Database A</span></span></p>
                        <p><span><span>(Primary region: ENAM)</span></span></p>
                    </td>
                    <td>
                        <p><span><span>Database B</span></span><br /><span><span>(Primary region: WNAM)</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>ENAM</span></span></p>
                    </td>
                    <td>
                        <p><span><span>N/A</span></span></p>
                    </td>
                    <td>
                        <p><span><span>30 ms</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>WNAM</span></span></p>
                    </td>
                    <td>
                        <p><span><span>45 ms</span></span></p>
                    </td>
                    <td>
                        <p><span><span>N/A</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>WEUR</span></span></p>
                    </td>
                    <td>
                        <p><span><span>55 ms</span></span></p>
                    </td>
                    <td>
                        <p><span><span>75 ms</span></span></p>
                    </td>
                </tr>
                <tr>
                    <td>
                        <p><span><span>EEUR</span></span></p>
                    </td>
                    <td>
                        <p><span><span>67 ms</span></span></p>
                    </td>
                    <td>
                        <p><span><span>75 ms</span></span></p>
                    </td>
                </tr>
            </tbody>
        </table>
    </figure>
</div><p><sup><i>Confirm lag for 2 replicated databases.  N/A means that we have no data for this combination.  The region abbreviations are the same ones used for </i></sup><a href="https://developers.cloudflare.com/durable-objects/reference/data-location/#supported-locations-1"><sup><i><u>Durable Object location hints</u></i></sup></a><sup><i>.</i></sup></p><p>The table shows that confirm lag is correlated with the network round-trip time between the data centers hosting the primary databases and their replicas.  This is clearly visible in the difference between the confirm lag for the European replicas of the two databases.  As airline route planners know, EEUR is <a href="http://www.gcmap.com/mapui?P=ewr-lhr,+ewr-waw"><u>appreciably further away</u></a> from ENAM than WEUR is, but from WNAM, both European regions (WEUR and EEUR) are <a href="http://www.gcmap.com/mapui?P=sjc-lhr,+sjc-waw"><u>about equally as far away</u></a>.  We see that in our replication numbers.</p><p>The exact placement of the D1 database in the region matters too.  Regions like ENAM and WNAM are quite large in themselves.  Database A’s placement in ENAM happens to be further away from most data centers in WNAM compared to database B’s placement in WNAM relative to the ENAM data centers.  As such, database B sees slightly lower confirm lag.</p><p>Try as we might, we can’t beat the speed of light!</p><p>Third, we updated the Durable Object routing system to be aware of Durable Object replicas.  When read replication is enabled on a Durable Object, two things happen.  First, we create a set of replicas according to a replication policy.  The current replication policy that D1 uses is simple: a static set of replicas in <a href="https://developers.cloudflare.com/d1/configuration/data-location/#available-location-hints"><u>every region that D1 supports</u></a>.  Second, we turn on a routing policy for the Durable Object.  The current policy that D1 uses is also simple: route to the Durable Object replica in the region close to where the user request is.  With this step, we have updateable read-only replicas, and can route requests to them!</p><p>Fourth, we updated D1’s Durable Object code to handle write queries on replicas. D1 uses SQLite to figure out whether a request is a write query or a read query.  This means that the determination of whether something is a read or write query happens <i>after</i> the request is routed.  Read replicas will have to handle write requests!  We solve this by instantiating each replica D1 Durable Object with a reference to its primary.  If the D1 Durable Object determines that the query is a write query, it forwards the request to the primary for the primary to handle. This happens transparently, keeping the user code simple.</p><p>As of this fourth step, we can handle read and write queries at every copy of the D1 Durable Object, whether it's a primary or not.  Unfortunately, as outlined above, if a user's requests get routed to different read replicas, they may see different views of the database, leading to a very weak consistency model.  So the last step is to implement the Sessions API across the D1 Worker and D1 Durable Object.  Recall that every WAL entry is marked with a bookmark.  These bookmarks uniquely identify a point in (logical) time in the database.  Our bookmarks are strictly monotonically increasing; every write to a database makes a new bookmark with a value greater than any other bookmark for that database.</p><p>Using bookmarks, we implement the Sessions API with the following algorithm split across the D1 binding implementation, the D1 Worker, and D1 Durable Object.</p><p>First up in the D1 binding, we have code that creates the <code>D1DatabaseSession</code> object and code within the <code>D1DatabaseSession</code> object to keep track of the latest bookmark.</p>
            <pre><code>// D1Binding is the binding code running within the user's Worker
// that provides the existing D1 Workers API and the new withSession method.
class D1Binding {
  // Injected by the runtime to the D1 Binding.
  d1Service: D1ServiceBinding

  function withSession(initialBookmark) {
    return D1DatabaseSession(this.d1Service, this.databaseId, initialBookmark);
  }
}

// D1DatabaseSession holds metadata about the session, most importantly the
// latest bookmark we know about for this session.
class D1DatabaseSession {
  constructor(d1Service, databaseId, initialBookmark) {
    this.d1Service = d1Service;
    this.databaseId = databaseId;
    this.bookmark = initialBookmark;
  }

  async exec(query) {
    // The exec method in the binding sends the query to the D1 Worker
    // and waits for the the response, updating the bookmark as
    // necessary so that future calls to exec use the updated bookmark.
    var resp = await this.d1Service.handleUserQuery(databaseId, query, bookmark);
    if (isNewerBookmark(this.bookmark, resp.bookmark)) {
      this.bookmark = resp.bookmark;
    }
    return resp;
  }

  // batch and other SQL APIs are implemented similarly.
}</code></pre>
            <p>The binding code calls into the D1 stateless Worker (<code>d1Service</code> in the snippet above), which figures out which Durable Object to use, and proxies the request to the Durable Object.</p>
            <pre><code>class D1Worker {
  async handleUserQuery(databaseId, query) {
    var doId = /* look up Durable Object for databaseId */;
    return await this.D1_DO.get(doId).handleWorkerQuery(query, bookmark)
  }
}</code></pre>
            <p>Finally, we reach the Durable Objects layer, which figures out how to actually handle the request.</p>
            <pre><code>class D1DurableObject {
  async handleWorkerQuery(queries, bookmark) {
    var bookmark = bookmark ?? "first-primary";
    var results = {};

    if (this.isPrimaryDatabase()) {
      // The primary always has the latest data so we can run the
      // query without checking the bookmark.
      var result = /* execute query directly */;
      bookmark = getCurrentBookmark();
      results = result;
    } else {
      // This is running on a replica.
      if (bookmark === "first-primary" || isWriteQuery(query)) {
        // The primary must handle this request, so we'll proxy the
        // request to the primary.
        var resp = await this.primary.handleWorkerQuery(query, bookmark);
        bookmark = resp.bookmark;
        results = resp.results;
      } else {
        // The replica can handle this request, but only after the
        // database is up-to-date with the bookmark.
        if (bookmark !== "first-unconstrained") {
          await waitForBookmark(bookmark);
        }
        var result = /* execute query locally */;
        bookmark = getCurrentBookmark();
        results = result;
      }
    }
    return { results: results, bookmark: bookmark };
  }
}</code></pre>
            <p>The D1 Durable Object first figures out if this instance can handle the query, or if the query needs to be sent to the primary.  If the Durable Object can execute the query, it ensures that we execute the query with a bookmark at least as up-to-date as the bookmark requested by the binding.</p><p>The upshot is that the three pieces of code work together to ensure that all of the queries in the session see the database in a sequentially consistent order, because each new query will be blocked until it has seen the results of previous queries within the same session.</p>
    <div>
      <h2>Conclusion</h2>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>D1’s new read replication feature is a significant step towards making globally distributed databases easier to use without sacrificing consistency. With automatically provisioned replicas in every region, your applications can now serve read queries faster while maintaining strong sequential consistency across requests, and keeping your application Worker code simple.</p><p>We’re excited for developers to explore this feature and see how it improves the performance of your applications. The public beta is just the beginning—we’re actively refining and expanding D1’s capabilities, including evolving replica placement policies, and your feedback will help shape what’s next.</p><p>Note that the Sessions API is only available through the <a href="https://developers.cloudflare.com/d1/worker-api/"><u>D1 Worker Binding</u></a> for now, and support for the HTTP REST API will follow soon.</p><p>Try out D1 read replication today by clicking the “Deploy to Cloudflare" button, check out <a href="http://developers.cloudflare.com/d1/best-practices/read-replication/"><u>documentation and examples</u></a>, and let us know what you build in the <a href="https://discord.com/channels/595317990191398933/992060581832032316"><u>D1 Discord channel</u></a>!</p><a href="https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/main/d1-starter-sessions-api-template"><img src="https://deploy.workers.cloudflare.com/button" /></a>
<p></p><p></p> ]]></content:encoded>
            <category><![CDATA[Developer Week]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <category><![CDATA[D1]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <category><![CDATA[Edge Database]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[SQL]]></category>
            <guid isPermaLink="false">2qUAO70BqnRBomg83fCRPe</guid>
            <dc:creator>Justin Mazzola Paluska</dc:creator>
            <dc:creator>Lambros Petrou</dc:creator>
        </item>
        <item>
            <title><![CDATA[Pools across the sea: how Hyperdrive speeds up access to databases and why we’re making it free]]></title>
            <link>https://blog.cloudflare.com/how-hyperdrive-speeds-up-database-access/</link>
            <pubDate>Tue, 08 Apr 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ Hyperdrive, Cloudflare's global connection pooler, relies on some key innovations to make your database connections work. Let's dive deeper, in celebration of its availability for Free Plan customers. ]]></description>
            <content:encoded><![CDATA[ <p></p><p></p>
    <div>
      <h2>Free as in beer</h2>
      <a href="#free-as-in-beer">
        
      </a>
    </div>
    <p>In acknowledgement of its pivotal role in building distributed applications that rely on regional databases, we’re making Hyperdrive available on the free plan of Cloudflare Workers!</p><p><a href="https://developers.cloudflare.com/hyperdrive/"><u>Hyperdrive</u></a> enables you to build performant, global apps on Workers with <a href="https://developers.cloudflare.com/hyperdrive/examples/"><u>your existing SQL databases</u></a>. Tell it your database connection string, bring your existing drivers, and Hyperdrive will make connecting to your database faster. No major <a href="https://www.cloudflare.com/learning/cloud/how-to-refactor-applications/">refactors</a> or convoluted configuration required.</p><p>Over the past year, Hyperdrive has become a key service for teams that want to build their applications on Workers and connect to SQL databases. This includes our own engineering teams, with Hyperdrive serving as the tool of choice to connect from Workers to our own Postgres clusters for many of the control-plane actions of our billing, <a href="https://www.cloudflare.com/developer-platform/products/d1/"><u>D1</u></a>, <a href="https://www.cloudflare.com/developer-platform/products/r2/"><u>R2</u></a>, and <a href="https://www.cloudflare.com/developer-platform/products/workers-kv/"><u>Workers KV</u></a> teams (just to name a few). </p><p>This has highlighted for us that Hyperdrive is a fundamental building block, and it solves a common class of problems for which there isn’t a great alternative. We want to make it possible for everyone building on Workers to connect to their database of choice with the best performance possible, using the drivers and frameworks they already know and love.</p>
    <div>
      <h3>Performance is a feature</h3>
      <a href="#performance-is-a-feature">
        
      </a>
    </div>
    <p>To illustrate how much Hyperdrive can improve your application’s performance, let’s write the world’s simplest benchmark. This is obviously not production code, but is meant to be reflective of a common application you’d bring to the Workers platform. We’re going to use a simple table, a very popular OSS driver (<a href="https://github.com/porsager/postgres"><u>postgres.js</u></a>), and run a standard OLTP workload from a Worker. We’re going to keep our origin database in London, and query it from Chicago (those locations will come back up later, so keep them in mind).</p>
            <pre><code>// This is the test table we're using
// CREATE TABLE IF NOT EXISTS test_data(userId bigint, userText text, isActive bool);

import postgres from 'postgres';

let direct_conn = '&lt;direct connection string here!&gt;';
let hyperdrive_conn = env.HYPERDRIVE.connectionString;

async function measureLatency(connString: string) {
	let beginTime = Date.now();
	let sql = postgres(connString);

	await sql`INSERT INTO test_data VALUES (${999}, 'lorem_ipsum', ${true})`;
	await sql`SELECT userId, userText, isActive FROM test_data WHERE userId = ${999}`;

	let latency = Date.now() - beginTime;
	ctx.waitUntil(sql.end());
	return latency;
}

let directLatency = await measureLatency(direct_conn);
let hyperdriveLatency = await measureLatency(hyperdrive_conn);</code></pre>
            <p>The code above</p><ol><li><p>Takes a standard database connection string, and uses it to create a database connection.</p></li><li><p>Loads a user record into the database.</p></li><li><p>Queries all records for that user.</p></li><li><p>Measures how long this takes to do with a direct connection, and with Hyperdrive.</p></li></ol><p>When connecting directly to the origin database, this set of queries takes an average of 1200 ms. With absolutely no other changes, just swapping out the connection string for <code>env.HYPERDRIVE.connectionString</code>, this number is cut down to 500 ms (an almost 60% reduction). If you enable Hyperdrive’s caching, so that the SELECT query is served from cache, this takes only 320 ms. With this one-line change, Hyperdrive will reduce the latency of this Worker by almost 75%! In addition to this speedup, you also get secure auth and transport, as well as a connection pool to help protect your database from being overwhelmed when your usage scales up. See it for yourself using our <a href="https://hyperdrive-demo.pages.dev/"><u>demo application</u></a>.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6TTbEV9d7NClGk0iRmkEG3/4d5e4fdeb195337a92942bc7e13dbb6f/image7.png" />
          </figure><p><sup><i>A demo application comparing latencies between Hyperdrive and direct-to-database connections.</i></sup></p><p>Traditional SQL databases are familiar and powerful, but they are designed to be colocated with long-running compute. They were not conceived in the era of modern serverless applications, and have connection models that don't take the constraints of such an environment into account. Instead, they require highly stateful connections that do not play well with Workers’ global and stateless model. Hyperdrive solves this problem by maintaining database connections across Cloudflare’s network ready to be used at a moment’s notice, caching your queries for fast access, and eliminating round trips to minimize network latency.</p><p>With this announcement, many developers are going to be taking a look at Hyperdrive for the first time over the coming weeks and months. To help people dive in and try it out, we think it’s time to talk about how Hyperdrive actually works.</p>
    <div>
      <h2>Staying warm in the pool</h2>
      <a href="#staying-warm-in-the-pool">
        
      </a>
    </div>
    <p>Let’s talk a bit about database connection poolers, how they work, and what problems they already solve. They are <a href="https://github.com/pgbouncer/pgbouncer/commit/a0d2b294e0270f8a246e5b98f0700716c0672b0d"><u>hardly a new technology</u></a>, after all. </p><p>The point of any connection pooler, Hyperdrive or others, is to minimize the overhead of establishing and coordinating database connections. Every new database connection requires additional <a href="https://blog.anarazel.de/2020/10/07/measuring-the-memory-overhead-of-a-postgres-connection/"><u>memory</u></a> and CPU time from the database server, and this can only scale just so well as the number of concurrent connections climbs. So the question becomes, how should database connections be shared across clients? </p><p>There are three <a href="https://www.pgbouncer.org/features.html"><u>commonly-used approaches</u></a> for doing so. These are:</p><ul><li><p><b>Session mode:</b> whenever a client connects, it is assigned a connection of its own until it disconnects. This dramatically reduces the available concurrency, in exchange for much simpler implementation and a broader selection of supported features</p></li><li><p><b>Transaction mode:</b> when a client is ready to send a query or open a transaction, it is assigned a connection on which to do so. This connection will be returned to the pool when the query or transaction concludes. Subsequent queries during the same client session may (or may not) be assigned a different connection.</p></li><li><p><b>Statement mode:</b> Like transaction mode, but a connection is given out and returned for each statement. Multi-statement transactions are disallowed.</p></li></ul><p>When building Hyperdrive, we had to decide which of these modes we wanted to use. Each of the approaches implies some <a href="https://jpcamara.com/2023/04/12/pgbouncer-is-useful.html"><u>fairly serious tradeoffs</u></a>, so what’s the right choice? For a service intended to make using a database from Workers as pleasant as possible we went with the choice that balances features and performance, and designed Hyperdrive as a transaction-mode pooler. This best serves the goals of supporting a large number of short-lived clients (and therefore very high concurrency), while still supporting the transactional semantics that cause so many people to reach for an RDBMS in the first place.</p><p>In terms of this part of its design, Hyperdrive takes its cues from many pre-existing popular connection poolers, and manages operations to allow our users to focus on designing their full-stack applications. There is a configured limit to the number of connections the pool will give out, limits to how long a connection will be held idle until it is allowed to drop and return resources to the database, bookkeeping around <a href="https://blog.cloudflare.com/elephants-in-tunnels-how-hyperdrive-connects-to-databases-inside-your-vpc-networks/"><u>prepared statements</u></a> being shared across pooled connections, and other traditional concerns of the management of these resources to help ensure the origin database is able to run smoothly. These are all described in <a href="https://developers.cloudflare.com/hyperdrive/platform/limits/"><u>our documentation</u></a>.</p>
    <div>
      <h2>Round and round we go</h2>
      <a href="#round-and-round-we-go">
        
      </a>
    </div>
    <p>Ok, so why build Hyperdrive then? Other poolers that solve these problems already exist — couldn’t developers using Workers just run one of those and call it a day? It turns out that connecting to regional poolers from Workers has the same major downside as connecting to regional databases: network latency and round trips.</p><p>Establishing a connection, whether to a database or a pool, requires many exchanges between the client and server. While this is true for all fully-fledged client-server databases (e.g. <a href="https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html"><u>MySQL</u></a>, <a href="https://github.com/mongodb/specifications/blob/master/source/auth/auth.md"><u>MongoDB</u></a>), we are going to focus on the <a href="https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-START-UP"><u>PostgreSQL</u></a> connection protocol flow in this post. As we work through all of the steps involved, what we most want to keep track of is how many round trips it takes to accomplish. Note that we’re mostly concerned about having to wait around while these happen, so “half” round trips such as in the first diagram are not counted. This is because we can send off the message and then proceed without waiting.</p><p>The first step to establishing a connection between Postgres client and server is very familiar ground to anyone who’s worked much with networks: <a href="https://www.cloudflare.com/learning/ddos/glossary/tcp-ip/"><u>a TCP startup handshake</u></a>. Postgres uses TCP for its underlying transport, and so we must have that connection before anything else can happen on top of it.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/58qrxBsbOXbFCFBzkZFIff/19caf62d24cdbf9c4ad69bfd8286e022/image5.png" />
          </figure><p>With our transport layer in place, the next step is to <a href="https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-SSL"><u>encrypt</u></a> the connection. The <a href="https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/"><u>TLS Handshake</u></a> involves some back-and-forth in its own right, though this has been reduced to just one round trip for TLS 1.3. Below is the simplest and fastest version of this exchange, but there are certainly scenarios where it can be much more complex.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5q7fVVQkB9Q43X3eaE76GP/b69c0ce964df370bd0609242f8e3de0c/image4.png" />
          </figure><p>After the underlying transport is established and secured, the application-level traffic can actually start! However, we’re not quite ready for queries, the client still needs to authenticate to a specific user and database. Again, there are multiple supported approaches that offer varying levels of speed and security. To make this comparison as fair as possible, we’re again going to consider the version that offers the fastest startup (password-based authentication).</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7KU6NHgZAW95nQyobo9Zwn/5f61d6e9ab6233186c865a9093a7f352/image8.png" />
          </figure><p>So, for those keeping score, establishing a new connection to your database takes a bare minimum of 5 round trips, and can very quickly climb from there. </p><p>While the latency of any given network round trip is going to vary based on so many factors that “it depends” is the only meaningful measurement available, some quick benchmarking during the writing of this post shows ~125 ms from Chicago to London. Now multiply that number by 5 round trips and the problem becomes evident: 625 ms to start up a connection is not viable in a distributed serverless environment. So how does Hyperdrive solve it? What if I told you the trick is that we do it all twice? To understand Hyperdrive’s secret sauce, we need to dive into Hyperdrive’s architecture.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/291ua8XgVnowWDOfEm05eR/a2674a9a393fcaaef8e2cfe64dd57402/image1.png" />
          </figure>
    <div>
      <h2>Impersonating a database server</h2>
      <a href="#impersonating-a-database-server">
        
      </a>
    </div>
    <p>The rest of this post is a deep dive into answering the question of how Hyperdrive does what it does. To give the clearest picture, we’re going to talk about some internal subsystems by name. To help keep everything straight, let’s start with a short glossary that you can refer back to if needed. These descriptions may not make sense yet, but they will by the end of the article.
</p><table><tr><td><p><b>Hyperdrive subsystem name</b></p></td><td><p><b>Brief description</b></p></td></tr><tr><td><p>Client</p></td><td><p>Lives on the same server as your Worker, talks directly to your database driver. This caches query results and sends queries to Endpoint if needed.</p></td></tr><tr><td><p>Endpoint</p></td><td><p>Lives in the data center nearest to your origin database, talks to your origin database. This caches query results and houses a pool of connections to your origin database.</p></td></tr><tr><td><p>Edge Validator</p></td><td><p>Sends a request to a Cloudflare data center to validate that Hyperdrive can connect to your origin database at time of creation.</p></td></tr><tr><td><p>Placement</p></td><td><p>Builds on top of Edge Validator to connect to your origin database from all eligible data centers, to identify which have the fastest connections.</p></td></tr></table><p>The first subsystem we want to dig into is named <code>Client. Client</code>’s first job is to pretend to be a database server. When a user’s Worker wants to connect to their database via Hyperdrive, they use a special connection string that the Worker runtime generates on the fly. This tells the Worker to reach out to a Hyperdrive process running on the same Cloudflare server, and direct all traffic to and from the database client to it.</p>
            <pre><code>import postgres from "postgres";

// Connect to Hyperdrive
const sql = postgres(env.HYPERDRIVE.connectionString);

// sql will now talk over an RPC channel to Hyperdrive, instead of via TCP to Postgres</code></pre>
            <p>Once this connection is established, the database driver will perform the usual handshake expected of it, with our <code>Client</code> playing the role of a database server and sending the appropriate responses. All of this happens on the same Cloudflare server running the Worker, and we observe that the p90 for all this is 4 ms (p50 is 2 ms). Quite a bit better than 625 ms, but how does that help? The query still needs to get to the database, right?</p><p><code>Client</code>’s second main job is to inspect the queries sent from a Worker, and decide whether they can be served from Cloudflare’s cache. We’ll talk more about that later on. Assuming that there are no cached query results available, <code>Client</code> will need to reach out to our second important subsystem, which we call <code>Endpoint</code>.</p>
    <div>
      <h2>In for the long haul</h2>
      <a href="#in-for-the-long-haul">
        
      </a>
    </div>
    <p>Before we dig into the role <code>Endpoint</code> plays, it’s worth talking more about how the <code>Client→Endpoint</code> connection works, because it’s a key piece of our solution. We have already talked a lot about the price of network round trips, and how a Worker might be quite far away from the origin database, so how does Hyperdrive handle the long trip from the <code>Client</code> running alongside their Worker to the <code>Endpoint</code> running near their database without expensive round trips?</p><p>This is accomplished with a very handy bit of Cloudflare’s networking infrastructure. When <code>Client</code> gets a cache miss, it will submit a request to our networking platform for a connection to whichever data center <code>Endpoint</code> is running on. This platform keeps a pool of ready TCP connections between all of Cloudflare’s data centers, such that we don’t need to do any preliminary handshakes to begin sending application-level traffic. You might say we put a connection pooler in our connection pooler.</p><p>Over this TCP connection, we send an initialization message that includes all of the buffered query messages the Worker has sent to <code>Client</code> (the mental model would be something like a <code>SYN</code> and a payload all bundled together). <code>Endpoint</code> will do its job processing this query, and respond by streaming the response back to <code>Client</code>, leaving the streaming channel open for any followup queries until <code>Client</code> disconnects. This approach allows us to send queries around the world with zero wasted round trips.</p>
    <div>
      <h2>Impersonating a database client</h2>
      <a href="#impersonating-a-database-client">
        
      </a>
    </div>
    <p><code>Endpoint</code> has a couple different jobs it has to do. Its first job is to pretend to be a database client, and to do the client half of the handshake shown above. Second, it must also do the same query processing that <code>Client</code> does with query messages. Finally, <code>Endpoint</code> will make the same determination on when it needs to reach out to the origin database to get uncached query results.</p><p>When <code>Endpoint</code> needs to query the origin database, it will attempt to take a connection out of a limited-size pool of database connections that it keeps. If there is an unused connection available, it is handed out from the pool and used to ferry the query to the origin database, and the results back to <code>Endpoint</code>. Once <code>Endpoint</code> has these results, the connection is immediately returned to the pool so that another <code>Client</code> can use it. These warm connections are usable in a matter of microseconds, which is obviously a dramatic improvement over the round trips from one region to another that a cold startup handshake would require.</p><p>If there are no currently unused connections sitting in the pool, it may start up a new one (assuming the pool has not already given out as many connections as it is allowed to). This set of handshakes looks exactly the same as the one <code>Client</code> does, but it happens across the network between a Cloudflare data center and wherever the origin database happens to be. These are the same 5 round trips as our original example, but instead of a full Chicago→London path on every single trip, perhaps it’s Virginia→London, or even London→London. Latency here will depend on which data center <code>Endpoint</code> is being housed in.</p>
    <div>
      <h2>Distributed choreography</h2>
      <a href="#distributed-choreography">
        
      </a>
    </div>
    <p>Earlier, we mentioned that Hyperdrive is a transaction-mode pooler. This means that when a driver is ready to send a query or open a transaction it must get a connection from the pool to use. The core challenge for a transaction-mode pooler is in aligning the state of the driver with the state of the connection checked out from the pool. For example, if the driver thinks it’s in a transaction, but the database doesn’t, then you might get errors or even corrupted results.</p><p>Hyperdrive achieves this by ensuring all connections are in the same state when they’re checked out of the pool: idle and ready for a query. Where Hyperdrive differs from other transaction-mode poolers is that it does this dance of matching up the states of two different connections across machines, such that there’s no need to share state between <code>Client</code> and <code>Endpoint</code>! Hyperdrive can terminate the incoming connection in <code>Client</code> on the same machine running the Worker, and pool the connections to the origin database wherever makes the most sense.</p><p>The job of a transaction-mode pooler is a hard one. Database connections are fundamentally stateful and keeping track of that state is important to maintain our guise when impersonating either a database client or a server. As an example, one of the trickier pieces of state to manage are <a href="https://www.postgresql.org/docs/current/protocol-overview.html#PROTOCOL-QUERY-CONCEPTS"><u>prepared statements</u></a>. When a user creates a new prepared statement, the prepared statement is only created on whichever database connection happened to be checked out at that time. Once the user finishes the transaction or query they are processing, the connection holding that statement is returned to the pool. From the user’s perspective they’re still connected using the same database connection, so a new query or transaction can reasonably expect to use that previously prepared statement. If a different connection is handed out for the next query and the query wants to make use of this resource, the pooler has to do something about it. We went into some depth on this topic in a <a href="https://blog.cloudflare.com/postgres-named-prepared-statements-supported-hyperdrive/"><u>previous blog post</u></a> when we released this feature, but in sum, the process looks like this:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2ibtnO4URpLJ6m3Nyd2kpW/331059a6fd18c7d70b95a15af8f57cd6/image2.png" />
          </figure><p>Hyperdrive implements this by keeping track of what statements have been prepared by a given client, as well as what statements have been prepared on each origin connection in the pool. When a query comes in expecting to re-use a particular prepared statement (#8 above), Hyperdrive checks if it’s been prepared on the checked-out origin connection. If it hasn’t, Hyperdrive will replay the wire-protocol message sequence to prepare it on the newly-checked-out origin connection (#10 above) before sending the query over it. Many little corrections like this are necessary to keep the client’s connection to Hyperdrive and Hyperdrive’s connection to the origin database lined up so that both sides see what they expect.</p>
    <div>
      <h2>Better, faster, smarter, closer</h2>
      <a href="#better-faster-smarter-closer">
        
      </a>
    </div>
    <p>This “split connection” approach is the founding innovation of Hyperdrive, and one of the most vital aspects of it is how it affects starting up new connections. While the same 5+ round trips must always happen on startup, the actual time spent on the round trips can be dramatically reduced by conducting them over the smallest possible distances. This impact of distance can be so big that there is still a huge latency reduction even though the startup round trips must now happen <i>twice</i> (once each between the Worker and <code>Client</code>, and <code>Endpoint</code> and your origin database). So how do we decide where to run everything, to lean into that advantage as much as possible?</p><p>The placement of <code>Client</code> has not really changed since the original design of Hyperdrive. Sharing a server with the Worker sending the queries means that the Worker runtime can connect directly to Hyperdrive with no network hop needed. While there is always room for microoptimizations, it’s hard to do much better than that from an architecture perspective.  By far the bigger piece of the latency puzzle is where to run <code>Endpoint</code>.</p><p>Hyperdrive keeps a list of data centers that are eligible to house <code>Endpoint</code>s, requiring that they have sufficient capacity and the best routes available for pooled connections to use. The key challenge to overcome here is that a <a href="https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS"><u>database connection string</u></a> does not tell you where in the world a database actually is. The reality is that reliably going from a hostname to a precise (enough) geographic location is a hard problem, even leaving aside the additional complexity of doing so <a href="https://blog.cloudflare.com/elephants-in-tunnels-how-hyperdrive-connects-to-databases-inside-your-vpc-networks/"><u>within a private network</u></a>. So how do we pick from that list of eligible data centers?</p><p>For much of the time since its launch, Hyperdrive solved this with a regional pool approach. When a Worker connected to Hyperdrive, the location of the Worker was used to infer what region the end user was connecting from (e.g. ENAM, WEUR, APAC, etc. — see a rough breakdown <a href="https://www.cloudflare.com/network/"><u>here</u></a>). Data centers to house <code>Endpoint</code>s for any given Hyperdrive were deterministically selected from that region’s list of eligible options using <a href="https://en.wikipedia.org/wiki/Rendezvous_hashing"><u>rendezvous hashing</u></a>, resulting in one pool of connections <i>per region</i>.</p><p>This approach worked well enough, but it had some severe shortcomings. The first and most obvious is that there’s no guarantee that the data center selected for a given region is actually closer to the origin database than the user making the request. This means that, while you’re getting the benefit of the excellent routing available on <a href="https://www.cloudflare.com/network/"><u>Cloudflare's network</u></a>, you may be going significantly out of your way to do so. The second downside is that, in the scenario where a new connection must be created, the round trips to do so may be happening over a significantly larger distance than is necessary if the origin database is in a different region than the <code>Endpoint</code> housing the regional connection pool. This increases latency and reduces throughput for the query that needs to instantiate the connection.</p><p>The final key downside here is an unfortunate interaction with <a href="https://developers.cloudflare.com/workers/configuration/smart-placement/"><u>Smart Placement</u></a>, a feature of Cloudflare Workers that analyzes the duration of your Worker requests to identify the data center to run your Worker in. With regional <code>Endpoint</code>s, the best Smart Placement can possibly do is to put your requests close to the <code>Endpoint</code> for whichever region the origin database is in. Again, there may be other data centers that are closer, but Smart Placement has no way to do better than where the <code>Endpoint</code> is because all Hyperdrive queries must route through it.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3y9r3Dwn6APp5Pw6kqkg0Z/3a9e202670b6c65a22294fe777064add/image6.png" />
          </figure><p>We recently <a href="https://developers.cloudflare.com/changelog/2025-03-04-hyperdrive-pooling-near-database-and-ip-range-egress/"><u>shipped some improvements</u></a> to this system that significantly enhanced performance. The new system discards the concept of regional pools entirely, in favor of a single global <code>Endpoint</code> for each Hyperdrive that is in the eligible data center as close as possible to the origin database.</p><p>The way we solved locating the origin database such that we can accomplish this was ultimately very straightforward. We already had a subsystem to confirm, at the time of creation, that Hyperdrive could connect to an origin database using the provided information. We call this subsystem our <code>Edge Validator</code>.</p><p>It’s bad user experience to allow someone to create a Hyperdrive, and then find out when they go to use it that they mistyped their password or something. Now they’re stuck trying to debug with extra layers in the way, with a Hyperdrive that can’t possibly work. Instead, whenever a Hyperdrive is created, the <code>Edge Validator</code> will send a request to an arbitrary data center to use its instance of Hyperdrive to connect to the origin database. If this connection fails, the creation of the Hyperdrive will also fail, giving immediate feedback to the user at the time it is most helpful.</p><p>With our new subsystem, affectionately called <code>Placement</code>, we now have a solution to the geolocation problem. After <code>Edge Validator</code> has confirmed that the provided information works and the Hyperdrive is created, an extra step is run in the background. <code>Placement</code> will perform the exact same connection routine, except instead of being done once from an arbitrary data center, it is run a handful of times from every single data center that is eligible to house <code>Endpoints</code>. The latency of establishing these connections is collected, and the average is sent back to a central instance of <code>Placement</code>. The data centers that can connect to the origin database the fastest are, by definition, where we want to run <code>Endpoint</code> for this Hyperdrive. The list of these is saved, and at runtime is used to select the <code>Endpoint</code> best suited to housing the pool of connections to the origin database.</p><p>Given that the secret sauce of Hyperdrive is in managing and minimizing the latency of establishing these connections, moving <code>Endpoint</code>s right next to their origin databases proved to be pretty impactful.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1MZpaxXjj4tlOAinZkDqOF/e1a555a47141ac11aa391d27806cbfbc/image9.png" />
          </figure><p><sup><i>Pictured: query latency as measured from Endpoint to origin databases. The backfill of Placement to existing customers was done in stages on 02/22 and 02/25.</i></sup></p>
    <div>
      <h2>Serverless drivers exist, though?</h2>
      <a href="#serverless-drivers-exist-though">
        
      </a>
    </div>
    <p>While we went in a different direction, it’s worth acknowledging that other teams have <a href="https://neon.tech/blog/quicker-serverless-postgres"><u>solved this same problem</u></a> with a very different approach. Custom database drivers, usually called “serverless drivers”, have made several optimization efforts to reduce both the number of round trips and how quickly they can be conducted, while still connecting directly from your client to your database in the traditional way. While these drivers are impressive, we chose not to go this route for a couple of reasons.</p><p>First off, a big part of the appeal of using Postgres is its <a href="https://www.lastweekinaws.com/podcast/screaming-in-the-cloud/the-ever-growing-ecosystem-of-postgres-with-alvaro-hernandez/"><u>vibrant ecosystem</u></a>. Odds are good you’ve used Postgres before, and it can probably help solve whichever problem you’re tackling with your newest project. This familiarity and shared knowledge across projects is an absolute superpower. We wanted to lean into this advantage by supporting the most popular drivers already in this ecosystem, instead of fragmenting it by adding a competing one.</p><p>Second, Hyperdrive also functions as a cache for individual queries (a bit of trivia: its name while still in Alpha was actually <code>sql-query-cache</code>). Doing this as effectively as possible for distributed users requires some clever positioning of where exactly the query results should be cached. One of the unique advantages of running a distributed service on Cloudflare’s network is that we have a lot of flexibility on where to run things, and can confidently surmount challenges like those. If we’re going to be playing three-card monte with where things are happening anyway, it makes the most sense to favor that route for solving the other problems we’re trying to tackle too.</p>
    <div>
      <h2>Pick your favorite cache pun</h2>
      <a href="#pick-your-favorite-cache-pun">
        
      </a>
    </div>
    <p>As we’ve <a href="https://blog.cloudflare.com/postgres-named-prepared-statements-supported-hyperdrive/"><u>talked about</u></a> in the past, Hyperdrive buffers protocol messages until it has enough information to know whether a query can be served from cache. In a post about how Hyperdrive works it would be a shame to skip talking about how exactly we cache query results, so let’s close by diving into that.</p><p>First and foremost, Hyperdrive uses <a href="https://developers.cloudflare.com/cache/"><u>Cloudflare's cache</u></a>, because when you have technology like that already available to you, it’d be silly not to use it. This has some implications for our architecture that are worth exploring.</p><p>The cache exists in each of Cloudflare’s data centers, and by default these are separate instances. That means that a <code>Client</code> operating close to the user has one, and an <code>Endpoint</code> operating close to the origin database has one. However, historically we weren’t able to take full advantage of that, because the logic for interacting with cache was tightly bound to the logic for managing the pool of connections.</p><p>Part of our recent architecture refactoring effort, where we switched to global <code>Endpoint</code>s, was to split up this logic such that we can take advantage of <code>Client</code>’s cache too. This was necessary because, with <code>Endpoint</code> moving to a single location for each Hyperdrive, users from other regions would otherwise have gotten cache hits served from almost as far away as the origin.</p><p>With the new architecture, the role of <code>Client</code> during active query handling transitioned from that of a “dumb pipe” to more like what <code>Endpoint</code> had always been doing. It now buffers protocol messages, and serves results from cache if possible. In those scenarios, Hyperdrive’s traffic never leaves the data center that the Worker is running in, reducing query latencies from 20-70 ms to an average of around 4 ms. As a side benefit, it also substantially reduces the network bandwidth Hyperdrive uses to serve these queries. A win-win!</p><p>In the scenarios where query results can’t be served from the cache in <code>Client</code>’s data center, all is still not lost. <code>Endpoint</code> may also have cached results for this query, because it can field traffic from many different <code>Client</code>s around the world. If so, it will provide these results back to <code>Client</code>, along with how much time is remaining before they expire, such that <code>Client</code> can both return them and store them correctly into its own cache. Likewise, if <code>Endpoint</code> does need to go to the origin database for results, they will be stored into both <code>Client</code> and <code>Endpoint</code> caches. This ensures that followup queries from that same <code>Client</code> data center will get the happy path with single-digit ms response times, and also reduce load on the origin database from any other <code>Client</code>’s queries. This functions similarly to how <a href="https://developers.cloudflare.com/cache/how-to/tiered-cache/"><u>Cloudflare's Tiered Cache</u></a> works, with <code>Endpoint</code>’s cache functioning as a final layer of shielding for the origin database.</p>
    <div>
      <h2>Come on in, the water’s fine!</h2>
      <a href="#come-on-in-the-waters-fine">
        
      </a>
    </div>
    <p>With this announcement of a Free Plan for Hyperdrive, and newly armed with the knowledge of how it works under the hood, we hope you’ll enjoy building your next project with it! You can get started with a single Wrangler command (or using the dashboard):</p>
            <pre><code>wrangler hyperdrive create postgres-hyperdrive 
--connection-string="postgres://user:password@db-host.example.com:5432/defaultdb"</code></pre>
            <p>We’ve also included a Deploy to Cloudflare button below to let you get started with a sample Worker app using Hyperdrive, just bring your existing Postgres database! If you have any questions or ideas for future improvements, please feel free to visit our <a href="https://discord.com/channels/595317990191398933/1150557986239021106"><u>Discord channel!</u></a></p><a href="https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/main/postgres-hyperdrive-template"><img src="https://deploy.workers.cloudflare.com/button" /></a>
<p></p><p></p> ]]></content:encoded>
            <category><![CDATA[Developer Week]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Hyperdrive]]></category>
            <category><![CDATA[Smart Placement]]></category>
            <category><![CDATA[SQL]]></category>
            <guid isPermaLink="false">3YedZXQKWaCm2jUQPvAeQv</guid>
            <dc:creator>Andrew Repp</dc:creator>
            <dc:creator>Matt Alonso</dc:creator>
        </item>
        <item>
            <title><![CDATA[Over 700 million events/second: How we make sense of too much data]]></title>
            <link>https://blog.cloudflare.com/how-we-make-sense-of-too-much-data/</link>
            <pubDate>Mon, 27 Jan 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ Here we explain how we made our data pipeline scale to 700 million events per second while becoming more resilient than ever before. We share some math behind our approach and some of the designs of  ]]></description>
            <content:encoded><![CDATA[ <p>Cloudflare's network provides an enormous array of services to our customers. We collect and deliver associated data to customers in the form of event logs and aggregated analytics. As of December 2024, our data pipeline is ingesting up to 706M events per second generated by Cloudflare's services, and that represents 100x growth since our <a href="https://blog.cloudflare.com/http-analytics-for-6m-requests-per-second-using-clickhouse/"><u>2018 data pipeline blog post</u></a>. </p><p>At peak, we are moving 107 <a href="https://simple.wikipedia.org/wiki/Gibibyte"><u>GiB</u></a>/s of compressed data, either pushing it directly to customers or subjecting it to additional queueing and batching.</p><p>All of these data streams power things like <a href="https://developers.cloudflare.com/logs/"><u>Logs</u></a>, <a href="https://developers.cloudflare.com/analytics/"><u>Analytics</u></a>, and billing, as well as other products, such as training machine learning models for bot detection. This blog post is focused on techniques we use to efficiently and accurately deal with the high volume of data we ingest for our Analytics products. A previous <a href="https://blog.cloudflare.com/cloudflare-incident-on-november-14-2024-resulting-in-lost-logs/"><u>blog post</u></a> provides a deeper dive into the data pipeline for Logs. </p><p>The pipeline can be roughly described by the following diagram.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5ihv6JXx19nJiEyfCaCg8V/ad7081720514bafd070cc38a04bc7097/BLOG-2486_2.jpg" />
          </figure><p>The data pipeline has multiple stages, and each can and will naturally break or slow down because of hardware failures or misconfiguration. And when that happens, there is just too much data to be able to buffer it all for very long. Eventually some will get dropped, causing gaps in analytics and a degraded product experience unless proper mitigations are in place.</p>
    <div>
      <h3>Dropping data to retain information</h3>
      <a href="#dropping-data-to-retain-information">
        
      </a>
    </div>
    <p>How does one retain valuable information from more than half a billion events per second, when some must be dropped? Drop it in a controlled way, by downsampling.</p><p>Here is a visual analogy showing the difference between uncontrolled data loss and downsampling. In both cases the same number of pixels were delivered. One is a higher resolution view of just a small portion of a popular painting, while the other shows the full painting, albeit blurry and highly pixelated.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4kUGB4RLQzFb7cphMpHqAg/e7ccf871c73e0e8ca9dcac32fe265f18/Screenshot_2025-01-24_at_10.57.17_AM.png" />
          </figure><p>As we noted above, any point in the pipeline can fail, so we want the ability to downsample at any point as needed. Some services proactively downsample data at the source before it even hits Logfwdr. This makes the information extracted from that data a little bit blurry, but much more useful than what otherwise would be delivered: random chunks of the original with gaps in between, or even nothing at all. The amount of "blur" is outside our control (we make our best effort to deliver full data), but there is a robust way to estimate it, as discussed in the <a href="/how-we-make-sense-of-too-much-data/#extracting-value-from-downsampled-data"><u>next section</u></a>.</p><p>Logfwdr can decide to downsample data sitting in the buffer when it overflows. Logfwdr handles many data streams at once, so we need to prioritize them by assigning each data stream a weight and then applying <a href="https://en.wikipedia.org/wiki/Max-min_fairness"><u>max-min fairness</u></a> to better utilize the buffer. It allows each data stream to store as much as it needs, as long as the whole buffer is not saturated. Once it is saturated, streams divide it fairly according to their weighted size.</p><p>In our implementation (Go), each data stream is driven by a goroutine, and they cooperate via channels. They consult a single tracker object every time they allocate and deallocate memory. The tracker uses a <a href="https://en.wikipedia.org/wiki/Heap_(data_structure)"><u>max-heap</u></a> to always know who the heaviest participant is and what the total usage is. Whenever the total usage goes over the limit, the tracker repeatedly sends the "please shed some load" signal to the heaviest participant, until the usage is again under the limit.</p><p>The effect of this is that healthy streams, which buffer a tiny amount, allocate whatever they need without losses. But any lagging streams split the remaining memory allowance fairly.</p><p>We downsample more or less uniformly, by always taking some of the least downsampled batches from the buffer (using min-heap to find those) and merging them together upon downsampling.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/15VP0VYkrvkQboX9hrOy0q/e3d087fe704bd1b0ee41eb5b7a24b899/BLOG-2486_4.png" />
          </figure><p><sup><i>Merging keeps the batches roughly the same size and their number under control.</i></sup></p><p>Downsampling is cheap, but since data in the buffer is compressed, it causes recompression, which is the single most expensive thing we do to the data. But using extra CPU time is the last thing you want to do when the system is under heavy load! We compensate for the recompression costs by starting to downsample the fresh data as well (before it gets compressed for the first time) whenever the stream is in the "shed the load" state.</p><p>We called this approach "bottomless buffers", because you can squeeze effectively infinite amounts of data in there, and it will just automatically be thinned out. Bottomless buffers resemble <a href="https://en.wikipedia.org/wiki/Reservoir_sampling"><u>reservoir sampling</u></a>, where the buffer is the reservoir and the population comes as the input stream. But there are some differences. First is that in our pipeline the input stream of data never ends, while reservoir sampling assumes it ends to finalize the sample. Secondly, the resulting sample also never ends.</p><p>Let's look at the next stage in the pipeline: Logreceiver. It sits in front of a distributed queue. The purpose of logreceiver is to partition each stream of data by a key that makes it easier for Logpush, Analytics inserters, or some other process to consume.</p><p>Logreceiver proactively performs adaptive sampling of analytics. This improves the accuracy of analytics for small customers (receiving on the order of 10 events per day), while more aggressively downsampling large customers (millions of events per second). Logreceiver then pushes the same data at multiple resolutions (100%, 10%, 1%, etc.) into different topics in the distributed queue. This allows it to keep pushing something rather than nothing when the queue is overloaded, by just skipping writing the high-resolution samples of data.</p><p>The same goes for Inserters: they can skip <i>reading or writing</i> high-resolution data. The Analytics APIs can skip <i>reading</i> high resolution data. The analytical database might be unable to read high resolution data because of overload or degraded cluster state or because there is just too much to read (very wide time range or very large customer). Adaptively dropping to lower resolutions allows the APIs to return <i>some</i> results in all of those cases.</p>
    <div>
      <h3>Extracting value from downsampled data</h3>
      <a href="#extracting-value-from-downsampled-data">
        
      </a>
    </div>
    <p>Okay, we have some downsampled data in the analytical database. It looks like the original data, but with some rows missing. How do we make sense of it? How do we know if the results can be trusted?</p><p>Let's look at the math.</p>Since the amount of sampling can vary over time and between nodes in the distributed system, we need to store this information along with the data. With each event $x_i$ we store its sample interval, which is the reciprocal to its inclusion probability $\pi_i = \frac{1}{\text{sample interval}}$. For example, if we sample 1 in every 1,000 events, each of the events included in the resulting sample will have its $\pi_i = 0.001$, so the sample interval will be 1,000. When we further downsample that batch of data, the inclusion probabilities (and the sample intervals) multiply together: a 1 in 1,000 sample from a 1 in 1,000 sample is a 1 in 1,000,000 sample of the original population. The sample interval of an event can also be interpreted roughly as the number of original events that this event represents, so in the literature it is known as weight $w_i = \frac{1}{\pi_i}$.
<p></p>
We rely on the <a href="https://en.wikipedia.org/wiki/Horvitz%E2%80%93Thompson_estimator">Horvitz-Thompson estimator</a> (HT, <a href="https://www.stat.cmu.edu/~brian/905-2008/papers/Horvitz-Thompson-1952-jasa.pdf">paper</a>) in order to derive analytics about $x_i$. It gives two estimates: the analytical estimate (e.g. the population total or size) and the estimate of the variance of that estimate. The latter enables us to figure out how accurate the results are by building <a href="https://en.wikipedia.org/wiki/Confidence_interval">confidence intervals</a>. They define ranges that cover the true value with a given probability <i>(confidence level)</i>. A typical confidence level is 0.95, at which a confidence interval (a, b) tells that you can be 95% sure the true SUM or COUNT is between a and b.
<p></p><p>So far, we know how to use the HT estimator for doing SUM, COUNT, and AVG.</p>Given a sample of size $n$, consisting of values $x_i$ and their inclusion probabilities $\pi_i$, the HT estimator for the population total (i.e. SUM) would be

$$\widehat{T}=\sum_{i=1}^n{\frac{x_i}{\pi_i}}=\sum_{i=1}^n{x_i w_i}.$$

The variance of $\widehat{T}$ is:

$$\widehat{V}(\widehat{T}) = \sum_{i=1}^n{x_i^2 \frac{1 - \pi_i}{\pi_i^2}} + \sum_{i \neq j}^n{x_i x_j \frac{\pi_{ij} - \pi_i \pi_j}{\pi_{ij} \pi_i \pi_j}},$$

where $\pi_{ij}$ is the probability of both $i$-th and $j$-th events being sampled together.
<p></p>
We use <a href="https://en.wikipedia.org/wiki/Poisson_sampling">Poisson sampling</a>, where each event is subjected to an independent <a href="https://en.wikipedia.org/wiki/Bernoulli_trial">Bernoulli trial</a> ("coin toss") which determines whether the event becomes part of the sample. Since each trial is independent, we can equate $\pi_{ij} = \pi_i \pi_j$, which when plugged in the variance estimator above turns the right-hand sum to zero:

$$\widehat{V}(\widehat{T}) = \sum_{i=1}^n{x_i^2 \frac{1 - \pi_i}{\pi_i^2}} + \sum_{i \neq j}^n{x_i x_j \frac{0}{\pi_{ij} \pi_i \pi_j}},$$

thus

$$\widehat{V}(\widehat{T}) = \sum_{i=1}^n{x_i^2 \frac{1 - \pi_i}{\pi_i^2}} = \sum_{i=1}^n{x_i^2 w_i (w_i-1)}.$$

For COUNT we use the same estimator, but plug in $x_i = 1$. This gives us:

$$\begin{align}
\widehat{C} &amp;= \sum_{i=1}^n{\frac{1}{\pi_i}} = \sum_{i=1}^n{w_i},\\
\widehat{V}(\widehat{C}) &amp;= \sum_{i=1}^n{\frac{1 - \pi_i}{\pi_i^2}} = \sum_{i=1}^n{w_i (w_i-1)}.
\end{align}$$

For AVG we would use

$$\begin{align}
\widehat{\mu} &amp;= \frac{\widehat{T}}{N},\\
\widehat{V}(\widehat{\mu}) &amp;= \frac{\widehat{V}(\widehat{T})}{N^2},
\end{align}$$

if we could, but the original population size $N$ is not known, it is not stored anywhere, and it is not even possible to store because of custom filtering at query time. Plugging $\widehat{C}$ instead of $N$ only partially works. It gives a valid estimator for the mean itself, but not for its variance, so the constructed confidence intervals are unusable.
<p></p>
In all cases the corresponding pair of estimates are used as the $\mu$ and $\sigma^2$ of the normal distribution (because of the <a href="https://en.wikipedia.org/wiki/Central_limit_theorem">central limit theorem</a>), and then the bounds for the confidence interval (of confidence level ) are:

$$\Big( \mu - \Phi^{-1}\big(\frac{1 + \alpha}{2}\big) \cdot \sigma, \quad \mu + \Phi^{-1}\big(\frac{1 + \alpha}{2}\big) \cdot \sigma\Big).$$<p>We do not know the N, but there is a workaround: simultaneous confidence intervals. Construct confidence intervals for SUM and COUNT independently, and then combine them into a confidence interval for AVG. This is known as the <a href="https://www.sciencedirect.com/topics/mathematics/bonferroni-method"><u>Bonferroni method</u></a>. It requires generating wider (half the "inconfidence") intervals for SUM and COUNT. Here is a simplified visual representation, but the actual estimator will have to take into account the possibility of the orange area going below zero.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/69Vvi2CHSW8Gew0TWHSndj/1489cfe1ff57df4e7e1ca3c31a8444a5/BLOG-2486_5.png" />
          </figure><p>In SQL, the estimators and confidence intervals look like this:</p>
            <pre><code>WITH sum(x * _sample_interval)                              AS t,
     sum(x * x * _sample_interval * (_sample_interval - 1)) AS vt,
     sum(_sample_interval)                                  AS c,
     sum(_sample_interval * (_sample_interval - 1))         AS vc,
     -- ClickHouse does not expose the erf⁻¹ function, so we precompute some magic numbers,
     -- (only for 95% confidence, will be different otherwise):
     --   1.959963984540054 = Φ⁻¹((1+0.950)/2) = √2 * erf⁻¹(0.950)
     --   2.241402727604945 = Φ⁻¹((1+0.975)/2) = √2 * erf⁻¹(0.975)
     1.959963984540054 * sqrt(vt) AS err950_t,
     1.959963984540054 * sqrt(vc) AS err950_c,
     2.241402727604945 * sqrt(vt) AS err975_t,
     2.241402727604945 * sqrt(vc) AS err975_c
SELECT t - err950_t AS lo_total,
       t            AS est_total,
       t + err950_t AS hi_total,
       c - err950_c AS lo_count,
       c            AS est_count,
       c + err950_c AS hi_count,
       (t - err975_t) / (c + err975_c) AS lo_average,
       t / c                           AS est_average,
       (t + err975_t) / (c - err975_c) AS hi_average
FROM ...</code></pre>
            <p>Construct a confidence interval for each timeslot on the timeseries, and you get a confidence band, clearly showing the accuracy of the analytics. The figure below shows an example of such a band in shading around the line.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4JEnnC6P4BhM8qB8J5yKqt/3635835967085f9b24f64a5731457ddc/BLOG-2486_6.png" />
          </figure>
    <div>
      <h3>Sampling is easy to screw up</h3>
      <a href="#sampling-is-easy-to-screw-up">
        
      </a>
    </div>
    <p>We started using confidence bands on our internal dashboards, and after a while noticed something scary: a systematic error! For one particular website the "total bytes served" estimate was higher than the true control value obtained from rollups, and the confidence bands were way off. See the figure below, where the true value (blue line) is outside the yellow confidence band at all times.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/CHCyKyXqPMj8DnMpBUf3N/772fb61f02b79c59417f66d9dc0b5d19/BLOG-2486_7.png" />
          </figure><p>We checked the stored data for corruption, it was fine. We checked the math in the queries, it was fine. It was only after reading through the source code for all of the systems responsible for sampling that we found a candidate for the root cause.</p><p>We used simple random sampling everywhere, basically "tossing a coin" for each event, but in Logreceiver sampling was done differently. Instead of sampling <i>randomly</i> it would perform <i>systematic sampling</i> by picking events at equal intervals starting from the first one in the batch.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4xUwjxdylG5ARlFlDtv1OC/76db68677b7ae072b0a065f59d82c6f2/BLOG-2486_8.png" />
          </figure><p>Why would that be a problem?</p>There are two reasons. The first is that we can no longer claim $\pi_{ij} = \pi_i \pi_j$, so the simplified variance estimator stops working and confidence intervals cannot be trusted. But even worse, the estimator for the total becomes biased. To understand why exactly, we wrote a short repro code in Python:
<br /><p></p>
            <pre><code>import itertools

def take_every(src, period):
    for i, x in enumerate(src):
    if i % period == 0:
        yield x

pattern = [10, 1, 1, 1, 1, 1]
sample_interval = 10 # bad if it has common factors with len(pattern)
true_mean = sum(pattern) / len(pattern)

orig = itertools.cycle(pattern)
sample_size = 10000
sample = itertools.islice(take_every(orig, sample_interval), sample_size)

sample_mean = sum(sample) / sample_size

print(f"{true_mean=} {sample_mean=}")</code></pre>
            <p>After playing with different values for <code><b>pattern</b></code> and <code><b>sample_interval</b></code> in the code above, we realized where the bias was coming from.</p><p>Imagine a person opening a huge generated HTML page with many small/cached resources, such as icons. The first response will be big, immediately followed by a burst of small responses. If the website is not visited that much, responses will tend to end up all together at the start of a batch in Logfwdr. Logreceiver does not cut batches, only concatenates them. The first response remains first, so it always gets picked and skews the estimate up.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2WZUzqCwr2A6WgX1T5UE8z/7a2e08b611fb64e64a61e3d5c792fe23/BLOG-2486_9.png" />
          </figure><p>We checked the hypothesis against the raw unsampled data that we happened to have because that particular website was also using one of the <a href="https://developers.cloudflare.com/logs/"><u>Logs</u></a> products. We took all events in a given time range, and grouped them by cutting at gaps of at least one minute. In each group, we ranked all events by time and looked at the variable of interest (response size in bytes), and put it on a scatter plot against the rank inside the group.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2IXtqGkRjV0xs3wvwx609A/81e67736cacbccdd839c2177769ee4fe/BLOG-2486_10.png" />
          </figure><p>A clear pattern! The first response is much more likely to be larger than average.</p><p>We fixed the issue by making Logreceiver shuffle the data before sampling. As we rolled out the fix, the estimation and the true value converged.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4TL1pKDLw7MA6yGMSCahJN/227cb22054e0e8fe65c7766aa6e4b541/BLOG-2486_11.png" />
          </figure><p>Now, after battle testing it for a while, we are confident the HT estimator is implemented properly and we are using the correct sampling process.</p>
    <div>
      <h3>Using Cloudflare's analytics APIs to query sampled data</h3>
      <a href="#using-cloudflares-analytics-apis-to-query-sampled-data">
        
      </a>
    </div>
    <p>We already power most of our analytics datasets with sampled data. For example, the <a href="https://developers.cloudflare.com/analytics/analytics-engine/"><u>Workers Analytics Engine</u></a> exposes the <a href="https://developers.cloudflare.com/analytics/analytics-engine/sql-api/#sampling"><u>sample interval</u></a> in SQL, allowing our customers to build their own dashboards with confidence bands. In the GraphQL API, all of the data nodes that have "<a href="https://developers.cloudflare.com/analytics/graphql-api/sampling/#adaptive-sampling"><u>Adaptive</u></a>" in their name are based on sampled data, and the sample interval is exposed as a field there as well, though it is not possible to build confidence intervals from that alone. We are working on exposing confidence intervals in the GraphQL API, and as an experiment have added them to the count and edgeResponseBytes (sum) fields on the httpRequestsAdaptiveGroups nodes. This is available under <code><b>confidence(level: X)</b></code>.</p><p>Here is a sample GraphQL query:</p>
            <pre><code>query HTTPRequestsWithConfidence(
  $accountTag: string
  $zoneTag: string
  $datetimeStart: string
  $datetimeEnd: string
) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      httpRequestsAdaptiveGroups(
        filter: {
          datetime_geq: $datetimeStart
          datetime_leq: $datetimeEnd
      }
      limit: 100
    ) {
      confidence(level: 0.95) {
        level
        count {
          estimate
          lower
          upper
          sampleSize
        }
        sum {
          edgeResponseBytes {
            estimate
            lower
            upper
            sampleSize
          }
        }
      }
    }
  }
}
</code></pre>
            <p>The query above asks for the estimates and the 95% confidence intervals for <code><b>SUM(edgeResponseBytes)</b></code> and <code><b>COUNT</b></code>. The results will also show the sample size, which is good to know, as we rely on the <a href="https://en.wikipedia.org/wiki/Central_limit_theorem"><u>central limit theorem</u></a> to build the confidence intervals, thus small samples don't work very well.</p><p>Here is the response from this query:</p>
            <pre><code>{
  "data": {
    "viewer": {
      "zones": [
        {
          "httpRequestsAdaptiveGroups": [
            {
              "confidence": {
                "level": 0.95,
                "count": {
                  "estimate": 96947,
                  "lower": "96874.24",
                  "upper": "97019.76",
                  "sampleSize": 96294
                },
                "sum": {
                  "edgeResponseBytes": {
                    "estimate": 495797559,
                    "lower": "495262898.54",
                    "upper": "496332219.46",
                    "sampleSize": 96294
                  }
                }
              }
            }
          ]
        }
      ]
    }
  },
  "errors": null
}
</code></pre>
            <p>The response shows the estimated count is 96947, and we are 95% confident that the true count lies in the range 96874.24 to 97019.76. Similarly, the estimate and range for the sum of response bytes are provided.</p><p>The estimates are based on a sample size of 96294 rows, which is plenty of samples to calculate good confidence intervals.</p>
    <div>
      <h3>Conclusion</h3>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>We have discussed what kept our data pipeline scalable and resilient despite doubling in size every 1.5 years, how the math works, and how it is easy to mess up. We are constantly working on better ways to keep the data pipeline, and the products based on it, useful to our customers. If you are interested in doing things like that and want to help us build a better Internet, check out our <a href="http://www.cloudflare.com/careers"><u>careers page</u></a>.</p> ]]></content:encoded>
            <category><![CDATA[Bugs]]></category>
            <category><![CDATA[Analytics]]></category>
            <category><![CDATA[Data]]></category>
            <category><![CDATA[GraphQL]]></category>
            <category><![CDATA[SQL]]></category>
            <category><![CDATA[Go]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <category><![CDATA[Sampling]]></category>
            <guid isPermaLink="false">64DSvKdN853gq5Bx3Cyfij</guid>
            <dc:creator>Constantin Pan</dc:creator>
            <dc:creator>Jim Hawkridge</dc:creator>
        </item>
        <item>
            <title><![CDATA[Elephants in tunnels: how Hyperdrive connects to databases inside your VPC networks]]></title>
            <link>https://blog.cloudflare.com/elephants-in-tunnels-how-hyperdrive-connects-to-databases-inside-your-vpc-networks/</link>
            <pubDate>Fri, 25 Oct 2024 13:00:00 GMT</pubDate>
            <description><![CDATA[ Hyperdrive (Cloudflare’s globally distributed SQL connection pooler and cache) recently added support for directing database traffic from Workers across Cloudflare Tunnels. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>With September’s <a href="https://blog.cloudflare.com/builder-day-2024-announcements/#connect-to-private-databases-from-workers"><u>announcement</u></a> of Hyperdrive’s ability to send database traffic from <a href="https://workers.cloudflare.com/"><u>Workers</u></a> over <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/"><u>Cloudflare Tunnels</u></a>, we wanted to dive into the details of what it took to make this happen.</p>
    <div>
      <h2>Hyper-who?</h2>
      <a href="#hyper-who">
        
      </a>
    </div>
    <p>Accessing your data from anywhere in Region Earth can be hard. Traditional databases are powerful, familiar, and feature-rich, but your users can be thousands of miles away from your database. This can cause slower connection startup times, slower queries, and connection exhaustion as everything takes longer to accomplish.</p><p><a href="https://developers.cloudflare.com/workers/"><u>Cloudflare Workers</u></a> is an incredibly lightweight runtime, which enables our customers to deploy their applications globally by default and renders the <a href="https://en.wikipedia.org/wiki/Cold_start_(computing)"><u>cold start</u></a> problem almost irrelevant. The trade-off for these light, ephemeral execution contexts is the lack of persistence for things like database connections. Database connections are also notoriously expensive to spin up, with many round trips required between client and server before any query or result bytes can be exchanged.</p><p><a href="https://blog.cloudflare.com/hyperdrive-making-regional-databases-feel-distributed"><u>Hyperdrive</u></a> is designed to make the centralized databases you already have feel like they’re global while keeping connections to those databases hot. We use our <a href="https://www.cloudflare.com/network/"><u>global network</u></a> to get faster routes to your database, keep connection pools primed, and cache your most frequently run queries as close to users as possible.</p>
    <div>
      <h2>Why a Tunnel?</h2>
      <a href="#why-a-tunnel">
        
      </a>
    </div>
    <p>For something as sensitive as your database, exposing access to the public Internet can be uncomfortable. It is common to instead host your database on a private network, and allowlist known-safe IP addresses or configure <a href="https://www.cloudflare.com/learning/network-layer/what-is-gre-tunneling/"><u>GRE tunnels</u></a> to permit traffic to it. This is complex, toilsome, and error-prone. </p><p>On Cloudflare’s <a href="https://www.cloudflare.com/en-gb/developer-platform/"><u>Developer Platform</u></a>, we strive for simplicity and ease-of-use. We cannot expect all of our customers to be experts in configuring networking solutions, and so we went in search of a simpler solution. <a href="https://www.cloudflare.com/the-net/top-of-mind-security/customer-zero/"><u>Being your own customer</u></a> is rarely a bad choice, and it so happens that Cloudflare offers an excellent option for this scenario: Tunnels.</p><p><a href="https://www.cloudflare.com/products/tunnel/"><u>Cloudflare Tunnel</u></a> is a Zero Trust product that creates a secure connection between your private network and Cloudflare. Exposing services within your private network can be as simple as <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"><u>running a </u><code><u>cloudflared</u></code><u> binary</u></a>, or deploying a Docker container running the <a href="https://hub.docker.com/r/cloudflare/cloudflared"><code><u>cloudflared</u></code><u> image we distribute</u></a>. </p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3182f43rbwdH9krF1xhdlC/d22430cdb1efa134031f94fea691c36e/image1.png" />
          </figure>
    <div>
      <h2>A custom handler and generic streams</h2>
      <a href="#a-custom-handler-and-generic-streams">
        
      </a>
    </div>
    <p>Integrating with Tunnels to support sending Postgres directly through them was a bit of a new challenge for us. Most of the time, when we use Tunnels internally (more on that later!), we rely on the excellent job <code>cloudflared</code> does of handling all of the mechanics, and we just treat them as pipes. That wouldn’t work for Hyperdrive, though, so we had to dig into how Tunnels actually ingress traffic to build a solution.</p><p>Hyperdrive handles Postgres traffic using an entirely custom implementation of the <a href="https://www.postgresql.org/docs/current/protocol.html"><u>Postgres message protocol</u></a>. This is necessary, because we sometimes have to <a href="https://blog.cloudflare.com/postgres-named-prepared-statements-supported-hyperdrive"><u>alter the specific type or content</u></a> of messages sent from client to server, or vice versa. Handling individual bytes gives us the flexibility to implement whatever logic any new feature might need.</p><p>An additional, perhaps less obvious, benefit of handling Postgres message traffic as just bytes is that we are not bound to the transport layer choices of some <a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping"><u>ORM</u></a> or library. One of the nuances of running services in Cloudflare is that we may want to egress traffic over different services or protocols, for a variety of different reasons. In this case, being able to egress traffic via a Tunnel would be pretty challenging if we were stuck with whatever raw TCP socket a library had established for us.</p><p>The way we accomplish this relies on a mainstay of Rust: <a href="https://doc.rust-lang.org/book/ch10-02-traits.html"><u>traits</u></a> (which are how Rust lets developers apply logic across generic functions and types). In the Rust ecosystem, there are two traits that define the behavior Hyperdrive wants out of its transport layers: <a href="https://docs.rs/tokio/latest/tokio/io/trait.AsyncRead.html"><code><u>AsyncRead</u></code></a> and <a href="https://docs.rs/tokio/latest/tokio/io/trait.AsyncWrite.html"><code><u>AsyncWrite</u></code></a>. There are a couple of others we also need, but we’re going to focus on just these two. These traits enable us to code our entire custom handler against a generic stream of data, without the handler needing to know anything about the underlying protocol used to implement the stream. So, we can pass around a WebSocket connection as a generic I/O stream, wherever it might be needed.</p><p>As an example, the code to create a generic TCP stream and send a Postgres startup message across it might look like this:</p>
            <pre><code>/// Send a startup message to a Postgres server, in the role of a PG client.
/// https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-STARTUPMESSAGE
pub async fn send_startup&lt;S&gt;(stream: &amp;mut S, user_name: &amp;str, db_name: &amp;str, app_name: &amp;str) -&gt; Result&lt;(), ConnectionError&gt;
where
    S: AsyncWrite + Unpin,
{
    let protocol_number = 196608 as i32;
    let user_str = &amp;b"user\0"[..];
    let user_bytes = user_name.as_bytes();
    let db_str = &amp;b"database\0"[..];
    let db_bytes = db_name.as_bytes();
    let app_str = &amp;b"application_name\0"[..];
    let app_bytes = app_name.as_bytes();
    let len = 4 + 4
        + user_str.len() + user_bytes.len() + 1
        + db_str.len() + db_bytes.len() + 1
        + app_str.len() + app_bytes.len() + 1 + 1;

    // Construct a BytesMut of our startup message, then send it
    let mut startup_message = BytesMut::with_capacity(len as usize);
    startup_message.put_i32(len as i32);
    startup_message.put_i32(protocol_number);
    startup_message.put(user_str);
    startup_message.put_slice(user_bytes);
    startup_message.put_u8(0);
    startup_message.put(db_str);
    startup_message.put_slice(db_bytes);
    startup_message.put_u8(0);
    startup_message.put(app_str);
    startup_message.put_slice(app_bytes);
    startup_message.put_u8(0);
    startup_message.put_u8(0);

    match stream.write_all(&amp;startup_message).await {
        Ok(_) =&gt; Ok(()),
        Err(err) =&gt; {
            error!("Error writing startup to server: {}", err.to_string());
            ConnectionError::InternalError
        }
    }
}

/// Connect to a TCP socket
let stream = match TcpStream::connect(("localhost", 5432)).await {
    Ok(s) =&gt; s,
    Err(err) =&gt; {
        error!("Error connecting to address: {}", err.to_string());
        return ConnectionError::InternalError;
    }
};
let _ = send_startup(&amp;mut stream, "db_user", "my_db").await;</code></pre>
            <p>With this approach, if we wanted to encrypt the stream using <a href="https://www.cloudflare.com/learning/ssl/transport-layer-security-tls/#:~:text=Transport%20Layer%20Security%2C%20or%20TLS,web%20browsers%20loading%20a%20website."><u>TLS</u></a> before we write to it (upgrading our existing <code>TcpStream</code> connection in-place, to an <code>SslStream</code>), we would only have to change the code we use to create the stream, while generating and sending the traffic would remain unchanged. This is because <code>SslStream</code> also implements <code>AsyncWrite</code>!</p>
            <pre><code>/// We're handwaving the SSL setup here. You're welcome.
let conn_config = new_tls_client_config()?;

/// Encrypt the TcpStream, returning an SslStream
let ssl_stream = match tokio_boring::connect(conn_config, domain, stream).await {
    Ok(s) =&gt; s,
    Err(err) =&gt; {
        error!("Error during websocket TLS handshake: {}", err.to_string());
        return ConnectionError::InternalError;
    }
};
let _ = send_startup(&amp;mut ssl_stream, "db_user", "my_db").await;</code></pre>
            
    <div>
      <h2>Whence WebSocket</h2>
      <a href="#whence-websocket">
        
      </a>
    </div>
    <p><a href="https://datatracker.ietf.org/doc/html/rfc6455"><u>WebSocket</u></a> is an application layer protocol that enables bidirectional communication between a client and server. Typically, to establish a WebSocket connection, a client initiates an HTTP request and indicates they wish to upgrade the connection to WebSocket via the “Upgrade” header. Then, once the client and server complete the handshake, both parties can send messages over the connection until one of them terminates it.</p><p>Now, it turns out that the way Cloudflare Tunnels work under the hood is that both ends of the tunnel want to speak WebSocket, and rely on a translation layer to convert all traffic to or from WebSocket. The <code>cloudflared</code> daemon you spin up within your private network handles this for us! For Hyperdrive, however, we did not have a suitable translation layer to send Postgres messages across WebSocket, and had to write one.</p><p>One of the (many) fantastic things about Rust traits is that the contract they present is very clear. To be <code>AsyncRead</code>, you just need to implement <code>poll_read</code>. To be <code>AsyncWrite</code>, you need to implement only three functions (<code>poll_write</code>, <code>poll_flush</code>, and <code>poll_shutdown</code>). Further, there is excellent support for WebSocket in Rust built on top of the <a href="https://github.com/snapview/tungstenite-rs"><u>tungstenite-rs library</u></a>.</p><p>Thus, building our custom WebSocket stream such that it can share the same machinery as all our other generic streams just means translating the existing WebSocket support into these poll functions. There are some existing OSS projects that do this, but for multiple reasons we could not use the existing options. The primary reason is that Hyperdrive operates across multiple threads (thanks to the <a href="https://docs.rs/tokio/latest/tokio/runtime/index.html"><u>tokio runtime</u></a>), and so we rely on our connections to also handle <a href="https://doc.rust-lang.org/std/marker/trait.Send.html"><code><u>Send</u></code></a>, <a href="https://doc.rust-lang.org/std/marker/trait.Sync.html"><code><u>Sync</u></code></a>, and <a href="https://doc.rust-lang.org/std/marker/trait.Unpin.html"><code><u>Unpin</u></code></a>. None of the available solutions had all five traits handled. It turns out that most of them went with the paradigm of <a href="https://docs.rs/futures/latest/futures/sink/trait.Sink.html"><code><u>Sink</u></code></a> and <a href="https://docs.rs/futures/latest/futures/stream/trait.Stream.html"><code><u>Stream</u></code></a>, which provide a solid base from which to translate to <code>AsyncRead</code> and <code>AsyncWrite</code>. In fact some of the functions overlap, and can be passed through almost unchanged. For example, <code>poll_flush</code> and <code>poll_shutdown</code> have 1-to-1 analogs, and require almost no engineering effort to convert from <code>Sink</code> to <code>AsyncWrite</code>.</p>
            <pre><code>/// We use this struct to implement the traits we need on top of a WebSocketStream
pub struct HyperSocket&lt;S&gt;
where
    S: AsyncRead + AsyncWrite + Send + Sync + Unpin,
{
    inner: WebSocketStream&lt;S&gt;,
    read_state: Option&lt;ReadState&gt;,
    write_err: Option&lt;Error&gt;,
}

impl&lt;S&gt; AsyncWrite for HyperSocket&lt;S&gt;
where
    S: AsyncRead + AsyncWrite + Send + Sync + Unpin,
{
    fn poll_flush(mut self: Pin&lt;&amp;mut Self&gt;, cx: &amp;mut Context&lt;'_&gt;) -&gt; Poll&lt;io::Result&lt;()&gt;&gt; {
        match ready!(Pin::new(&amp;mut self.inner).poll_flush(cx)) {
            Ok(_) =&gt; Poll::Ready(Ok(())),
            Err(err) =&gt; Poll::Ready(Err(Error::new(ErrorKind::Other, err))),
        }
    }

    fn poll_shutdown(mut self: Pin&lt;&amp;mut Self&gt;, cx: &amp;mut Context&lt;'_&gt;) -&gt; Poll&lt;io::Result&lt;()&gt;&gt; {
        match ready!(Pin::new(&amp;mut self.inner).poll_close(cx)) {
            Ok(_) =&gt; Poll::Ready(Ok(())),
            Err(err) =&gt; Poll::Ready(Err(Error::new(ErrorKind::Other, err))),
        }
    }
}
</code></pre>
            <p>With that translation done, we can use an existing WebSocket library to upgrade our <code>SslStream</code> connection to a Cloudflare Tunnel, and wrap the result in our <code>AsyncRead/AsyncWrite</code> implementation. The result can then be used anywhere that our other transport streams would work, without any changes needed to the rest of our codebase! </p><p>That would look something like this:</p>
            <pre><code>let websocket = match tokio_tungstenite::client_async(request, ssl_stream).await {
    Ok(ws) =&gt; Ok(ws),
    Err(err) =&gt; {
        error!("Error during websocket conn setup: {}", err.to_string());
        return ConnectionError::InternalError;
    }
};
let websocket_stream = HyperSocket::new(websocket));
let _ = send_startup(&amp;mut websocket_stream, "db_user", "my_db").await;</code></pre>
            
    <div>
      <h2>Access granted</h2>
      <a href="#access-granted">
        
      </a>
    </div>
    <p>An observant reader might have noticed that in the code example above we snuck in a variable named request that we passed in when upgrading from an <code>SslStream to a WebSocketStream</code>. This is for multiple reasons. The first reason is that Tunnels are assigned a hostname and use this hostname for routing. The second and more interesting reason is that (as mentioned above) when negotiating an upgrade from HTTP to WebSocket, a request must be sent to the server hosting the ingress side of the Tunnel to <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism"><u>perform the upgrade</u></a>. This is pretty universal, but we also add in an extra piece here.</p><p>At Cloudflare, we believe that <a href="https://blog.cloudflare.com/secure-by-default-understanding-new-cisa-guide/"><u>secure defaults</u></a> and <a href="https://www.cloudflare.com/learning/security/glossary/what-is-defense-in-depth/"><u>defense in depth</u></a> are the correct ways to build a better Internet. This is why traffic across Tunnels is encrypted, for example. However, that does not necessarily prevent unwanted traffic from being sent into your Tunnel, and therefore egressing out to your database. While Postgres offers a robust set of <a href="https://www.postgresql.org/docs/current/user-manag.html"><u>access control</u></a> options for protecting your database, wouldn’t it be best if unwanted traffic never got into your private network in the first place? </p><p>To that end, all <a href="https://developers.cloudflare.com/hyperdrive/configuration/connect-to-private-database/"><u>Tunnels set up for use with Hyperdrive</u></a> should have a <a href="https://developers.cloudflare.com/cloudflare-one/applications/"><u>Zero Trust Access Application</u></a> configured to protect them. These applications should use a <a href="https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/"><u>Service Token</u></a> to authorize connections. When setting up a new Hyperdrive, you have the option to provide the token’s ID and Secret, which will be encrypted and stored alongside the rest of your configuration. These will be presented as part of the WebSocket upgrade request to authorize the connection, allowing your database traffic through while preventing unwanted access.</p><p>This can be done within the request’s headers, and might look something like this:</p>
            <pre><code>let ws_url = format!("wss://{}", host);
let mut request = match ws_url.into_client_request() {
    Ok(req) =&gt; req,
    Err(err) =&gt; {
        error!(
            "Hostname {} could not be parsed into a valid request URL: {}", 
            host,
            err.to_string()
        );
        return ConnectionError::InternalError;
    }
};
request.headers_mut().insert(
    "CF-Access-Client-Id",
    http::header::HeaderValue::from_str(&amp;client_id).unwrap(),
);
request.headers_mut().insert(
    "CF-Access-Client-Secret",
    http::header::HeaderValue::from_str(&amp;client_secret).unwrap(),
);
</code></pre>
            
    <div>
      <h2>Building for customer zero</h2>
      <a href="#building-for-customer-zero">
        
      </a>
    </div>
    <p>If you’ve been reading the blog for a long time, some of this might sound a bit familiar.  This isn’t the first time that we’ve <a href="https://blog.cloudflare.com/cloudflare-tunnel-for-postgres/"><u>sent Postgres traffic across a tunnel</u></a>, it’s something most of us do from our laptops regularly.  This works very well for interactive use cases with low traffic volume and a high tolerance for latency, but historically most of our products have not been able to employ the same approach.</p><p>Cloudflare operates <a href="https://www.cloudflare.com/network/"><u>many data centers</u></a> around the world, and most services run in every one of those data centers. There are some tasks, however, that make the most sense to run in a more centralized fashion. These include tasks such as managing control plane operations, or storing configuration state.  Nearly every Cloudflare product houses its control plane information in <a href="https://blog.cloudflare.com/performance-isolation-in-a-multi-tenant-database-environment/"><u>Postgres clusters</u></a> run centrally in a handful of our data centers, and we use a variety of approaches for accessing that centralized data from elsewhere in our network. For example, many services currently use a push-based model to publish updates to <a href="https://blog.cloudflare.com/moving-quicksilver-into-production/"><u>Quicksilver</u></a>, and work through the complexities implied by such a model. This has been a recurring challenge for any team looking to build a new product.</p><p>Hyperdrive’s entire reason for being is to make it easy to access such central databases from our global network. When we began exploring Tunnel integrations as a feature, many internal teams spoke up immediately and strongly suggested they’d be interested in using it themselves. This was an excellent opportunity for Cloudflare to scratch its own itch, while also getting a lot of traffic on a new feature before releasing it directly to the public. As always, being “customer zero” means that we get fast feedback, more reliability over time, stronger connections between teams, and an overall better suite of products. We jumped at the chance.</p><p>As we rolled out early versions of Tunnel integration, we worked closely with internal teams to get them access to it, and fixed any rough spots they encountered. We’re pleased to share that this first batch of teams have found great success building new or <a href="https://www.cloudflare.com/learning/cloud/how-to-refactor-applications/">refactored</a> products on Hyperdrive over Tunnels. For example: if you’ve already tried out <a href="https://blog.cloudflare.com/builder-day-2024-announcements/#continuous-integration-and-delivery"><u>Workers Builds</u></a>, or recently <a href="https://www.cloudflare.com/trust-hub/reporting-abuse/"><u>submitted an abuse report</u></a>, you’re among our first users!  At the time of this writing, we have several more internal teams working to onboard, and we on the Hyperdrive team are very excited to see all the different ways in which fast and simple connections from Workers to a centralized database can help Cloudflare just as much as they’ve been helping our external customers.</p>
    <div>
      <h2>Outro</h2>
      <a href="#outro">
        
      </a>
    </div>
    <p>Cloudflare is on a mission to make the Internet faster, safer, and more reliable. Hyperdrive was built to make connecting to centralized databases from the Workers runtime as quick and consistent as possible, and this latest development is designed to help all those who want to use Hyperdrive without directly exposing resources within their virtual private clouds (VPCs) on the public web.</p><p>To this end, we chose to build a solution around our suite of industry-leading <a href="https://developers.cloudflare.com/cloudflare-one/"><u>Zero Trust</u></a> tools, and were delighted to find how simple it was to implement in our runtime given the power and extensibility of the Rust <code>trait</code> system. </p><p>Without waiting for the ink to dry, multiple teams within Cloudflare have adopted this new feature to quickly and easily solve what have historically been complex challenges, and are happily operating it in production today.</p><p>And now, if you haven't already, try <a href="https://developers.cloudflare.com/hyperdrive/configuration/connect-to-private-database/"><u>setting up Hyperdrive across a Tunnel</u></a>, and let us know what you think in the <a href="https://discord.com/channels/595317990191398933/1150557986239021106"><u>Hyperdrive Discord channel</u></a>!</p> ]]></content:encoded>
            <category><![CDATA[Developer Platform]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Hyperdrive]]></category>
            <category><![CDATA[Postgres]]></category>
            <category><![CDATA[SQL]]></category>
            <category><![CDATA[Rust]]></category>
            <category><![CDATA[WebSockets]]></category>
            <guid isPermaLink="false">5GK429XQHhFzVyKSXGZ2R6</guid>
            <dc:creator>Andrew Repp</dc:creator>
            <dc:creator>Emilio Assunção</dc:creator>
            <dc:creator>Abhishek Chanda</dc:creator>
        </item>
        <item>
            <title><![CDATA[Supporting Postgres Named Prepared Statements in Hyperdrive]]></title>
            <link>https://blog.cloudflare.com/postgres-named-prepared-statements-supported-hyperdrive/</link>
            <pubDate>Fri, 28 Jun 2024 13:00:09 GMT</pubDate>
            <description><![CDATA[ Hyperdrive (Cloudflare’s globally distributed SQL connection pooler and cache) recently added support for Postgres protocol-level named prepared statements across pooled connections. We dive deep on what it took to add this feature ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Hyperdrive (Cloudflare’s globally distributed SQL connection pooler and cache) recently added support for Postgres protocol-level named prepared statements across pooled connections. Named prepared statements allow Postgres to cache query execution plans, providing potentially substantial performance improvements. Further, many popular drivers in the ecosystem use these by default, meaning that not having them is a bit of a footgun for developers. We are very excited that Hyperdrive’s users will now have access to better performance and a more seamless development experience, without needing to make any significant changes to their applications!</p><p>While we're not the first connection pooler to add this support (<a href="https://www.pgbouncer.org/">PgBouncer</a> got to it in October 2023 in <a href="https://github.com/pgbouncer/pgbouncer/releases/tag/pgbouncer_1_21_0">version 1.21</a>, for example), there were some unique challenges in how we implemented it. To that end, we wanted to do a deep dive on what it took for us to deliver this.</p>
    <div>
      <h3>Hyper-what?</h3>
      <a href="#hyper-what">
        
      </a>
    </div>
    <p>One of the classic problems of building on the web is that your users are everywhere, but your database tends to be in one spot.  Combine that with pesky limitations like network routing, or the speed of light, and you can often run into situations where your users feel the pain of having your database so far away. This can look like slower queries, slower startup times, and connection exhaustion as everything takes longer to accomplish.</p><p><a href="/hyperdrive-making-regional-databases-feel-distributed">Hyperdrive</a> is designed to make the centralized databases you already have feel like they’re global. We use our <a href="https://www.cloudflare.com/network/">global network</a> to get faster routes to your database, keep connection pools primed, and cache your most frequently run queries as close to users as possible.</p>
    <div>
      <h3>Postgres Message Protocol</h3>
      <a href="#postgres-message-protocol">
        
      </a>
    </div>
    <p>To understand exactly what the challenge with prepared statements is, it's first necessary to dig in a bit to the <a href="https://www.postgresql.org/docs/current/protocol-flow.html">Postgres Message Protocol</a>. Specifically, we are going to take a look at the protocol for an “extended” query, which uses different message types and is a bit more complex than a “simple” query, but which is more powerful and thus more widely used.</p><p>A query using Hyperdrive might be coded something like this, but a lot goes on under the hood in order for Postgres to reliably return your response.</p>
            <pre><code>import postgres from "postgres";

// with Hyperdrive, we don't have to disable prepared statements anymore!
// const sql = postgres(env.HYPERDRIVE.connectionString, {prepare: false});

// make a connection, with the default postgres.js settings (prepare is set to true)
const sql = postgres(env.HYPERDRIVE.connectionString);

// This sends the query, and while it looks like a single action it contains several 
// messages implied within it
let [{ a, b, c, id }] = await sql`SELECT a, b, c, id FROM hyper_test WHERE id = ${target_id}`;</code></pre>
            <p>To prepare a statement, a Postgres client begins by sending a <i>Parse</i> message. This includes the query string, the number of parameters to be interpolated, and the statement's name. The name is a key piece of this puzzle. If it is empty, then Postgres uses a special "unnamed" prepared statement slot that gets overwritten on each new <i>Parse</i>. These are relatively easy to support, as most drivers will keep the entirety of a message sequence for unnamed statements together, and will not try to get too aggressive about reusing the prepared statement because it is overwritten so often.</p><p>If the statement has a name, however, then it is kept prepared for the remainder of the Postgres session (unless it is explicitly removed with <i>DEALLOCATE</i>). This is convenient because parsing a query string and preparing the statement costs bytes sent on the wire and CPU cycles to process, so reusing a statement is quite a nice optimization.</p><p>Once done with <i>Parse</i>, there are a few remaining steps to (the simplest form of) an extended query:</p><ul><li><p>A <i>Bind</i> message, which provides the specific values to be passed for the parameters in the statement (if any).</p></li><li><p>An <i>Execute</i> message, which tells the Postgres server to actually perform the data retrieval and processing.</p></li><li><p>And finally a <i>Sync</i> message, which causes the server to close the implicit transaction, return results, and provides a synchronization point for error handling.</p></li></ul><p>While that is the core pattern for accomplishing an extended protocol query, there are many more complexities possible (named <i>Portal</i>, <i>ErrorResponse</i>, etc.).</p><p>We will briefly mention one other complexity we often encounter in this protocol, which is <i>Describe</i> messages. Many drivers leverage Postgres’ built-in types to help with deserialization of the results into structs or classes. This is accomplished by sending a <i>Parse-Describe-Flush/Sync</i> sequence, which will send a statement to be prepared, and will expect back information about the types and data the query will return. This complicates bookkeeping around named prepared statements, as now there are two separate queries, with two separate kinds of responses, that must be kept track of. We won’t go into much depth on the tradeoffs of an additional round-trip in exchange for advanced information about the results’ format, but suffice it to say that it must be handled explicitly in order for the overall system to gracefully support prepared statements.</p><p>So the basic query from our code above looks like this from a message perspective:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5ztflSTbecT9o4QU3YLDW3/4ee2003396e5b15a15dd2cb63cdd2711/unnamed-4.png" />
            
            </figure><p>A <a href="https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY">more complete description</a> and the <a href="https://www.postgresql.org/docs/current/protocol-message-formats.html">full structure of each message type</a> are well described in the Postgres documentation.</p><p>So, what's so hard about that?</p>
    <div>
      <h3>Buffering Messages</h3>
      <a href="#buffering-messages">
        
      </a>
    </div>
    <p>The first challenge that Hyperdrive must solve (that many other connection poolers don't have) is that it's also a cache.</p><p>The happiest path for a query on Hyperdrive never travels far, and we are quite proud of the low latency of our cache hits. However, this presents a particular challenge in the case of an extended protocol query. A <i>Parse</i> by itself is insufficient as a cache key, both because the parameter values in the <i>Bind</i> messages can alter the expected results, and because it might be followed up with either a <i>Describe</i> or an <i>Execute</i> message which will invoke drastically different responses.</p><p>So Hyperdrive cannot simply pass each message to the origin database, as we must buffer them in a message log until we have enough information to reliably distinguish between cache keys. It turns out that receiving a <i>Sync</i> is quite a natural point at which to check whether you have enough information to serve a response. For most scenarios, we buffer until we receive a <i>Sync</i>, and then (assuming the scenario is cacheable) we determine whether we can serve the response from cache or we need to take a connection to the origin database.</p>
    <div>
      <h3>Taking a Connection From the Pool</h3>
      <a href="#taking-a-connection-from-the-pool">
        
      </a>
    </div>
    <p>Assuming we aren't serving a response from cache, for whatever reason, we'll need to take an origin connection from our pool. One of the key advantages any connection pooler offers is in allowing many client connections to share few database connections, so minimizing how often and for how long these connections are held is crucial to making Hyperdrive performant.</p><p>To this end, <a href="https://developers.cloudflare.com/hyperdrive/configuration/how-hyperdrive-works/#connection-pooling">Hyperdrive operates</a> in what is traditionally called “transaction mode”. This means that a connection taken from the pool for any given transaction is returned once that transaction concludes. This is in contrast to what is often called “session mode”, where once a connection is taken from the pool it is held by the client until the client disconnects.</p><p>For Hyperdrive, allowing any client to take any database connection is vital. This is because if we "pin" a client to a given database connection then we have one fewer available for every other possible client. You can run yourself out of database connections very quickly once you start down that path, especially when your clients are many small Workers spread around the world.</p><p>The challenge prepared statements present to this scenario is that they exist at the "session" scope, which is to say, at the scope of one connection. If a client prepares a statement on connection A, but tries to reuse it and gets assigned connection B, Postgres will naturally throw an error claiming the statement doesn't exist in the given session. No results will be returned, the client is unhappy, and all that's left is to retry with a <i>Parse</i> message included. This causes extra round-trips between client and server, defeating the whole purpose of what is meant to be an optimization.</p><p>One of the goals of a connection pooler is to be as transparent to the client and server as possible. There are limitations, as Postgres will let you do some powerful things to session state that cannot be reasonably shared across arbitrary client connections, but to the extent possible the endpoints should not have to know or care about any multiplexing happening between them.</p><p>This means that when a client sends a <i>Parse</i> message on its connection, it should expect that the statement will be available for reuse when it wants to send a <i>Bind-Execute-Sync</i> sequence later on. It also means that the server should not get <i>Bind</i> messages for statements that only exist on some other session. Maintaining this illusion is the crux of providing support for this feature.</p>
    <div>
      <h3>Putting it all together</h3>
      <a href="#putting-it-all-together">
        
      </a>
    </div>
    <p>So, what does the solution look like? If a client sends <i>Parse-Bind-Execute-Sync</i> with a named prepared statement, then later sends <i>Bind-Execute-Sync</i> to reuse it, how can we make sure that everything happens as expected? The solution, it turns out, needs just a few built-in Rust data structures for efficiently capturing what we need (a <a href="https://doc.rust-lang.org/std/collections/struct.HashMap.html">HashMap</a>, some <a href="https://docs.rs/lru/latest/lru/struct.LruCache.html"><i>LruCaches</i></a> and a <a href="https://doc.rust-lang.org/std/collections/struct.VecDeque.html">VecDeque</a>), and some straightforward business logic to keep track of when to intervene in the messages being passed back and forth.</p><p>Whenever a named <i>Parse</i> comes in, we store it in an in-memory <i>HashMap</i> on the server that handles message processing for that client’s connection. This persists until the client is disconnected. This means that whenever we see anything referencing the statement, we can go retrieve the complete message defining it. We'll come back to this in a moment.</p><p>Once we've buffered all the messages we can and gotten to the point where it's time to return results (let's say because the client sent a <i>Sync</i>), we need to start applying some logic. For the sake of brevity we're going to omit talking through error handling here, as it does add some significant complexity but is somewhat out of scope for this discussion.</p><p>There are two main questions that determine how we should proceed:</p><ol><li><p>Does our message sequence include a <i>Parse</i>, or are we trying to reuse a pre-existing statement?</p></li><li><p>Do we have a cache hit or are we serving from the origin database?</p></li></ol><p>This gives us four scenarios to consider:</p><ol><li><p><i>Parse</i> with cache hit</p></li><li><p><i>Parse</i> with cache miss</p></li><li><p>Reuse with cache hit</p></li><li><p>Reuse with cache miss</p></li></ol><p>A <i>Parse</i> with a cache hit is the easiest path to address, as we don't need to do anything special. We use the messages sent as a cache key, and serve the results back to the client. We will still keep the <i>Parse</i> in our <i>HashMap</i> in case we want it later (#2 below), but otherwise we're good to go.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/33CSY4vk0u6lYLkKBRW7XH/291ea17f55196c35caee8d29d0f733a6/unnamed--1--4.png" />
            
            </figure><p>A <i>Parse</i> with a cache miss is a bit more complicated, as now we need to send these messages to the origin server. We take a connection at random from our pool and do so, passing the results back to the client. With that, we've begun to make changes to session state such that all our database connections are no longer identical to each other. To keep track of what we've done to muddy up our state, we keep a <i>LruCache</i> on each connection of which statements it already has prepared. In the case where we need to evict from such a cache, we will also <i>DEALLOCATE</i> the statement on the connection to keep things tracked correctly.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3NFnGuHT6uAmr2dMRdp41m/a95422a38aa44e7720cbdcca7bd73513/unnamed--2--2.png" />
            
            </figure><p>Reuse with a cache hit is yet more tricky, but still straightforward enough. In the example below, we are sent a <i>Bind</i> with the same parameters twice (#1 and #9). We must identify that we received a <i>Bind</i> without a preceding <i>Parse</i>, we must go retrieve that <i>Parse</i> (#10), and we must use the information from it to build our cache key. Once all that is accomplished, we can serve our results from cache, needing only to trim out the <i>ParseComplete</i> within the cached results before returning them to the client.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3ksUndQnXpbjzu6Ggm3veo/9fa185c8c8a26829a1c4894efd24ccaa/unnamed--3--2.png" />
            
            </figure><p>Reuse with a cache miss is the hardest scenario, as it may require us to lie in both directions. In the example below, we cache results for one set of parameters (#8), but are sent a <i>Bind</i> with different parameters (#9). As in the cache hit scenario, we must identify that we were not sent a <i>Parse</i> as part of the current message sequence, retrieve it from our <i>HashMap</i> (#10), and build our cache key to GET from cache and confirm the miss (#11). Once we take a connection from the pool, though, we then need to check if it already has the statement we want prepared. If not, we must take our saved <i>Parse</i> and prepend it to our message log to be sent along to the origin database (#13). Thus, what the server receives looks like a perfectly valid <i>Parse-Bind-Execute-Sync</i> sequence. This is where our <i>VecDeque</i> (mentioned above) comes in, as converting our message log to that structure allowed us to very ergonomically make such changes without needing to rebuild the whole byte sequence. Once we receive the response from the server, all that's needed is to trim out the initial <i>ParseComplete</i> response from the server, as a well-made client would likely be very confused receiving such a response to a <i>Parse</i> it didn't send. With that message trimmed out, however, the client is in the position of getting exactly what it asked for, and both sides of the conversation are happy.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/lWQHcpash88r4sjmT3Thq/993c69dfe03c4a8bb0c4e472df2b4a7a/unnamed--4--1.png" />
            
            </figure>
    <div>
      <h3>Dénouement</h3>
      <a href="#denouement">
        
      </a>
    </div>
    <p>Now that we've got a working solution, where all parties are functioning well, let's review! Our solution lets us share database connections across arbitrary clients with no "pinning", no custom handling on either client or server, and supports reuse of prepared statements to reduce CPU load on re-parsing queries and reduce network traffic on re-sending <i>Parse</i> messages. Engineering always involves tradeoffs, so the cost of this is that we will sometimes still need to sneak in a <i>Parse</i> because a client got assigned a different connection on reuse, and in those scenarios there is a small amount of additional memory overhead because the same statement is prepared on multiple connections.</p><p>And now, if you haven't already, go give <a href="https://developers.cloudflare.com/hyperdrive/">Hyperdrive</a> a spin, and let us know what you think in the <a href="https://discord.com/channels/595317990191398933/1150557986239021106">Hyperdrive Discord channel</a>!</p> ]]></content:encoded>
            <category><![CDATA[Developer Platform]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Hyperdrive]]></category>
            <category><![CDATA[Postgres]]></category>
            <category><![CDATA[SQL]]></category>
            <category><![CDATA[Message Protocol]]></category>
            <category><![CDATA[Prepared Statements]]></category>
            <guid isPermaLink="false">65jmeFCIBZN3YdPbpEwmY1</guid>
            <dc:creator>Andrew Repp</dc:creator>
        </item>
        <item>
            <title><![CDATA[Thwarting the Tactics of the Equifax Attackers]]></title>
            <link>https://blog.cloudflare.com/thwarting-the-tactics-of-the-equifax-attackers/</link>
            <pubDate>Mon, 13 Nov 2017 16:09:00 GMT</pubDate>
            <description><![CDATA[ We are now 3 months on from one of the biggest, most significant data breaches in history, but has it redefined people's awareness on security? ]]></description>
            <content:encoded><![CDATA[ <p>We are now 3 months on from one of the biggest, most significant data breaches in history, but has it redefined people's awareness on security?</p><p>The answer to that is absolutely yes, awareness is at an all-time high. Awareness, however, does not always result in positive action. The fallacy which is often assumed is "surely, if I keep my software up to date with all the patches, that's more than enough to keep me safe?". It's true, keeping software up to date does defend against known vulnerabilities, but it's a very reactive stance. The more important part is protecting against the unknown.</p><p>Something every engineer will agree on is that security is hard, and maintaining systems is even harder. Patching or upgrading systems can lead to unforeseen outages or unexpected behaviour due to other fixes which may be applied. This, in most cases, can cause huge delays in the deployment of patches or upgrades, due to requiring either regression testing or deployment in a staging environment. Whilst processes are followed, and tests are done, systems are sat vulnerable, ready to be exploited if they are exposed to the internet.</p><p>Looking at the wider landscape, an increase in security research has created a surge of CVEs (Common Vulnerability and Exposures) being announced. This compounded by GDPR, NIST and other new data protection legislation, businesses are now forced to pay much more attention to security vulnerabilities that potentially could affect their software, and ultimately put them on the forever growing list of victims of data breaches.</p>
            <figure>
            <a href="https://public.tableau.com/profile/nela7296#!/vizhome/CommonVulnerabilitiesandExposurescvedetails_com/Sheet2">
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/8XeikDcpPYgT2tFoIN9IT/fc5083fcf76036e99e0a5500a030851d/cve2.png" />
            </a>
            </figure><p>Dissecting the Equifax tragedy, in testimony from the CEO, he mentions that the reason for the breach was that there was one single person within the organisation who was responsible for communicating the availability of the patch for Apache Struts, the software at the heart of the breach. The crucial lesson learned from Equifax is that we are all human, and that mistakes can happen, however having multiple people responsible for communicating and notifying teams about threats is crucial. In this case, the mistake almost destroyed one of the largest credit agencies in the world.</p><p>How could attacks and breaches like Equifax be avoided? First is about understanding how these attacks happen. There are some key attacks which are often the source of <a href="https://www.cloudflare.com/learning/security/what-is-data-exfiltration/">data exfiltration</a> through vulnerable software.</p><ul><li><p>Remote Code Execution (RCE) - which is what was used in the Equifax Breach</p></li><li><p>SQL Injection (SQLi), which is delivering an SQL statement hidden in a payload, accessing a backend database powering a website.</p></li></ul>
    <div>
      <h3>Remote Code Execution</h3>
      <a href="#remote-code-execution">
        
      </a>
    </div>
    <p>The Struts vulnerability, <a href="https://nvd.nist.gov/vuln/detail/CVE-2017-5638">CVE-2017-5638</a>, which is protected by rule 100054 in Cloudflare Specials, was quite simple. In a payload targeted at the web-server, a specific command could be executed which can be seen in the example below:</p>
            <pre><code>"(#context.setMemberAccess(#dm))))."
"(#cmd='touch /tmp/hacked')."
"(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))."
"(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))."
"(#p=new java.lang.ProcessBuilder(#cmds))."</code></pre>
            <p>More critically however, further to this CVE, Apache Struts also announced another vulnerability earlier this year (<a href="https://nvd.nist.gov/vuln/detail/CVE-2017-9805">CVE-2017-9805</a>), which works by delivering a payload against the REST plugin combined with the Xstream handler, which provides an XML ingest capability. By delivering an specially crafted XML payload, a shell command can be embedded, and will be executed.</p>
            <pre><code>&lt;next class="java.lang.ProcessBuilder"&gt;
   &lt;command&gt;
      "touch /tmp/hacked".
   &lt;/command&gt;
   &lt;redirectErrorStream&gt;false&lt;/redirectErrorStream&gt;
&lt;/next&gt;</code></pre>
            <p>And the result from the test:</p>
            <pre><code>root@struts-demo:~$ ls /tmp
hacked
root@struts-demo:~$</code></pre>
            <p>In the last week, we have seen over 180,000 hits on our WAF rules protecting against Apache Struts across the Cloudflare network.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/IPzn62W8nzdCNR4F8if95/debceba1c6e4582142c2854525567ddc/Struts-1.png" />
            
            </figure>
    <div>
      <h3>SQL Injection</h3>
      <a href="#sql-injection">
        
      </a>
    </div>
    <p>SQL Injection (SQLi) is an attempt to inject nefarious queries into a GET or POST dynamic variable, which is used to query a database. Cloudflare, on a day to day basis will see over 2.3m SQLi attempts on our network. Most commonly, we see SQLi attacks against Wordpress sites, as it is one of the biggest web applications used on Cloudflare today. Wordpress is used by some of the world's giants, like Sony Music, all the way down to "mom &amp; pop" businesses. The challenge with being the leader in the space, is you then become a hot target. Looking at the CVE list as we near the close of 2017, there have been 41 vulnerabilities found in multiple versions of Wordpress which would force people to upgrade to the latest versions. To protect our customers, and buy them time to upgrade, Cloudflare works with a number of vendors to address vulnerabilities and then virtual-patching using our WAF to <a href="https://www.cloudflare.com/learning/security/threats/how-to-prevent-sql-injection/">prevent these vulnerabilities being exploited</a>.</p><p>The way a SQL injection works is by "breaking out" or malforming a query when a web application is needing data from a database. As an example, a Forgotten Password page has a single email input field, which will be used to validate whether the username exists, and if so, sends the user a “Forgotten Password” link. Below is a straightforward SQL query example, which could be used in a web application:</p>
            <pre><code>SELECT user, password FROM users WHERE user = 'john@smith';</code></pre>
            <p>Which results in:</p>
            <pre><code>+------------+------------------------------+
|    user   |           password           |
+------------+------------------------------+
| john@smith | $2y$10$h9XJRX.EBnGFrWQlnt... |
+------------+------------------------------+</code></pre>
            <p>Without the right query validation, an attacker could escape out of this query, and carry out some extremely malicious queries. For example, if an attacker was looking to <a href="https://www.cloudflare.com/zero-trust/solutions/account-takeover-prevention/">takeover another user’s account</a>, and an attacker found that the query validation was inadequate, he could escape the query, and UPDATE the username, which is an email address in this instance, to his own. This can simply be done by entering the query string below into the email input field, instead of an email address.</p>
            <pre><code>dontcare@bla.com’;UPDATE users SET user = ‘mr@robot’ WHERE user = ‘john@smith’;</code></pre>
            
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6xkZ2dHeg7dLuOI7TUoyfS/46bc35df59150c95d70e8be3e6d1a035/emaileg.png" />
            
            </figure><p>Due to the lack of validation, the query which the web application sends to the database will be:</p>
            <pre><code>SELECT user, password FROM users WHERE user = 'dontcare@bla.com’;UPDATE users SET user = ‘mr@robot’ WHERE user = ‘john@smith’;</code></pre>
            <p>Now this has been updated, the attacker can now request a password reset using his own email address, gaining him access to the victim’s account.</p>
            <pre><code>+----------+------------------------------+
|  user   |           password           |
+----------+------------------------------+
| mr@robot | $2y$10$h9XJRX.EBnGFrWQlnt... |
+----------+------------------------------+</code></pre>
            <p>Many SQLi attacks are often on fields which are not often considered high risk, like an authentication form for example. To put the seriousness of SQLi attacks in perspective, in the last week, we have seen over 2.4 million matches.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4521BwLEn69dAUQR0W5sbC/4fc29447225748f0583fe55448c8f51d/SQLi-1.png" />
            
            </figure><p>The Cloudflare WAF is built to not only protect customers against SQLi and RCE based attacks, but also add <a href="https://www.cloudflare.com/learning/security/how-to-prevent-xss-attacks/">protection against Cross Site Scripting (XSS)</a> and a number of other known attacks. On an average week, just on our Cloudflare Specials WAF ruleset, we see over 138 million matches.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6T7PzjGJwXvJLNAk006MMf/4cfc9cb7f2c1ad9602526bef2ccfef2a/WAF.png" />
            
            </figure><p>The next important part is communication and awareness; understanding what you have installed, what versions you are running, and most importantly, what announcements your vendor is making. Generally, most notifications are received via email, and are usually quite cumbersome to digest, regardless of their complexity, it is crucial to try and understand them.</p><p>And, finally, the last line of defense is to have protection in front of your application, which is where Cloudflare can help. At Cloudflare, Security is very core to our values, and was one of the foundation pillars we were founded upon. Even to this day, we are known as one of the most cost effective ways of being able to shore up your Web Applications with just our Pro Plan@$20/mo.</p> ]]></content:encoded>
            <category><![CDATA[Privacy]]></category>
            <category><![CDATA[Attacks]]></category>
            <category><![CDATA[SQL]]></category>
            <category><![CDATA[Security]]></category>
            <category><![CDATA[Vulnerabilities]]></category>
            <guid isPermaLink="false">1fLOuMITKI0fs60V1oGinL</guid>
            <dc:creator>Alex Cruz Farmer</dc:creator>
        </item>
        <item>
            <title><![CDATA[The Sleepy User Agent]]></title>
            <link>https://blog.cloudflare.com/the-sleepy-user-agent/</link>
            <pubDate>Tue, 17 May 2016 13:07:33 GMT</pubDate>
            <description><![CDATA[ From time to time a customer writes in and asks about certain requests that have been blocked by the CloudFlare WAF. Recently, a customer couldn’t understand why it appeared that some simple GET requests for their homepage were listed as blocked in WAF analytics. ]]></description>
            <content:encoded><![CDATA[ <p>From time to time a customer writes in and asks about certain requests that have been blocked by the CloudFlare <a href="https://www.cloudflare.com/waf/">WAF</a>. Recently, a customer couldn’t understand why it appeared that some simple GET requests for their homepage were listed as blocked in WAF analytics.</p><p>A sample request looked liked this:</p>
            <pre><code>GET / HTTP/1.1
Host: www.example.com
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (compatible; MSIE 11.0; Windows NT 6.1; Win64; x64; Trident/5.0)'+(select*from(select(sleep(20)))a)+' 
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,fr;q=0.6</code></pre>
            <p>As I said, a simple request for the homepage of the web site, which at first glance doesn’t look suspicious at all. Unless your take a look at the <code>User-Agent</code> header (its value is the string that identifies the browser being used):</p>
            <pre><code>Mozilla/5.0 (compatible; MSIE 11.0; Windows NT 6.1; Win64; x64; Trident/5.0)'+(select*from(select(sleep(20)))a)+</code></pre>
            <p>The start looks reasonable (it’s apparently Microsoft Internet Explorer 11) but the agent strings ends with <code>'+(select*from(select(sleep(20)))a)+</code>. The attacker is attempting a <a href="https://en.wikipedia.org/wiki/SQL_injection">SQL injection</a> inside the <code>User-Agent</code> value.</p><p>It’s common to see SQL injection in URIs and form parameters, but here the attacker has hidden the SQL query <code>select * from (select(sleep(20)))</code> inside the <code>User-Agent</code> HTTP request header. This technique is commonly used by scanning tools; for example, <a href="http://sqlmap.org/">sqlmap</a> will try SQL injection against specific HTTP request headers with the <code>-p</code> option.</p>
    <div>
      <h3>You are getting very sleep</h3>
      <a href="#you-are-getting-very-sleep">
        
      </a>
    </div>
    <p>Many SQL injection attempts try to extract information from a website (such as the names of users, or their passwords, or other private information). This SQL statement is doing something different: it’s asking the database that’s processing the request to sleep for 20 seconds.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1rmqocwlPw8OrygkT6pNeT/6803912a90963ddff7241702192de824/4036024362_8752bea514_z.jpg" />
            
            </figure><p><a href="https://creativecommons.org/licenses/by-sa/2.0/">CC BY-SA 2.0</a> <a href="https://www.flickr.com/photos/vancouverlaser/4036024362/in/photolist-79DFqW-6bgKJ-hgsZxu-cp4c1Q-6miL6P-m4i1Xp-dxPs8E-8mCoyF-3BFYxy-kLLfcD-c12jub-niAXry-bAubec-ou2BkV-nom5Ly-k3Asnv-e5x4Lz-pYmu5c-nBWZAv-75BDDM-oBkwMY-8ZRzKS-h66fwy-p8adfb-s36TNQ-ejtuw9-nDUYFd-8uKeGF-qhtCL4-6RsCFn-3p1HAG-2MfZ7x-e5x65R-inNGRD-nAbCjW-h66xXW-awx1PJ-8iGb58-nTv2E6-p7cUnR-m4tfVT-nm4KwK-nxqyyb-pYrWxE-9J3x8y-nrB4VY-apoiVi-iMCsos-pQXzae-dUoEYW">image</a> by <a href="https://www.flickr.com/photos/vancouverlaser/">Dr Braun</a></p><p>This is a form of <a href="https://www.owasp.org/index.php/Blind_SQL_Injection">blind SQL injection</a>. In a common SQL injection the output of the SQL query would be returned to the attacker as part of a web page. But in a blind injection the attacker doesn’t get to see the output of their query and so they need some other way of determining that their injection worked.</p><p>Two common methods are to make the web server generate an error or to make it delay so that the response to the HTTP request comes back after a pause. The use of <code>sleep</code> means that the web server will take 20 seconds to respond and the attacker can be sure that a SQL injection is possible. Once they know it’s possible they can move onto a more sophisticated attack.</p>
    <div>
      <h3>Example</h3>
      <a href="#example">
        
      </a>
    </div>
    <p>To illustrate how this might work I created a really insecure application in PHP that records visits by saving the <code>User-Agent</code> to a MySQL database. This sort of code might exist in a real web application to save analytics information such as number of visits.</p><p>In this example, I’ve ignored all good security practices because I want to illustrate a working SQL injection.</p><p><b>BAD CODE: DO NOT COPY/PASTE MY CODE!</b></p><p>Here’s the PHP code:</p>
            <pre><code>&lt;?php

$link = new mysqli('localhost', 'insecure', '1ns3cur3p4ssw0rd', 'analytics');

$query = sprintf("INSERT INTO visits (ua, dt) VALUES ('%s', '%s')",
       $_SERVER["HTTP_USER_AGENT"],
       date("Y-m-d h:i:s"));

$link-&gt;query($query);

?&gt;

&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;b&gt;Thanks for visiting&lt;/b&gt;&lt;/body&gt;&lt;/html&gt;</code></pre>
            <p>It connects to a local MySQL database and selects the <code>analytics</code> database and then inserts the user agent of the visitor (which comes from the <code>User-Agent</code> HTTP header and is stored in <code>$_SERVER["HTTP_USER_AGENT"]</code>) into the database (along with the current date and time) without any sanitization at all!</p><p>This is ripe for a SQL injection, but because my code doesn’t report any errors the attacker won’t know they managed an injection without something like the sleep trick.</p>
            <figure>
            <a href="https://xkcd.com/327/">
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7MwWphAx0Xw3xAToFfDw6s/19d3e78edce9d149ce48e8fb6706e41d/static_qr_code_without_logo.jpg" />
            </a>
            </figure><p>To exploit this application it’s enough to do the following (where <code>insecure.php</code> is the script above):</p>
            <pre><code>curl -A "Mozilla/5.0', (select*from(select(sleep(20)))a)) #" http://example.com/insecure.php</code></pre>
            <p>This sets the <code>User-Agent</code> HTTP header to <code>Mozilla/5.0', (select*from(select(sleep(20)))a)) #</code>. The poor PHP code that creates the query just inserts this string into the middle of the SQL query without any sanitization so the query becomes:</p>
            <pre><code>INSERT INTO visits (ua, dt) VALUES ('Mozilla/5.0', (select*from(select(sleep(20)))a)) #', '2016-05-17 03:16:06')</code></pre>
            <p>The two values to be inserted are now <code>Mozilla/5.0</code> and the result of the subquery <code>(select*from(select(sleep(20)))a)</code> (which takes 20 seconds). The <code>#</code> means that the rest of the query (which contains the inserted date/time) is turned into a comment and ignored.</p><p>In the database an entry like this appears:</p>
            <pre><code>+---------------------+---------------+
| dt                  | ua            |
+---------------------+---------------+
| 0                   | Mozilla/5.0   |
+---------------------+---------------+</code></pre>
            <p>Notice how the date/time is <code>0</code> (the result of the <code>(select*from(select(sleep(20)))a)</code>) and the user agent is just <code>Mozilla/5.0</code>. Entries like that are likely the only indication that an attacker had succeeded with a SQL injection.</p><p>Here’s what the request looks like when it runs. I’ve used the <code>time</code> command to see how long the request takes to process.</p>
            <pre><code>$ time curl -v -A "Mozilla/5.0', (select*from(select(sleep(20)))a) #" http://example.com/insecure.php
* Connected to example.com port 80 (#0)
&gt; GET /insecure.php HTTP/1.1
&gt; Host: example.com
&gt; User-Agent: Mozilla/5.0', (select*from(select(sleep(20)))a) #
&gt; Accept: */*
&gt;
&lt; HTTP/1.1 200 OK
&lt; Date: Mon, 16 May 2016 10:45:05 GMT
&lt; Content-Type: text/html
&lt; Transfer-Encoding: chunked
&lt; Connection: keep-alive
&lt; Server: nginx

&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;b&gt;Thanks for visiting&lt;/b&gt;&lt;/body&gt;&lt;/html&gt;
* Connection #0 to host example.com left intact

real   0m20.614s
user   0m0.007s
sys    0m0.012s</code></pre>
            <p>It took 20 seconds. The SQL injection worked.</p>
    <div>
      <h3>Exploitation</h3>
      <a href="#exploitation">
        
      </a>
    </div>
    <p>At this point you might be thinking “that’s neat, but doesn’t seem to enable an attacker to hack the web site”.</p><p>Unfortunately, the richness of SQL means that this chink in the <code>insecure.php</code> code (a mere 3 lines of PHP!) lets an attacker go much further than just making a slow response happen. Even though the <code>INSERT INTO</code> query being attacked only writes to the database it’s possible to turn this around and extract information and gain access.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/LFivPXVG3iL4s9NNuc5zV/2f560073829c9541274b1b4ae258ee25/4813392151_410cf9a73b_z.jpg" />
            
            </figure><p><a href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0</a> <a href="https://www.flickr.com/photos/schill/4813392151/in/photolist-8kkTMR-bUTUKK-8FBu6H-pVRDno-7xXSxj-78ePNM-dEBQ1Z-r2ouJ-apHUxy-9yNrec-5A9bu9-pEvMok-5A4UmF-5GU2A9-dRweT2-9YVh9-5GU2zo-8PBLrM-8PBqxv-81AwZa-7w3oG3-7Nfb9c-got9Ti-dRezcB-6GNKrM-5A4Ub2-cdM6PJ-5A4Uwv-5GPJYg-D5coqS-5A9bEY-5GPJVX-8kKDCP-91N2sr-49XqW-3eKNbN-8ohQ4f-Cn1mG-ciNZdd-4CMtNC-DhQ2M-yoMGq-N1HP8-88YcCu-t2ruTe">image</a> by <a href="https://www.flickr.com/photos/schill/">Scott Schiller</a></p><p>As an illustration I created a table in the database called <code>users</code> containing a user called root and a user called <code>john</code>. Here’s how an attacker might discover that there is a <code>john</code> user. They can craft a query that works out the name of a user letter by letter just by looking at the time a request takes to return.</p><p>For example,</p>
            <pre><code>curl -A "Mozilla/5.0', (select sleep(20) from users where substring(name,1,1)='a')) #" http://example.com/insecure.php</code></pre>
            <p>returns immediately because there are no users with a name starting with <code>a</code>. But</p>
            <pre><code>curl -A "Mozilla/5.0', (select sleep(20) from users where substring(name,1,1)='j')) #" http://example.com/insecure.php</code></pre>
            <p>takes 20 seconds. The attacker can then try two letters, three letters, and so on. The same technique can be used to extract other data from the database.</p><p>If my web app was a little more sophisticated, say, for example, it was part of a blogging platform that allowed comments, it would be possible to use this vulnerability to dump the contents of an entire database table into a comment. The attacker could return and display the appropriate comment to read the table's contents. That way large amounts of data can be <a href="https://www.cloudflare.com/learning/security/what-is-data-exfiltration/">exfiltrated</a>.</p>
    <div>
      <h3>Securing my code</h3>
      <a href="#securing-my-code">
        
      </a>
    </div>
    <p>The better way to write the PHP code above is as follows:</p>
            <pre><code>&lt;?php

$link = new mysqli('localhost', 'analytics_user', 'aSecurePassword', 'analytics_db');

$stmt = $link-&gt;prepare("INSERT INTO visits (ua, dt) VALUES (?, ?)");
$stmt-&gt;bind_param("ss", $_SERVER["HTTP_USER_AGENT"], date("Y-m-d h:i:s"));
$stmt-&gt;execute();

?&gt;

&lt;html&gt;
&lt;head&gt;&lt;/head&gt;
&lt;body&gt;&lt;b&gt;Thanks for visiting&lt;/b&gt;&lt;/body&gt;</code></pre>
            <p>This prepares the SQL query to perform the insertion using <a href="https://secure.php.net/manual/en/mysqli.prepare.php"><code>prepare</code></a> and then binds the two parameters (the user agent and the date/time) using <a href="https://secure.php.net/manual/en/mysqli-stmt.bind-param.php"><code>bind_param</code></a> and then runs the query with <a href="https://secure.php.net/manual/en/mysqli-stmt.execute.php"><code>execute</code></a>.</p><p><code>bind_param</code> ensures that the special SQL characters like quotes are escaped correctly for insertion in the database. Trying to repeat the injection above results in the following database entry:</p>
            <pre><code>+---------------------+----------------------------------------------------+
| dt                  | ua                                                 |
+---------------------+----------------------------------------------------+
| 2016-05-17 04:46:02 | Mozilla/5.0',(select*from(select(sleep(20)))a)) #  |
+---------------------+----------------------------------------------------+</code></pre>
            <p>The attacker's SQL statement has not turned into a SQL injection and has simply been stored in the database.</p>
    <div>
      <h3>Conclusion</h3>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>SQL injection is a perennial favorite of attackers and can happen anywhere input controlled by an attacker is processed by a web application. It's easy to imagine how an attacker might manipulate a web form or a URI, but even HTTP request headers are vulnerable. Literally any input the web browser sends to a web application should be considered hostile.</p><p>We saw the same attacker use many variants on this theme. Some tried to make the web server respond slowly using SQL, others using Python or Ruby code (to see if the web server could be tricked into running that code).</p><p>CloudFlare's WAF helps mitigate attacks like this with rules to <a href="https://www.cloudflare.com/learning/security/threats/how-to-prevent-sql-injection/">block injection of SQL statements</a> and code.</p> ]]></content:encoded>
            <category><![CDATA[WAF Rules]]></category>
            <category><![CDATA[WAF]]></category>
            <category><![CDATA[SQL]]></category>
            <category><![CDATA[Security]]></category>
            <guid isPermaLink="false">6TZD2wJSHn8Y73YSuDO25F</guid>
            <dc:creator>John Graham-Cumming</dc:creator>
        </item>
        <item>
            <title><![CDATA[Scaling out PostgreSQL for CloudFlare Analytics using CitusDB]]></title>
            <link>https://blog.cloudflare.com/scaling-out-postgresql-for-cloudflare-analytics-using-citusdb/</link>
            <pubDate>Thu, 09 Apr 2015 17:32:05 GMT</pubDate>
            <description><![CDATA[ When I joined CloudFlare about 18 months ago, we had just started to build out our new Data Platform. At that point, the log processing and analytics pipeline built in the early days of the company had reached its limits.  ]]></description>
            <content:encoded><![CDATA[ <p>When I joined CloudFlare about 18 months ago, we had just started to build out our new Data Platform. At that point, the log processing and analytics pipeline built in the early days of the company had reached its limits. This was due to the rapidly increasing log volume from our Edge Platform where we’ve had to deal with traffic growth in excess of 400% annually.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4AxkHPDBZrwj6QJQVWuQcX/fec02af530de1ab2f8a1f516ece59057/keepcalm_scaled.png" />
            
            </figure><p>Our log processing pipeline started out like most everybody else’s: compressed log files shipped to a central location for aggregation by a motley collection of Perl scripts and C++ programs with a single PostgreSQL instance to store the aggregated data. Since then, CloudFlare has grown to serve millions of requests per second for millions of sites. Apart from the hundreds of terabytes of log data that has to be aggregated every day, we also face some unique challenges in providing detailed analytics for each of the millions of sites on CloudFlare.</p><p>For the next iteration of our Customer Analytics application, we wanted to get something up and running quickly, try out Kafka, write the aggregation application in Go, and see what could be done to scale out our trusty go-to database, PostgreSQL, from a single machine to a cluster of servers without requiring us to deal with sharding in the application.</p><p>As we were analyzing our scaling requirements for PostgreSQL, we came across <a href="https://www.citusdata.com/">Citus Data</a>, one of the companies to launch out of <a href="https://www.ycombinator.com/">Y Combinator</a> in the summer of 2011. Citus Data builds a database called CitusDB that scales out PostgreSQL for real-time workloads. Because CitusDB enables both real-time data ingest and sub-second queries across billions of rows, it has become a crucial part of our analytics infrastructure.</p>
    <div>
      <h4>Log Processing Pipeline for Analytics</h4>
      <a href="#log-processing-pipeline-for-analytics">
        
      </a>
    </div>
    <p>Before jumping into the details of our database backend, let’s review the pipeline that takes a log event from CloudFlare’s Edge to our analytics database.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4I3yKJFMKyL4M3gtS2SlTy/b34a4a58d3da74e788950e6af1699582/image01.png" />
            
            </figure><p>An HTTP access log event proceeds through the CloudFlare data pipeline as follows:</p><ol><li><p>A web browser makes a request (e.g., an HTTP GET request).</p></li><li><p>An Nginx web server running <a href="/pushing-nginx-to-its-limit-with-lua/">Lua code</a> handles the request and generates a binary log event in <a href="https://capnproto.org">Cap’n Proto format</a>.</p></li><li><p>A Go program akin to <a href="https://github.com/mozilla-services/heka">Heka</a> receives the log event from Nginx over a UNIX socket, batches it with other events, compresses the batch using a fast algorithm like <a href="https://github.com/google/snappy">Snappy</a> or <a href="https://github.com/Cyan4973/lz4">LZ4</a>, and sends it to our data center over a TLS-encrypted TCP connection.</p></li><li><p>Another Go program (the Kafka shim) receives the log event stream, decrypts it, decompresses the batches, and produces the events into a Kafka topic with partitions replicated on many servers.</p></li><li><p>Go aggregators (one process per partition) consume the topic-partitions and insert aggregates (not individual events) with 1-minute granularity into the CitusDB database. Further rollups to 1-hour and 1-day granularity occur later to reduce the amount of data to be queried and to speed up queries over intervals spanning many hours or days.</p></li></ol>
    <div>
      <h4>Why Go?</h4>
      <a href="#why-go">
        
      </a>
    </div>
    <p>Previous blog <a href="/what-weve-been-doing-with-go/">posts</a> and <a href="https://www.youtube.com/watch?v=8igk2ylk_X4">talks</a> have covered <a href="/go-at-cloudflare/">various CloudFlare projects that have been built using Go</a>. We’ve found that Go is a great language for teams to use when building the kinds of distributed systems needed at CloudFlare, and this is true regardless of an engineer’s level of experience with Go. Our Customer Analytics team is made up of engineers that have been using Go since before its 1.0 release as well as complete Go newbies. Team members that were new to Go were able to spin up quickly, and the code base has remained maintainable even as we’ve continued to build many more data processing and aggregation applications such as a new version of <a href="https://www.hakkalabs.co/articles/optimizing-go-3k-requestssec-480k-requestssec">our Layer 7 DDoS attack mitigation system</a>.</p><p>Another factor that makes Go great is the ever-expanding ecosystem of third party libraries. We used <a href="https://github.com/glycerine/go-capnproto">go-capnproto</a> to generate Go code to handle binary log events in Cap’n Proto format from a common schema shared between Go, C++, and <a href="/introducing-lua-capnproto-better-serialization-in-lua/">Lua projects</a>. Go support for Kafka with <a href="https://godoc.org/github.com/Shopify/sarama">Shopify’s Sarama</a> library, support for ZooKeeper with <a href="https://github.com/samuel/go-zookeeper">go-zookeeper</a>, support for PostgreSQL/CitusDB through <a href="http://golang.org/pkg/database/sql/">database/sql</a> and the <a href="https://github.com/lib/pq">lib/pq driver</a> are all very good.</p>
    <div>
      <h4>Why Kafka?</h4>
      <a href="#why-kafka">
        
      </a>
    </div>
    <p>As we started building our new data processing applications in Go, we had some additional requirements for the pipeline:</p><ol><li><p>Use a queue with persistence to allow short periods of downtime for downstream servers and/or consumer services.</p></li><li><p>Make the data available for processing in real time by <a href="https://github.com/mumrah/kafka-python">scripts</a> written by members of our Site Reliability Engineering team.</p></li><li><p>Allow future aggregators to be built in other languages like Java, <a href="https://github.com/edenhill/librdkafka">C or C++</a>.</p></li></ol><p>After extensive testing, we selected <a href="https://kafka.apache.org/">Kafka</a> as the first stage of the log processing pipeline.</p>
    <div>
      <h4>Why Postgres?</h4>
      <a href="#why-postgres">
        
      </a>
    </div>
    <p>As we mentioned when <a href="http://www.postgresql.org/about/press/presskit93/">PostgreSQL 9.3 was released</a>, PostgreSQL has long been an important part of our stack, and for good reason.</p><p>Foreign data wrappers and other extension mechanisms make PostgreSQL an excellent platform for storing lots of data, or as a gateway to other NoSQL data stores, without having to give up the power of SQL. PostgreSQL also has great performance and documentation. Lastly, PostgreSQL has a large and active community, and we've had the privilege of meeting many of the PostgreSQL contributors at meetups held at the CloudFlare office and elsewhere, organized by the <a href="http://www.meetup.com/postgresql-1/">The San Francisco Bay Area PostgreSQL Meetup Group</a>.</p>
    <div>
      <h4>Why CitusDB?</h4>
      <a href="#why-citusdb">
        
      </a>
    </div>
    <p>CloudFlare has been using PostgreSQL since day one. We trust it, and we wanted to keep using it. However, CloudFlare's data has been growing rapidly, and we were running into the limitations of a single PostgreSQL instance. Our team was tasked with scaling out our analytics database in a short time so we started by defining the criteria that are important to us:</p><ol><li><p><b>Performance</b>: Our system powers the Customer Analytics dashboard, so typical queries need to return in less than a second even when dealing with data from many customer sites over long time periods.</p></li><li><p><b>PostgreSQL</b>: We have extensive experience running PostgreSQL in production. We also find several extensions useful, e.g., Hstore enables us to store semi-structured data and HyperLogLog (HLL) makes unique count approximation queries fast.</p></li><li><p><b>Scaling</b>: We need to dynamically scale out our cluster for performance and huge data storage. That is, if we realize that our cluster is becoming overutilized, we want to solve the problem by just adding new machines.</p></li><li><p><b>High availability</b>: This cluster needs to be highly available. As such, the cluster needs to automatically recover from failures like disks dying or servers going down.</p></li><li><p><b>Business intelligence queries</b>: in addition to sub-second responses for customer queries, we need to be able to perform business intelligence queries that may need to analyze billions of rows of analytics data.</p></li></ol><p>At first, we evaluated what it would take to build an application that deals with sharding on top of stock PostgreSQL. We investigated using the <a href="http://www.postgresql.org/docs/9.4/static/postgres-fdw.html">postgres_fdw</a> extension to provide a unified view on top of a number of independent PostgreSQL servers, but this solution did not deal well with servers going down.</p><p>Research into the major players in the PostgreSQL space indicated that CitusDB had the potential to be a great fit for us. On the performance point, they already had customers running real-time analytics with queries running in parallel across a large cluster in tens of milliseconds.</p><p>CitusDB has also maintained compatibility with PostgreSQL, not by forking the code base like other vendors, but by extending it to plan and execute distributed queries. Furthermore, CitusDB used the concept of many logical shards so that if we were to add new machines to our cluster, we could easily rebalance the shards in the cluster by calling a simple PostgreSQL user-defined function.</p><p>With CitusDB, we could replicate logical shards to independent machines in the cluster, and automatically fail over between replicas even during queries. In case of a hardware failure, we could also use the rebalance function to re-replicate shards in the cluster.</p>
    <div>
      <h4>CitusDB Architecture</h4>
      <a href="#citusdb-architecture">
        
      </a>
    </div>
    
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/74Tl2hP3Tfk1HNzpF1MV4i/121b31b62700289180653b647a653edb/image00.png" />
            
            </figure><p>CitusDB follows an architecture similar to Hadoop to scale out Postgres: one primary node holds authoritative metadata about shards in the cluster and parallelizes incoming queries. The worker nodes then do all the actual work of running the queries.</p><p>In CloudFlare's case, the cluster holds about 1 million shards and each shard is replicated to multiple machines. When the application sends a query to the cluster, the primary node first prunes away unrelated shards and finds the specific shards relevant to the query. The primary node then transforms the query into many smaller queries for <a href="http://www.citusdata.com/blog/19-ozgun/114-how-to-build-your-distributed-database">parallel execution</a> and ships those smaller queries to the worker nodes.</p><p>Finally, the primary node receives intermediate results from the workers, merges them, and returns the final results to the application. This takes anywhere between 25 milliseconds to 2 seconds for queries in the CloudFlare analytics cluster, depending on whether some or all of the data is available in page cache.</p><p>From a high availability standpoint, when a worker node fails, the primary node automatically fails over to the replicas, even during a query. The primary node holds slowly changing metadata, making it a good fit for continuous backups or PostgreSQL's streaming replication feature. Citus Data is currently working on further improvements to make it easy to replicate the primary metadata to all the other nodes.</p><p>At CloudFlare, we love the CitusDB architecture because it enabled us to continue using PostgreSQL. Our analytics dashboard and BI tools connect to Citus using standard PostgreSQL connectors, and tools like <code>pg_dump</code> and <code>pg_upgrade</code> just work. Two features that stand out for us are CitusDB’s PostgreSQL extensions that power our analytics dashboards, and CitusDB’s ability to parallelize the logic in those extensions out of the box.</p>
    <div>
      <h4>Postgres Extensions on CitusDB</h4>
      <a href="#postgres-extensions-on-citusdb">
        
      </a>
    </div>
    <p>PostgreSQL extensions are pieces of software that add functionality to the core database itself. Some examples are data types, user-defined functions, operators, aggregates, and custom index types. PostgreSQL has more than 150 publicly available official extensions. We’d like to highlight two of these extensions that might be of general interest. It’s worth noting that with CitusDB all of these extensions automatically scale to many servers without any changes.</p>
    <div>
      <h4>HyperLogLog</h4>
      <a href="#hyperloglog">
        
      </a>
    </div>
    <p><a href="https://en.wikipedia.org/wiki/HyperLogLog">HyperLogLog</a> is a sophisticated algorithm developed for doing unique count approximations quickly. And since a <a href="https://github.com/aggregateknowledge/postgresql-hll">HLL implementation for PostgreSQL</a> was open sourced by the good folks at Aggregate Knowledge, we could use it with CitusDB unchanged because it’s compatible with most (if not all) Postgres extensions.</p><p>HLL was important for our application because we needed to compute unique IP counts across various time intervals in real time and we didn’t want to store the unique IPs themselves. With this extension, we could, for example, count the number of unique IP addresses accessing a customer site in a minute, but still have an accurate count when further rolling up the aggregated data into a 1-hour aggregate.</p>
    <div>
      <h4>Hstore</h4>
      <a href="#hstore">
        
      </a>
    </div>
    <p>The <a href="http://www.postgresql.org/docs/9.4/static/hstore.html">hstore data type</a> stores sets of key/value pairs within a single PostgreSQL value. This can be helpful in various scenarios such as with rows with many attributes that are rarely examined, or to represent semi-structured data. We use the hstore data type to hold counters for sparse categories (e.g. country, HTTP status, data center).</p><p>With the hstore data type, we save ourselves from the burden of denormalizing our table schema into hundreds or thousands of columns. For example, we have one hstore data type that holds the number of requests coming in from different data centers per minute per CloudFlare customer. With millions of customers and hundreds of data centers, this counter data ends up being very sparse. Thanks to hstore, we can efficiently store that data, and thanks to CitusDB, we can efficiently parallelize queries of that data.</p><p>For future applications, we are also investigating other extensions such as the Postgres columnar store extension <a href="https://github.com/citusdata/cstore_fdw">cstore_fdw</a> that Citus Data has open sourced. This will allow us to compress and store even more historical analytics data in a smaller footprint.</p>
    <div>
      <h4>Conclusion</h4>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>CitusDB has been working very well for us as the new backend for our Customer Analytics system. We have also found many uses for the analytics data in a business intelligence context. The ease with which we can run distributed queries on the data allows us to quickly answer new questions about the CloudFlare network that arise from anyone in the company, from the SRE team through to Sales.</p><p>We are looking forward to features available in the recently released <a href="https://www.citusdata.com/citus-products/citusdb-software">CitusDB 4.0</a>, especially the performance improvements and the new shard rebalancer. We’re also excited about using the JSONB data type with CitusDB 4.0, along with all the other improvements that come standard as part of <a href="http://www.postgresql.org/docs/9.4/static/release-9-4.html">PostgreSQL 9.4</a>.</p><p>Finally, if you’re interested in building and operating distributed services like Kafka or CitusDB and writing Go as part of a dynamic team dealing with big (nay, gargantuan) amounts of data, <a href="https://www.cloudflare.com/join-our-team">CloudFlare is hiring</a>.</p> ]]></content:encoded>
            <category><![CDATA[Analytics]]></category>
            <category><![CDATA[SQL]]></category>
            <category><![CDATA[Postgres]]></category>
            <category><![CDATA[Kafka]]></category>
            <category><![CDATA[LUA]]></category>
            <category><![CDATA[DDoS]]></category>
            <guid isPermaLink="false">4WkjJAXrP1iZH5uthDDnAh</guid>
            <dc:creator>Albert Strasheim</dc:creator>
        </item>
        <item>
            <title><![CDATA[Drupal 7 SA-CORE-2014-005 SQL Injection Protection]]></title>
            <link>https://blog.cloudflare.com/drupal-7-sa-core-2014-005-sql-injection-protection/</link>
            <pubDate>Thu, 16 Oct 2014 09:05:12 GMT</pubDate>
            <description><![CDATA[ Yesterday the Drupal Security Team released a critical security patch for Drupal 7 that fixes a very serious SQL injection vulnerability. ]]></description>
            <content:encoded><![CDATA[ <p>Yesterday the Drupal Security Team released a <a href="https://www.drupal.org/SA-CORE-2014-005">critical security patch</a> for Drupal 7 that fixes a very serious <a href="https://www.sektioneins.de/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html">SQL injection</a> vulnerability. At the same time we pushed an update to our Drupal WAF rules to mitigate this problem. Any customer using the WAF and with the Drupal ruleset enabled will have received automatic protection.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3efo085aTM09kdrW2DTfIZ/0925122451cc34030a0ea273a119a2fd/Screen-Shot-2014-10-16-at-10-57-27.png" />
            
            </figure><p>Rule D0002 provides protection against this vulnerability. If you do not have that ruleset enabled and are using Drupal clicking the ON button next to CloudFlare Drupal in the WAF Settings will enable protection immediately.</p><p>CloudFlare WAF protection can help mitigate vulnerabilities like this, but it is vital that Drupal 7 users <a href="https://www.drupal.org/drupal-7.32-release-notes">upgrade</a> to the safe version of Drupal immediately.</p>
    <div>
      <h3>Important Update on October 29, 2014</h3>
      <a href="#important-update-on-october-29-2014">
        
      </a>
    </div>
    <p>The Drupal Security team has posted a <a href="https://www.drupal.org/PSA-2014-003">PSA</a> on this vulnerability that states:</p><blockquote><p>You should proceed under the assumption that every Drupal 7 website was compromised unless updated or patched before Oct 15th, 11pm UTC, that is 7 hours after the announcement.</p></blockquote><p>Given the severity of that statement, if you did not update your Drupal 7 installation please read the PSA and follow the instructions on cleaning up your site.</p><blockquote><p>If you have not updated or applied this patch, do so immediately, then continue reading this announcement; updating to version 7.32 or applying the patch fixes the vulnerability but does not fix an already compromised website. If you find that your site is already patched but you didn’t do it, that can be a symptom that the site was compromised - some attacks have applied the patch as a way to guarantee they are the only attacker in control of the site.</p></blockquote> ]]></content:encoded>
            <category><![CDATA[Vulnerabilities]]></category>
            <category><![CDATA[Drupal]]></category>
            <category><![CDATA[SQL]]></category>
            <category><![CDATA[WAF]]></category>
            <guid isPermaLink="false">4Nxb2eiarXcKirj33lgCVI</guid>
            <dc:creator>John Graham-Cumming</dc:creator>
        </item>
        <item>
            <title><![CDATA[Integrating Kyoto Tycoon With PostgreSQL]]></title>
            <link>https://blog.cloudflare.com/kyoto_tycoon_with_postgresql/</link>
            <pubDate>Mon, 08 Jul 2013 06:00:00 GMT</pubDate>
            <description><![CDATA[ CloudFlare has Points of Presence (PoPs) in 23 datacenters around the world and plans to expand to many more soon. It also has a single portal, CloudFlare.com, where website owners interact with the system.  ]]></description>
            <content:encoded><![CDATA[ <p></p><p>CloudFlare has Points of Presence (PoPs) in 23 datacenters around the world and plans to expand to many more soon. It also has a single portal, <a href="https://cloudflare.com">CloudFlare.com</a>, where website owners interact with the system. This creates a giant configuration propagation problem.</p><p>Any time you log into CloudFlare.com and turn on a feature, app, or update a DNS record, you create what we refer to as a new rule. Whenever a request arrives at a PoP our DNS and web servers use these rules to determine how to respond.</p>
    <div>
      <h3>Replicating Rules</h3>
      <a href="#replicating-rules">
        
      </a>
    </div>
    <p>When you make a new rule, you expect this rule to be in effect everywhere, right away. Furthermore, you want CloudFlare to respect creation time ordering. For example, if you turn an app on and then off, you need a guarantee that the final state is off rather than on. And just to make things more fun, at peak times CloudFlare sees thousands of these rules created per second.</p><p>To solve this problem, CloudFlare uses two technologies. The first, <a href="http://www.postgresql.org/">PostgreSQL</a>, is a classic SQL database engine. It powers CloudFlare.com. Any time you create a rule, on the backend this logic is inserted into a row oriented table.</p><p>The second, <a href="http://fallabs.com/kyototycoon/">Kyoto Tycoon</a> or KT, is a distributed key-value store. A primary instance here in our main datacenter is replicated out to all of our PoPs. This means that any key/value pair inserted in the primary location will be readable at any place in the world. Timestamp-based replication ensures eventual consistency and guarantees ordering. In practice, we see at most around 3 seconds of delay for full propagation in normal conditions. The trick though is in moving data from PostgreSQL to Kyoto Tycoon.</p>
    <div>
      <h3>Getting Rules Out of Postgres</h3>
      <a href="#getting-rules-out-of-postgres">
        
      </a>
    </div>
    <p>This process of reconciling the PostgreSQL with KT has always been a somewhat painful process. Currently, it is accomplished with a homebrewed system of scripts that periodically read the database and perform the operations necessary to reconcile the two systems.</p><p>This method is far from optimal because it introduces artificial lag to the system as well as the additional complexity and logic necessary for reconciliation.</p>
    <div>
      <h3>FDW FTW</h3>
      <a href="#fdw-ftw">
        
      </a>
    </div>
    <p>Thanks to PostgreSQL 9.3's new writable <a href="http://www.postgresql.org/docs/9.3/static/postgres-fdw.html">foreign data wrappers</a> (FDW) though, that's all about to change.</p><p>PostgreSQL's FDW are a method of integrating external sources of data within databases.</p><p>To a PostgreSQL user, FDW data sources appear as any other database table while the logic to communicate with the external data sources and convert data into tables is handled by the FDW. Prior to version 9.3, FDW were read-only with no capacity to export data. But now, PostgreSQL can write to foreign data sources.</p><p>By making a FDW for Kyoto Tycoon, we allow data in KT to be represented as a database table. This then allows us to use database triggers to perform data synchronization with KT, both lowering latency and simplifying the logic of data synchronization.</p><p>As an added bonus, we translate PostgreSQL transactions into KT transactions.</p><p>This means that all the ACID guarantees of PostgreSQL are exported to KT, allowing you to perform transactional operations on KT using the familiar SQL commands of BEGIN, COMMIT and ROLLBACK.</p>
    <div>
      <h3>Try It Yourself</h3>
      <a href="#try-it-yourself">
        
      </a>
    </div>
    <p>We think this integration is so useful that we have decided to open source the FDW code and make it available at <a href="https://github.com/cloudflare/kt_fdw">https://github.com/cloudflare/kt_fdw</a>.</p><p>Usage is very simple. After installing the KT FDW simply run:</p><p>CREATE SERVER  FOREIGN DATA WRAPPER kt_fdw OPTIONS
(host '127.0.0.1', port '1978', timeout '-1');
(the above options are the defaults)</p><p>CREATE USER MAPPING FOR PUBLIC SERVER kt_server;</p><p>CREATE FOREIGN TABLE  (key TEXT, value TEXT) SERVER ;</p><p>And now you can SELECT, INSERT, UPDATE, and DELETE from this table. Those commands will perform the corresponding operation on the KT server.</p><p>As one of the first FDWs to make use of the new writable API for PostgreSQL, we hope this code will be used by other people who wish to plug in any alternative storage engine.</p> ]]></content:encoded>
            <category><![CDATA[SQL]]></category>
            <category><![CDATA[Speed & Reliability]]></category>
            <category><![CDATA[Cloudflare Network]]></category>
            <guid isPermaLink="false">2nmxSBB7wJQjcfNkvUv4EM</guid>
            <dc:creator>Matvey Arye</dc:creator>
        </item>
        <item>
            <title><![CDATA[Ian Pye Presents SortaSQL @ the CloudFlare Loft]]></title>
            <link>https://blog.cloudflare.com/ian-pye-presents-sortasql-the-cloudflare-loft/</link>
            <pubDate>Fri, 22 Apr 2011 19:49:00 GMT</pubDate>
            <description><![CDATA[ Last night CloudFlare hosted a Hybrid DB meetup where Ian Pye, our lead Analytics Engineer, presented on the data infrastructure at CloudFlare. He discussed how CloudFlare looked at several NoSQL and SQL solutions and ended up with a hybrid model, SortaSQL. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Last night CloudFlare hosted a <a href="http://www.meetup.com/SFNoSQL/events/16890931/">Hybrid DB meetup</a> where Ian Pye, our lead Analytics Engineer, presented on the data infrastructure at CloudFlare. He discussed how CloudFlare looked at several NoSQL and SQL solutions and ended up with a hybrid model, SortaSQL.</p><p>Reviews from attendees: "mindblowing, actually" and "very clever."</p><p><a href="http://www.slideshare.net/cloudflare/sortasql">Click here</a> to view Ian's slide show presentation and to learn more details of CloudFlare's SortaSQL.</p><p>CloudFlare has several meetups in the works for May and June, stay tuned to find out what we will be hosting next!</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5otOXjtoqK2XGX3K0M9mQL/172e66ad9a180e800646aaa87639d346/IMG_1612.JPG.scaled500.jpg" />
            
            </figure> ]]></content:encoded>
            <category><![CDATA[Cloudflare Meetups]]></category>
            <category><![CDATA[SQL]]></category>
            <guid isPermaLink="false">60BK4zewCwg4CQ0m5shGC</guid>
            <dc:creator>Kristin Tarr</dc:creator>
        </item>
    </channel>
</rss>