
<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>Tue, 14 Apr 2026 03:35:07 GMT</lastBuildDate>
        <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[Introducing Timing Insights: new performance metrics via our GraphQL API]]></title>
            <link>https://blog.cloudflare.com/introducing-timing-insights/</link>
            <pubDate>Tue, 20 Jun 2023 13:00:47 GMT</pubDate>
            <description><![CDATA[ If you care about the performance of your website or APIs, it’s critical to understand why things are slow.  Today we're introducing new analytics tools to help you understand what is contributing to "Time to First Byte" (TTFB) of Cloudflare and your origin ]]></description>
            <content:encoded><![CDATA[ 
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1OastOs9G4xxa9jwrnKl3V/b50435c06e0316b8bf78cef88bce6888/xi31J0JmYNP5dcB-gEsTujRYG1gyFUsod_Fx7XPsjPwwxTQBIOFwTy9m0jPNQabe0bi5oUSwJHo5ubAq9rcAhgTXsqlTcoi9rpLM5pwoAwY-Yj8vuothGdHJHJbz.png" />
            
            </figure><p>If you care about the performance of your website or APIs, it’s critical to understand why things are slow.</p><p>Today we're introducing new analytics tools to help you understand what is contributing to "Time to First Byte" (TTFB) of Cloudflare and your origin. TTFB is just a simple timer from when a client sends a request until it receives the first byte in response. Timing Insights breaks down TTFB from the perspective of our servers to help you understand <i>what</i> is slow, so that you can begin addressing it.</p><p>But wait – maybe you've heard that <a href="/ttfb-time-to-first-byte-considered-meaningles/">you should stop worrying about TTFB</a>? Isn't Cloudflare <a href="/ttfb-is-not-what-it-used-to-be/">moving away</a> from TTFB as a metric? Read on to understand why there are still situations where TTFB matters.</p>
    <div>
      <h3>Why you may need to care about TTFB</h3>
      <a href="#why-you-may-need-to-care-about-ttfb">
        
      </a>
    </div>
    <p>It's true that TTFB on its own can be a misleading metric. When measuring web applications, metrics like <a href="https://web.dev/vitals/">Web Vitals</a> provide a more holistic view into user experience. That's why we offer Web Analytics and Lighthouse within <a href="/cloudflare-observatory-generally-available/">Cloudflare Observatory</a>.</p><p>But there are two reasons why you still may need to pay attention to TTFB:</p><p><b>1. Not all applications are websites</b>More than half of Cloudflare traffic is for APIs, and many customers with API traffic don't control the environments where those endpoints are called. In those cases, there may not be anything you can <a href="https://www.cloudflare.com/application-services/solutions/app-performance-monitoring/">monitor</a> or improve besides TTFB.</p><p><b>2. Sometimes TTFB is the problem</b>Even if you are measuring Web Vitals metrics like LCP, sometimes the reason your site is slow is because TTFB is slow! And when that happens, you need to know why, and what you can do about it.</p><p>When you need to know why TTFB is slow, we’re here to help.</p>
    <div>
      <h3>How Timing Insights can help</h3>
      <a href="#how-timing-insights-can-help">
        
      </a>
    </div>
    <p>We now expose performance data through our <a href="https://developers.cloudflare.com/analytics/graphql-api/">GraphQL Analytics API</a> that will let you query TTFB performance, and start to drill into what contributes to TTFB.</p><p>Specifically, customers on our Pro, Business, and Enterprise plans can now query for the following fields in the <code>httpRequestsAdaptiveGroups</code> dataset:</p><p><b>Time to First Byte</b> (edgeTimeToFirstByteMs)</p><p>What is the time elapsed between when Cloudflare started processing the first byte of the request received from an end user, until when we started sending a response?</p><p><b>Origin DNS lookup time</b> (edgeDnsResponseTimeMs)</p><p>If Cloudflare had to <a href="https://developers.cloudflare.com/dns/zone-setups/partial-setup/setup/">resolve a CNAME</a> to reach your origin, how long did this take?</p><p><b>Origin Response Time</b> (originResponseDurationMs)</p><p>How long did it take to reach, and receive a response from your origin?</p><p>We are exposing each metric as an average, median, 95th, and 99th percentiles (i.e. P50 / P95 / P99).</p><p>The <code>httpRequestAdaptiveGroups</code> dataset powers the <a href="https://dash.cloudflare.com/?to=/:account/:zone/analytics/traffic">Traffic</a> analytics page in our dashboard, and represents all of the HTTP requests that flow through our network. The upshot is that this dataset gives you the ability to filter and “group by” any aspect of the HTTP request.</p>
    <div>
      <h3>An example of how to use Timing Insights</h3>
      <a href="#an-example-of-how-to-use-timing-insights">
        
      </a>
    </div>
    <p>Let’s walk through an example of how you’d actually use this data to pinpoint a problem.</p><p>To start with, I want to understand the lay of the land by querying TTFB at various quantiles:</p>
            <pre><code>query TTFBQuantiles($zoneTag: string) {
  viewer {
    zones(filter: {zoneTag: $zoneTag}) {
      httpRequestsAdaptiveGroups(limit: 1) {
        quantiles {
          edgeTimeToFirstByteMsP50
          edgeTimeToFirstByteMsP95
          edgeTimeToFirstByteMsP99
        }
      }
    }
  }
}

Response:
{
  "data": {
    "viewer": {
      "zones": [
        {
          "httpRequestsAdaptiveGroups": [
            {
              "quantiles": {
                "edgeTimeToFirstByteMsP50": 32,
                "edgeTimeToFirstByteMsP95": 1392,
                "edgeTimeToFirstByteMsP99": 3063,
              }
            }
          ]
        }
      ]
    }
  }
}</code></pre>
            <p>This shows that TTFB is over 1.3 seconds at P95 – that’s fairly slow, given that <a href="https://web.dev/lcp/">best practices</a> are for 75% of pages to <i>finish rendering</i> within 2.5 seconds, and TTFB is just one component of LCP.</p><p>If I want to dig into why TTFB, it would be helpful to understand <i>which URLs</i> are slowest. In this query I’ll filter to that slowest 5% of page loads, and now look at the <i>aggregate</i> time taken – this helps me understand which pages contribute most to slow loads:</p>
            <pre><code>query slowestURLs($zoneTag: string, $filter:filter) {
  viewer {
    zones(filter: {zoneTag: $zoneTag}) {
      httpRequestsAdaptiveGroups(limit: 3, filter: {edgeTimeToFirstByteMs_gt: 1392}, orderBy: [sum_edgeTimeToFirstByteMs_DESC]) {
        sum {
          edgeTimeToFirstByteMs
        }
        dimensions {
          clientRequestPath
        }
      }
    }
  }
}

Response:
{
  "data": {
    "viewer": {
      "zones": [
        {
          "httpRequestsAdaptiveGroups": [
            {
              "dimensions": {
                "clientRequestPath": "/api/v2"
              },
              "sum": {
                "edgeTimeToFirstByteMs": 1655952
              }
            },
            {
              "dimensions": {
                "clientRequestPath": "/blog"
              },
              "sum": {
                "edgeTimeToFirstByteMs": 167397
              }
            },
            {
              "dimensions": {
                "clientRequestPath": "/"
              },
              "sum": {
                "edgeTimeToFirstByteMs": 118542
              }
            }
          ]
        }
      ]
    }
  }
}</code></pre>
            <p>Based on this query, it looks like the <code>/api/v2</code> path is most often responsible for these slow requests. In order to know how to fix the problem, we need to know <i>why</i> these pages are slow. To do this, we can query for the average (mean) DNS and origin response time for queries on these paths, where TTFB is above our P95 threshold:</p>
            <pre><code>query originAndDnsTiming($zoneTag: string, $filter:filter) {
  viewer {
    zones(filter: {zoneTag: $zoneTag}) {
      httpRequestsAdaptiveGroups(filter: {edgeTimeToFirstByteMs_gt: 1392, clientRequestPath_in: [$paths]}) {
        avg {
          originResponseDurationMs
          edgeDnsResponseTimeMs
        }
      }
    }
}

Response:
{
  "data": {
    "viewer": {
      "zones": [
        {
          "httpRequestsAdaptiveGroups": [
            {
              "average": {
                "originResponseDurationMs": 4955,
                "edgeDnsResponseTimeMs": 742,
              }
            }
          ]
        }
      ]
    }
  }
}</code></pre>
            <p>According to this, most of the long TTFB values are actually due to resolving DNS! The good news is that’s something we can fix – for example, by setting longer TTLs with my DNS provider.</p>
    <div>
      <h3>Conclusion</h3>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>Coming soon, we’ll be bringing this to Cloudflare Observatory in the dashboard so that you can easily explore timing data via the UI.</p><p>And we’ll be adding even more granular metrics so you can see exactly which components are contributing to high TTFB. For example, we plan to separate out the difference between origin “connection time” (how long it took to establish a TCP and/or TLS connection) vs “application response time” (how long it took an HTTP server to respond).</p><p>We’ll also be making improvements to our GraphQL API to allow more flexible querying – for example, the ability to query arbitrary percentiles, not just 50th, 95th, or 99th.</p><p>Start using the <a href="https://developers.cloudflare.com/analytics/graphql-api/">GraphQL API</a> today to get Timing Insights, or hop on the discussion about our Analytics products in <a href="https://discord.com/channels/595317990191398933/1115387663982276648">Discord</a>.</p>
    <div>
      <h3>Watch on Cloudflare TV</h3>
      <a href="#watch-on-cloudflare-tv">
        
      </a>
    </div>
    <div></div> ]]></content:encoded>
            <category><![CDATA[Speed Week]]></category>
            <category><![CDATA[Performance]]></category>
            <category><![CDATA[Analytics]]></category>
            <category><![CDATA[GraphQL]]></category>
            <guid isPermaLink="false">1vitH7RVlSZWDB2VUyPmVA</guid>
            <dc:creator>Jon Levine</dc:creator>
            <dc:creator>Miki Mokrysz</dc:creator>
        </item>
        <item>
            <title><![CDATA[Announcing Network Analytics]]></title>
            <link>https://blog.cloudflare.com/announcing-network-analytics/</link>
            <pubDate>Mon, 16 Mar 2020 12:00:00 GMT</pubDate>
            <description><![CDATA[ Back in March 2019, we released Firewall Analytics which provides insights into HTTP security events across all of Cloudflare's protection suite; Firewall rule matches, HTTP DDoS Attacks, Site Security Level which harnesses Cloudflare's threat intelligence, and more. ]]></description>
            <content:encoded><![CDATA[ 
    <div>
      <h2>Our Analytics Platform</h2>
      <a href="#our-analytics-platform">
        
      </a>
    </div>
    <p>Back in March 2019, we released <a href="/new-firewall-tab-and-analytics/">Firewall Analytics</a> which provides insights into HTTP security events across all of Cloudflare's protection suite; Firewall rule matches, HTTP DDoS Attacks, Site Security Level which harnesses Cloudflare's threat intelligence, and more. It helps customers tailor their security configurations more effectively. The initial release was for Enterprise customers, however we believe that everyone should have access to powerful tools, not just large enterprises, and so in December 2019 we <a href="/updates-to-firewall-analytics/">extended those same enterprise-level analytics to our Business and Pro customers</a>.</p><p>Since then, we’ve built on top of our analytics platform; improved the usability, added more functionality and extended it to additional Cloudflare services in the form of Account Analytics, DNS Analytics, Load Balancing Analytics, Monitoring Analytics and more.</p><p>Our entire <a href="https://www.cloudflare.com/analytics/">analytics platform</a> harnesses the powerful <a href="https://developers.cloudflare.com/analytics/graphql-api/">GraphQL framework</a> which is also available to customers that want to build, export and share their own custom reports and dashboards.</p>
    <div>
      <h2>Extending Visibility From L7 To L3</h2>
      <a href="#extending-visibility-from-l7-to-l3">
        
      </a>
    </div>
    <p>Until recently, all of our dashboards were mostly HTTP-oriented and provided visibility into HTTP attributes such as the user agent, hosts, cached resources, etc. This is valuable to customers that use Cloudflare to protect and accelerate HTTP applications, mobile apps, or similar. We’re able to provide them visibility into the application layer (Layer 7 in the OSI model) because we proxy their traffic at L7.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2ZBntqdxMb0c6QOFu3vBmu/fbf5d06627ddde8184a357f27f927742/pasted-image-0-2.png" />
            
            </figure><p>DDoS Protection for Layer 3-7</p><p>However with <a href="https://www.cloudflare.com/magic-transit/">Magic Transit</a>, we don’t proxy traffic at L7 but rather route it at L3 (network layer). Using BGP Anycast, customer traffic is routed to the closest point of presence of Cloudflare’s network edge where it is filtered by customer-defined network firewall rules and automatic DDoS mitigation systems. Clean traffic is then routed via dynamic GRE Anycast tunnels to the customer's data-centers. Routing at L3 means that we have limited visibility into the higher layers. So in order to provide Magic Transit customers visibility into traffic and attacks, we needed to extend our analytics platform to the packet-layer.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4av9KajzHgNLjrfDbhGD08/363c783d5a4ce6261c200c7e10bd23ff/image-2-1.png" />
            
            </figure><p>Magic Transit Traffic Flow</p><p>On January 16, 2020, we released the <a href="https://support.cloudflare.com/hc/en-us/articles/360038696631-Understanding-Cloudflare-Network-Analytics">Network Analytics</a> dashboard for <a href="https://www.cloudflare.com/magic-transit/">Magic Transit</a> customers and Bring Your Own IP (BYOIP) customers. This packet and bit oriented dashboard provides near real-time visibility into network- and transport-layer traffic patterns and DDoS attacks that are blocked at the Cloudflare edge in over 200 cities around the world.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/42tmqoCWcdfUsI7Lguko4c/93872eb5e86eefb3177b5c2b615e36e7/image-4-1.png" />
            
            </figure><p>Network Analytics - Packets &amp; Bits Over Time</p>
    <div>
      <h3>Analytics For A Year</h3>
      <a href="#analytics-for-a-year">
        
      </a>
    </div>
    <p>The way we've architected the analytics data-stores enables us to provide our customers one year's worth of insights. Traffic is sampled at the edge data-centers. From those samples we structure IP flow logs which are similar to SFlow and bucket one minute's worth of traffic, grouped by destination IP, port and protocol. IP flows includes multiple packet attributes such as TCP flags, source IPs and ports, Cloudflare data-center, etc. The source IP is considered PII data and is therefore only stored for 30 days, after which the source IPs are discarded and logs are rolled up into one hour groups, and then one day groups. The one hour roll-ups are stored for 6 months and the one day roll-ups for 1 year.</p><p>Similarly, attack logs are also stored efficiently. Attacks are stored as summaries with start/end timestamps, min/max/average/total bits and packets per second, attack type, action taken and more. A DDoS attack could easily consist of billions of packets which could impact performance due to the number of read/write calls to the data-store. By storing attacks as summary logs, we're able to overcome these challenges and therefore provide attack logs for up to 1 year back.</p>
    <div>
      <h3>Network Analytics via GraphQL API</h3>
      <a href="#network-analytics-via-graphql-api">
        
      </a>
    </div>
    <p>We built this dashboard on the same analytics platform, meaning that our packet-level analytics are also available by GraphQL. As an example, below is an attack report query that would show the top attacker IPs, the data-center cities and countries where the attack was observed, the IP version distribution, the ASNs that were used by the attackers and the ports. The query is done at the account level, meaning it would provide a report for all of your IP ranges. In order to narrow the report down to a specific destination IP or port range, you can simply add additional filters. The same filters also exist in the UI.</p>
            <pre><code>{
  viewer {
    accounts(filter: { accountTag: $accountTag }) {
      topNPorts: ipFlows1mGroups(
        limit: 5
        filter: $portFilter
        orderBy: [sum_packets_DESC]
      ) {
        sum {
          count: packets
          __typename
        }
        dimensions {
          metric: sourcePort
          ipProtocol
          __typename
        }
        __typename
      }
      topNASN: ipFlows1mGroups(
        limit: 5
        filter: $filter
        orderBy: [sum_packets_DESC]
      ) {
        sum {
          count: packets
          __typename
        }
        dimensions {
          metric: sourceIPAsn
          description: sourceIPASNDescription
          __typename
        }
        __typename
      }
      topNIPs: ipFlows1mGroups(
        limit: 5
        filter: $filter
        orderBy: [sum_packets_DESC]
      ) {
        sum {
          count: packets
          __typename
        }
        dimensions {
          metric: sourceIP
          __typename
        }
        __typename
      }
      topNColos: ipFlows1mGroups(
        limit: 10
        filter: $filter
        orderBy: [sum_packets_DESC]
      ) {
        sum {
          count: packets
          __typename
        }
        dimensions {
          metric: coloCity
          coloCode
          __typename
        }
        __typename
      }
      topNCountries: ipFlows1mGroups(
        limit: 10
        filter: $filter
        orderBy: [sum_packets_DESC]
      ) {
        sum {
          count: packets
          __typename
        }
        dimensions {
          metric: coloCountry
          __typename
        }
        __typename
      }
      topNIPVersions: ipFlows1mGroups(
        limit: 2
        filter: $filter
        orderBy: [sum_packets_DESC]
      ) {
        sum {
          count: packets
          __typename
        }
        dimensions {
          metric: ipVersion
          __typename
        }
        __typename
      }
      __typename
    }
    __typename
  }
}</code></pre>
            <p>Attack Report Query Example</p><p>After running the query using Altair GraphQL Client, the response is returned in a JSON format:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2Di3dzwDX6jtShRjooRjud/ddf2420c5fecaf290782c80a9809cc1a/image-8.png" />
            
            </figure>
    <div>
      <h2>What Do Customers Want?</h2>
      <a href="#what-do-customers-want">
        
      </a>
    </div>
    <p>As part of our product definition and design research stages, we interviewed internal customer-facing teams including Customer Support, Solution Engineering and more. I consider these stakeholders as super-user-aggregators because they're customer-facing teams and are constantly engaging and helping our users. After the internal research phase, we expanded externally to customers and prospects; particularly network and security engineers and leaders. We wanted to know how they expect the dashboard to fit in their work-flow, what are their use cases and how we can tailor the dashboard to their needs. Long story short, we identified two main use cases: Incident Response and Reporting. Let's go into each of these use cases in more detail.</p>
    <div>
      <h2>Incident Response</h2>
      <a href="#incident-response">
        
      </a>
    </div>
    <p>I started off by asking them a simple question - "what do you do when you're paged?" We wanted to better understand their incident response process; specifically, how they’d expect to use this dashboard when responding to an incident and what are the metrics that matter to them to help them make quick calculated decisions.</p>
    <div>
      <h3>You’ve Just Been Paged</h3>
      <a href="#youve-just-been-paged">
        
      </a>
    </div>
    <p>Let’s say that you’re a security operations engineer. It’s Black Friday. You’re on call. You’ve just been paged. Traffic levels to one of your data-centers has exceeded a safe threshold. Boom. What do you do? Responding quickly and resolving the issue as soon as possible is key.</p><p>If your workflows are similar to our customers’, then your objective is to resolve the page as soon as possible. However, before you can resolve it, you need to determine if there is any action that you need to take. For instance, is this a legitimate rise in traffic from excited Black Friday shoppers, perhaps a new game release or maybe an attack that hasn’t been mitigated? Do you need to shift traffic to another data-center or are levels still stable? Our customers tell us that these are the metrics that matter the most:</p><ol><li><p>Top Destination IP and port - helps understand what services are being impacted</p></li><li><p>Top source IPs, port, ASN, data-center and data-center Country - helps identify the source of the traffic</p></li><li><p>Real-time packet and bit rates - helps understand the traffic levels</p></li><li><p>Protocol distribution - helps understand what type of traffic is abnormal</p></li><li><p>TCP flag distribution - an abnormal distribution could indicate an attack</p></li><li><p>Attack Log - shows what types of traffic is being dropped/rate-limited</p></li></ol>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3jXNAgf5eCl4TZHjSum69h/fd1ae990776eacc8c42a4a71c93a440f/pasted-image-0--1-.png" />
            
            </figure><p>Customizable DDoS Attack Log</p><p>As network and transport layer attacks can be highly distributed and the packet attributes can be easily spoofed, it's usually not practical to block IPs. Instead, the dashboard enables you to quickly identify patterns such as an increased frequency of a specific TCP flag or increased traffic from a specific country. Identifying these patterns brings you one step closer to resolving the issue. After you’ve identified the patterns, packet-level filtering can be applied to drop or rate-limit the malicious traffic. If the attack was automatically mitigated by Cloudflare’s systems, you’ll be able to see it immediately along with the attack attributes in the activity log. By filtering by the Attack ID, the entire dashboard becomes your attack report.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/anQdgAoCWBPEpXqxwsIlu/ca684ea2d4a28572c257a418885a6f2a/pasted-image-0--2-.png" />
            
            </figure><p>Packet/Bit Distribution by Source &amp; Destination</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4gZCFJPtI8Ca5LzU1hObkq/c7d8e4fb66ff42d53266ee3f8a642052/pasted-image-0--3-.png" />
            
            </figure><p>TCP Flag Distribution</p>
    <div>
      <h2>Reporting</h2>
      <a href="#reporting">
        
      </a>
    </div>
    <p>During our interviews with security and network engineers, we also asked them what metrics and insights they need when creating reports for their managers, C-levels, colleagues and providing evidence to law-enforcement agencies. After all, processing data and <a href="https://www.helpnetsecurity.com/2019/06/24/enterprise-visibility-concerns/">creating reports can consume over a third (36%) of a security team’s time</a> (~3 hours a day) and is also one of the most frequent DDoS asks by our customers.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2R2VjjzdLkYfA6Rra6yNjE/776dccff0e59dcdf61f56e498fa11651/image-5-1.png" />
            
            </figure><p>Add filters, select-time range, print and share</p><p>On top of all of these customizable insights, we wanted to also provide a one-line summary that would reflect your recent activity. The one-liner is dynamic and changes based on your activity. It tells you whether you're currently <a href="https://www.cloudflare.com/ddos/under-attack/">under attack</a>, and how many attacks were blocked. If your <a href="https://www.cloudflare.com/ciso/">CISO</a> is asking for a security update, you can simply copy-paste it and convey the efficiency of the service:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4bLPVC0BSMDOIak6KM1NRv/4451204a172dda3bead45dec75951523/image-7-1.png" />
            
            </figure><p>Dynamic Summary</p><p>Our customers say that they want to reflect the value of Cloudflare to their managers and peers:</p><ol><li><p>How much potential downtime and bandwidth did Cloudflare spare me?</p></li><li><p>What are my top attacked IPs and ports?</p></li><li><p>Where are the attacks coming from? What types and what are the trends?</p></li></ol>
    <div>
      <h3>The Secret To Creating Good Reports</h3>
      <a href="#the-secret-to-creating-good-reports">
        
      </a>
    </div>
    <p>What does everyone love? Cool Maps! The key to a good report is adding a map; showing where the attack came from. But given that packet attributes can be easily spoofed, including the source IP, it won't do us any good to plot a map based on the locations of the source IPs. It would result in a spoofed source country and is therefore useless data. Instead, we decided to show the geographic distribution of packets and bits based on the Cloudflare data-center in which they were ingested. As opposed to legacy scrubbing center solutions with limited network infrastructures, Cloudflare has data-centers in more than 200 cities around the world. This enables us to provide precise geographic distribution with high confidence, making your reports accurate.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2oK50PYyfJ1YpCsoToPxUJ/8d2bc811cec25424abcd4cc9f004bcf0/pasted-image-0--4-.png" />
            
            </figure><p>Packet/Bit Distribution by geography: Data-center City &amp; Country</p>
    <div>
      <h3>Tailored For You</h3>
      <a href="#tailored-for-you">
        
      </a>
    </div>
    <p>One of the main challenges both our customers and we struggle with is how to process and generate actionable insights from all of the data points. This is especially critical when responding to an incident. Under this assumption, we built this dashboard with the purpose of speeding up your reporting and investigation processes. By tailoring it to your needs, we hope to make you more efficient and make the most out of Cloudflare’s services. Got any feedback or questions? Post them below in the comments section.</p><p>If you’re an existing Magic Transit or BYOIP customer, then the dashboard is already available to you. Not a customer yet? Click <a href="https://www.cloudflare.com/magic-transit/">here</a> to learn more.</p> ]]></content:encoded>
            <category><![CDATA[DDoS]]></category>
            <category><![CDATA[Analytics]]></category>
            <category><![CDATA[Magic Transit]]></category>
            <category><![CDATA[GraphQL]]></category>
            <category><![CDATA[Security]]></category>
            <category><![CDATA[Network]]></category>
            <category><![CDATA[Product News]]></category>
            <guid isPermaLink="false">46aPVEQ93JQ7TaQwqLXXgv</guid>
            <dc:creator>Omer Yoachimik</dc:creator>
        </item>
        <item>
            <title><![CDATA[How we used our new GraphQL Analytics API to build Firewall Analytics]]></title>
            <link>https://blog.cloudflare.com/how-we-used-our-new-graphql-api-to-build-firewall-analytics/</link>
            <pubDate>Thu, 12 Dec 2019 15:41:20 GMT</pubDate>
            <description><![CDATA[ Firewall Analytics is the first product in the Cloudflare dashboard to utilize the new GraphQL API. All Cloudflare dashboard products are built using the same public APIs that we provide to our customers, allowing us to understand the challenges they face when interfacing with our APIs. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Firewall Analytics is the first product in the Cloudflare dashboard to utilize the new GraphQL Analytics API. All Cloudflare dashboard products are built using the same public APIs that we provide to our customers, allowing us to understand the challenges they face when interfacing with our APIs. This parity helps us build and shape our products, most recently the new GraphQL Analytics API that we’re thrilled to release today.</p><p>By defining the data we want, along with the response format, our GraphQL Analytics API has enabled us to prototype new functionality and iterate quickly from our beta user feedback. It is helping us deliver more insightful analytics tools within the Cloudflare dashboard to our customers.</p><p>Our user research and testing for <a href="/new-firewall-tab-and-analytics/#new-firewall-analytics-for-analysing-events-and-maintaining-optimal-configurations">Firewall Analytics</a> surfaced common use cases in our customers' workflow:</p><ul><li><p>Identifying spikes in firewall activity over time</p></li><li><p>Understanding the common attributes of threats</p></li><li><p>Drilling down into granular details of an individual event to identify potential false positives</p></li></ul><p>We can address all of these use cases using our new GraphQL Analytics API.</p>
    <div>
      <h3>GraphQL Basics</h3>
      <a href="#graphql-basics">
        
      </a>
    </div>
    <p>Before we look into how to address each of these use cases, let's take a look at the format of a GraphQL query and how our schema is structured.</p><p>A GraphQL query is comprised of a structured set of fields, for which the server provides corresponding values in its response. The schema defines which fields are available and their type. You can find more information about the GraphQL query syntax and format in the <a href="https://graphql.org/learn/queries/">official GraphQL documentation</a>.</p><p>To run some GraphQL queries, we recommend downloading a GraphQL client, such as <a href="https://electronjs.org/apps/graphiql">GraphiQL</a>, to explore our schema and run some queries. You can find documentation on getting started with this in our <a href="https://developers.cloudflare.com/analytics/graphql-api/getting-started/">developer docs</a>.</p><p>At the top level of the schema is the <code>viewer</code> field. This represents the top level node of the user running the query. Within this, we can query the <code>zones</code> field to find zones the current user has access to, providing a <code>filter</code> argument, with a <code>zoneTag</code> of the identifier of the zone we'd like narrow down to.</p>
            <pre><code>{
  viewer {
    zones(filter: { zoneTag: "YOUR_ZONE_ID" }) {
      # Here is where we'll query our firewall events
    }
  }
}</code></pre>
            <p>Now that we have a query that finds our zone, we can start querying the firewall events which have occurred in that zone, to help solve some of the use cases we’ve identified.</p>
    <div>
      <h3>Visualising spikes in firewall activity</h3>
      <a href="#visualising-spikes-in-firewall-activity">
        
      </a>
    </div>
    <p>It's important for customers to be able to visualise and understand anomalies and spikes in their firewall activity, as these could indicate an attack or be the result of a misconfiguration.</p><p>Plotting events in a timeseries chart, by their respective action, provides users with a visual overview of the trend of their firewall events.</p><p>Within the <code>zones</code> field in the query we’ve created earlier, we can query our firewall event aggregates using the <code>firewallEventsAdaptiveGroups</code> field, providing arguments to limit the count of groups, a filter for the date range we're looking for (combined with any user-entered filters), and a list of fields to order by; in this case, just the <code>datetimeHour</code> field that we're grouping by.</p><p>Within the <code>zones</code> field in the query we created earlier, we can further query our firewall event aggregates using the <code>firewallEventsAdaptiveGroups</code> field and providing arguments for:</p><ul><li><p>A <code>limit</code> for the count of groups</p></li><li><p>A <code>filter</code> for the date range we're looking for (combined with any user-entered filters)</p></li><li><p>A list of fields to <code>orderBy</code> (in this case, just the <code>datetimeHour</code> field that we're grouping by).</p></li></ul><p>By adding the <code>dimensions</code> field, we're querying for groups of firewall events, aggregated by the fields nested within <code>dimensions</code>. In this case, our query includes the <code>action</code> and <code>datetimeHour</code> fields, meaning the response will be groups of firewall events which share the same action, and fall within the same hour. We also add a <code>count</code> field, to get a numeric count of how many events fall within each group.</p>
            <pre><code>query FirewallEventsByTime($zoneTag: string, $filter: FirewallEventsAdaptiveGroupsFilter_InputObject) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      firewallEventsAdaptiveGroups(
        limit: 576
        filter: $filter
        orderBy: [datetimeHour_DESC]
      ) {
        count
        dimensions {
          action
          datetimeHour
        }
      }
    }
  }
}</code></pre>
            <p><i>Note - Each of our groups queries require a limit to be set. A firewall event can have one of 8 possible actions, and we are querying over a 72 hour period. At most, we’ll end up with 567 groups, so we can set that as the limit for our query.</i></p><p>This query would return a response in the following format:</p>
            <pre><code>{
  "viewer": {
    "zones": [
      {
        "firewallEventsAdaptiveGroups": [
          {
            "count": 5,
            "dimensions": {
              "action": "jschallenge",
              "datetimeHour": "2019-09-12T18:00:00Z"
            }
          }
          ...
        ]
      }
    ]
  }
}</code></pre>
            <p>We can then take these groups and plot each as a point on a time series chart. Mapping over the <code>firewallEventsAdaptiveGroups</code> array, we can use the group’s <code>count</code> property on the y-axis for our chart, then use the nested fields within the <code>dimensions</code> object, using <code>action</code> as unique series and the <code>datetimeHour</code> as the time stamp on the x-axis.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4RH0u6Cc6dIk9ztyZ3E0Ag/21256377669133e2416150a4b1798e85/pasted-image-0--1--3.png" />
            
            </figure>
    <div>
      <h3>Top Ns</h3>
      <a href="#top-ns">
        
      </a>
    </div>
    <p>After identifying a spike in activity, our next step is to highlight events with commonality in their attributes. For example, if a certain IP address or individual user agent is causing many firewall events, this could be a sign of an individual attacker, or could be surfacing a false positive.</p><p>Similarly to before, we can query aggregate groups of firewall events using the <code>firewallEventsAdaptiveGroups</code> field. However, in this case, instead of supplying <code>action</code> and <code>datetimeHour</code> to the group’s <code>dimensions</code>, we can add individual fields that we want to find common groups of.</p><p>By ordering by descending count, we’ll retrieve groups with the highest commonality first, limiting to the top 5 of each. We can add a single field nested within <code>dimensions</code> to group by it. For example, adding <code>clientIP</code> will give five groups with the IP addresses causing the most events.</p><p>We can also add a <code>firewallEventsAdaptiveGroups</code> field with no nested <code>dimensions</code>. This will create a single group which allows us to find the total count of events matching our filter.</p>
            <pre><code>query FirewallEventsTopNs($zoneTag: string, $filter: FirewallEventsAdaptiveGroupsFilter_InputObject) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      topIPs: firewallEventsAdaptiveGroups(
        limit: 5
        filter: $filter
        orderBy: [count_DESC]
      ) {
        count
        dimensions {
          clientIP
        }
      }
      topUserAgents: firewallEventsAdaptiveGroups(
        limit: 5
        filter: $filter
        orderBy: [count_DESC]
      ) {
        count
        dimensions {
          userAgent
        }
      }
      total: firewallEventsAdaptiveGroups(
        limit: 1
        filter: $filter
      ) {
        count
      }
    }
  }
}</code></pre>
            <p><i>Note - we can add the </i><code><i>firewallEventsAdaptiveGroups</i></code><i> field multiple times within a single query, each aliased differently. This allows us to fetch multiple different groupings by different fields, or with no groupings at all. In this case, getting a list of top IP addresses, top user agents, and the total events.</i></p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/48ybDlyb9qiOXRcqbxYMBT/ef8646776579eae9468db6b0e34dd7cc/pasted-image-0--2--1.png" />
            
            </figure><p>We can then reference each of these aliases in the UI, mapping over their respective groups to render each row with its count, and a bar which represents the proportion of total events, showing the proportion of all events each row equates to.</p>
    <div>
      <h3>Are these firewall events false positives?</h3>
      <a href="#are-these-firewall-events-false-positives">
        
      </a>
    </div>
    <p>After users have identified spikes, anomalies and common attributes, we wanted to surface more information as to whether these have been caused by malicious traffic, or are false positives.</p><p>To do this, we wanted to provide additional context on the events themselves, rather than just counts. We can do this by querying the <code>firewallEventsAdaptive</code> field for these events.</p><p>Our GraphQL schema uses the same filter format for both the aggregate <code>firewallEventsAdaptiveGroups</code> field and the raw <code>firewallEventsAdaptive</code> field. This allows us to use the same filters to fetch the individual events which summate to the counts and aggregates in the visualisations above.</p>
            <pre><code>query FirewallEventsList($zoneTag: string, $filter: FirewallEventsAdaptiveFilter_InputObject) {
  viewer {
    zones(filter: { zoneTag: $zoneTag }) {
      firewallEventsAdaptive(
        filter: $filter
        limit: 10
        orderBy: [datetime_DESC]
      ) {
        action
        clientAsn
        clientCountryName
        clientIP
        clientRequestPath
        clientRequestQuery
        datetime
        rayName
        source
        userAgent
      }
    }
  }
}</code></pre>
            
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5BmadcWdFvLxALf8fO3MbW/3d8878f086b962e671162b6d62e4fc7f/pasted-image-0--3--1.png" />
            
            </figure><p>Once we have our individual events, we can render all of the individual fields we’ve requested, providing users the additional context on event they need to determine whether this is a false positive or not.</p><p>That’s how we used our new GraphQL Analytics API to build Firewall Analytics, helping solve some of our customers most common security workflow use cases. We’re excited to see what you build with it, and the problems you can help tackle.</p><p>You can find out how to get started querying our GraphQL Analytics API using GraphiQL in our <a href="https://developers.cloudflare.com/analytics/graphql-api/getting-started/">developer documentation</a>, or learn more about writing GraphQL queries on the official GraphQL Foundation <a href="https://graphql.org/learn/queries/">documentation</a>.</p> ]]></content:encoded>
            <category><![CDATA[Product News]]></category>
            <category><![CDATA[Analytics]]></category>
            <category><![CDATA[API]]></category>
            <category><![CDATA[GraphQL]]></category>
            <guid isPermaLink="false">3I5BKE6KqU328LA4QoiQQX</guid>
            <dc:creator>Nick Downie</dc:creator>
        </item>
        <item>
            <title><![CDATA[Introducing the GraphQL Analytics API: exactly the data you need, all in one place]]></title>
            <link>https://blog.cloudflare.com/introducing-the-graphql-analytics-api-exactly-the-data-you-need-all-in-one-place/</link>
            <pubDate>Thu, 12 Dec 2019 15:41:04 GMT</pubDate>
            <description><![CDATA[ With our new GraphQL Analytics API, all of your performance, security, and reliability data is available from one endpoint, and you can select exactly what you need. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Today we’re excited to announce a powerful and flexible new way to explore your Cloudflare metrics and logs, with an API conforming to the industry-standard <a href="https://graphql.org/">GraphQL specification</a>. With our new GraphQL Analytics API, all of your performance, security, and reliability data is available from one endpoint, and you can select exactly what you need, whether it’s one metric for one domain or multiple metrics aggregated for all of your domains. You can ask questions like <i>“How many cached bytes have been returned for these three domains?”</i> Or, <i>“How many requests have all the domains under my account received?”</i> Or even, <i>“What effect did changing my firewall rule an hour ago have on the responses my users were seeing?”</i></p><p>The GraphQL standard also has strong <a href="https://graphql.org/community/">community resources</a>, from extensive documentation to front-end clients, making it easy to start creating simple queries and progress to building your own sophisticated analytics dashboards.</p>
    <div>
      <h3>From many APIs...</h3>
      <a href="#from-many-apis">
        
      </a>
    </div>
    <p>Providing insights has always been a core part of Cloudflare’s offering. After all, by using Cloudflare, you’re relying on us for key parts of your infrastructure, and so we need to make sure you have the data to manage, <a href="https://www.cloudflare.com/application-services/solutions/app-performance-monitoring/">monitor</a>, and troubleshoot your website, app, or service. Over time, we developed a few key data APIs, including ones providing information regarding your domain’s traffic, DNS queries, and firewall events. This multi-API approach was acceptable while we had only a few products, but we started to run into some challenges as we added more products and analytics. We couldn’t expect users to adopt a new analytics API every time they started using a new product. In fact, some of the customers and partners that were relying on many of our products were already becoming confused by the various APIs.</p><p>Following the multi-API approach was also affecting how quickly we could develop new analytics within the Cloudflare dashboard, which is used by more people for data exploration than our APIs. Each time we built a new product, our product engineering teams had to implement a corresponding analytics API, which our user interface engineering team then had to learn to use. This process could take up to several months for each new set of analytics dashboards.</p>
    <div>
      <h3>...to one</h3>
      <a href="#to-one">
        
      </a>
    </div>
    <p>Our new GraphQL Analytics API solves these problems by providing access to all Cloudflare analytics. It offers a standard, flexible syntax for describing exactly the data you need and provides predictable, matching responses. This approach makes it an ideal tool for:</p><ol><li><p>Data exploration. You can think of it as a way to query your own virtual data warehouse, full of metrics and logs regarding the performance, security, and reliability of your Internet property.</p></li><li><p>Building amazing dashboards, which allow for flexible filtering, sorting, and drilling down or rolling up. Creating these kinds of dashboards would normally require paying thousands of dollars for a specialized analytics tool. You get them as part of our product and can customize them for yourself using the API.</p></li></ol><p>In a companion post that was also published today, my colleague Nick discusses using the GraphQL Analytics API to build dashboards. So, in this post, I’ll focus on examples of how you can use the API to explore your data. To make the queries, I’ll be using <a href="https://electronjs.org/apps/graphiql"><i>GraphiQL</i></a>, a popular open-source querying tool that takes advantage of GraphQL’s capabilities.</p>
    <div>
      <h3>Introspection: what data is available?</h3>
      <a href="#introspection-what-data-is-available">
        
      </a>
    </div>
    <p>The first thing you may be wondering: if the GraphQL Analytics API offers access to so much data, how do I figure out what exactly is available, and how I can ask for it? GraphQL makes this easy by offering “introspection,” meaning you can query the API itself to see the available data sets, the fields and their types, and the operations you can perform. <i>GraphiQL</i> uses this functionality to provide a “Documentation Explorer,” query auto-completion, and syntax validation. For example, here is how I can see all the data sets available for a zone (domain):</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6v9HqDr6UGaifduopecAMw/6f89f52d3ca080c7385710598bd8b26e/image1.gif" />
            
            </figure><p>If I’m writing a query, and I’m interested in data on firewall events, auto-complete will help me quickly find relevant data sets and fields:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5RSkIKlSwBMy7BVZWPWp0q/c605fa07bb8997758e2e84cbb47406f9/image6.gif" />
            
            </figure>
    <div>
      <h3>Querying: examples of questions you can ask</h3>
      <a href="#querying-examples-of-questions-you-can-ask">
        
      </a>
    </div>
    <p>Let’s say you’ve made a major product announcement and expect a surge in requests to your blog, your application, and several other zones (domains) under your account. You can check if this surge materializes by asking for the requests aggregated under your account, in the 30 minutes after your announcement post, broken down by the minute:</p>
            <pre><code>{
 viewer { 
   accounts (filter: {accountTag: $accountTag}) {
     httpRequests1mGroups(limit: 30, filter: {datetime_geq: "2019-09-16T20:00:00Z", datetime_lt: "2019-09-16T20:30:00Z"}, orderBy: [datetimeMinute_ASC]) {
	  dimensions {
		datetimeMinute
	  }
	  sum {
		requests
	  }
	}
   }
 }
}</code></pre>
            <p>Here is the first part of the response, showing requests for your account, by the minute:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7hsWUNy86iymsBlbiOfjL5/054c2bce4d1fef4090e56f264fee7aec/Screen-Shot-2019-09-17-at-2.21.41-PM.png" />
            
            </figure><p>Now, let’s say you want to compare the traffic coming to your blog versus your marketing site over the last hour. You can do this in one query, asking for the number of requests to each zone:</p>
            <pre><code>{
 viewer {
   zones(filter: {zoneTag_in: [$zoneTag1, $zoneTag2]}) {
     httpRequests1hGroups(limit: 2, filter: {datetime_geq: "2019-09-16T20:00:00Z",
datetime_lt: "2019-09-16T21:00:00Z"}) {
       sum {
         requests
       }
     }
   }
 }
}</code></pre>
            <p>Here is the response:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1piQ5uNUOVcsX3AqEADplu/08f469a6a92b8df8a2e59e523aa497b0/Screen-Shot-2019-12-11-at-4.04.37-PM.png" />
            
            </figure><p>Finally, let’s say you’re seeing an increase in error responses. Could this be correlated to an attack? You can look at error codes and firewall events over the last 15 minutes, for example:</p>
            <pre><code>{
 viewer {
   zones(filter: {zoneTag: $zoneTag}) {
     httpRequests1mGroups (limit: 100,
filter: {datetime_geq: "2019-09-16T21:00:00Z",
datetime_lt: "2019-09-16T21:15:00Z"}) {
       sum {
         responseStatusMap {
           edgeResponseStatus
           requests
         }
       }
     }
    firewallEventsAdaptiveGroups (limit: 100,
filter: {datetime_geq: "2019-09-16T21:00:00Z",
datetime_lt: "2019-09-16T21:15:00Z"}) {
       dimensions {
         action
       }
       count
     }
    }
  }
}</code></pre>
            <p>Notice that, in this query, we’re looking at multiple datasets at once, using a common zone identifier to “join” them. Here are the results:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7G5to39LSIQSOtv9KgIzIL/78f15ef423f901c4e027ef7cb67cddd8/Screen-Shot-2019-12-11-at-4.28.28-PM.png" />
            
            </figure><p>By examining both data sets in parallel, we can see a correlation: 31 requests were “dropped” or blocked by the Firewall, which is exactly the same as the number of “403” responses. So, the 403 responses were a result of Firewall actions.</p>
    <div>
      <h3>Try it today</h3>
      <a href="#try-it-today">
        
      </a>
    </div>
    <p>To learn more about the GraphQL Analytics API and start exploring your Cloudflare data, follow the “Getting started” guide in our <a href="https://developers.cloudflare.com/analytics/graphql-api/getting-started/">developer documentation</a>, which also has details regarding the current data sets and time periods available. We’ll be adding more data sets over time, so take advantage of the introspection feature to see the latest available.</p><p>Finally, to make way for the new API, the Zone Analytics API is now deprecated and will be sunset on May 31, 2020. The data that Zone Analytics provides is available from the GraphQL Analytics API. If you’re currently using the API directly, please follow our <a href="https://developers.cloudflare.com/analytics/migration-guides/zone-analytics/">migration guide</a> to change your API calls. If you get your analytics using the Cloudflare dashboard or our <a href="https://docs.datadoghq.com/integrations/cloudflare/">Datadog integration</a>, you don’t need to take any action.</p>
    <div>
      <h3>One more thing....</h3>
      <a href="#one-more-thing">
        
      </a>
    </div>
    <p>In the API examples above, if you find it helpful to get analytics aggregated for all the domains under your account, we have something else you may like: a brand new <a href="https://dash.cloudflare.com/?account=analytics">Analytics dashboard</a> (in beta) that provides this same information. If your account has many zones, the dashboard is helpful for knowing summary information on metrics such as requests, bandwidth, cache rate, and error rate. Give it a try and let us know what you think using the feedback link above the new dashboard.</p> ]]></content:encoded>
            <category><![CDATA[Analytics]]></category>
            <category><![CDATA[Product News]]></category>
            <category><![CDATA[API]]></category>
            <category><![CDATA[GraphQL]]></category>
            <category><![CDATA[Developers]]></category>
            <guid isPermaLink="false">4pDXIFEVbZmJSNDNttfleG</guid>
            <dc:creator>Filipp Nisenzoun</dc:creator>
        </item>
        <item>
            <title><![CDATA[Building a GraphQL server on the edge with Cloudflare Workers]]></title>
            <link>https://blog.cloudflare.com/building-a-graphql-server-on-the-edge-with-cloudflare-workers/</link>
            <pubDate>Wed, 14 Aug 2019 03:21:00 GMT</pubDate>
            <description><![CDATA[ Today, we're open-sourcing an exciting project that showcases the strengths of our Cloudflare Workers platform: workers-graphql-server is a batteries-included Apollo GraphQL server, designed to get you up and running quickly with GraphQL. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Today, we're open-sourcing an exciting project that showcases the strengths of our Cloudflare Workers platform: <code>workers-graphql-server</code> is a batteries-included <a href="https://apollographql.com">Apollo GraphQL</a> server, designed to get you up and running quickly with <a href="https://graphql.com">GraphQL</a>.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1AJH4z33GBgzCVmgEAuaSQ/e7643db81ee32fef75113d0bc22df3e8/Screen-Shot-2019-08-14-at-11.05.06-AM.png" />
            
            </figure><p>Testing GraphQL queries in the GraphQL Playground</p><p>As a full-stack developer, I’m really excited about GraphQL. I love building user interfaces with <a href="https://reactjs.org">React</a>, but as a project gets more complex, it can become really difficult to manage how your data is managed inside an application. GraphQL makes that really easy — instead of having to recall the REST URL structure of your backend API, or remember when your backend server doesn't <i>quite</i> follow REST conventions — you just tell GraphQL what data you want, and it takes care of the rest.</p><p>Cloudflare Workers is uniquely suited as a platform to being an incredible place to <a href="https://www.cloudflare.com/developer-platform/solutions/hosting/">host</a> a GraphQL server. Because your code is running on Cloudflare's servers around the world, the average latency for your requests is extremely low, and by using <a href="https://github.com/cloudflare/wrangler">Wrangler</a>, our open-source command line tool for building and managing Workers projects, you can deploy new versions of your GraphQL server around the world within seconds.</p><p>If you'd like to try the GraphQL server, check out a demo GraphQL playground, <a href="https://graphql-on-workers.signalnerve.com/___graphql">deployed on Workers.dev</a>. This optional add-on to the GraphQL server allows you to experiment with GraphQL queries and mutations, giving you a super powerful way to understand how to interface with your data, without having to hop into a codebase.</p><p>If you're ready to get started building your own GraphQL server with our new open-source project, we've added a new tutorial to our <a href="https://workers.cloudflare.com/docs">Workers documentation</a> to help you get up and running — <a href="https://developers.cloudflare.com/workers/get-started/quickstarts#frameworks">check it out here</a>!</p><p>Finally, if you're interested in <i>how</i> the project works, or want to help contribute — it's open-source! We'd love to hear your feedback and see your contributions. Check out the project <a href="https://github.com/signalnerve/workers-graphql-server">on GitHub</a>.</p> ]]></content:encoded>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Serverless]]></category>
            <category><![CDATA[GraphQL]]></category>
            <category><![CDATA[Developers]]></category>
            <category><![CDATA[JavaScript]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <guid isPermaLink="false">l68Fsw4jkO2iFbriaKvyD</guid>
            <dc:creator>Kristian Freeman</dc:creator>
        </item>
    </channel>
</rss>