Improving Time To First Byte (TTFB) With Cloudflare

TTFB speed is relative so if your origin web server is located further away from test location then TTFB is higher and vice versa. Also TTFB can be impacted by client side factors like the speed and CPU power of the client device connecting to the server or even what the client device is doing in background (like watching a video).

For relative distance factors, this is due to 2 reasons:

  1. Cloudflare not caching HTML content by default (see below). So for optimal TTFB speed, you want your origin real web server to be hosted in a location closest to your majority traffic visitors and then put Cloudflare in front. For instance, my forums has 50% US visitors 40% Asian visitors and 10% Oceania. So my optimal geographic location for my origin is US West Coast as it sits in middle of US, Europe and Asian so equal round trip times for majority of visitors. Cloudflare cache certain static content Default Cache Behavior · Cloudflare Cache (CDN) docs but not dynamic/static generated html itself by default (which is what Webpagetest.org TTFB is testing for). But you can tell Cloudflare to cache dynamic/static generated html content to some extent depending on Cloudflare plan you’re on via cache everything page rule but have to be careful to only do this for static html content and not dynamic html content (otherwise you would cache private logged in user content).

  2. The higher the Cloudflare plan you go, the better the routing and ISP/network peering gets. Cloudflare refers to this on their Plan’s page Our Plans | Pricing compare all features link display as Network Prioritization. Due to varying ISP costs around the world Bandwidth Costs Around the World. The latest example is Cloudflare’s Guam datacenter deployment and peering and it’s impact on network latencies. As it may cost Cloudflare more money to serve a request from some geographic datacenters than others, financially Cloudflare can’t always route visitors to the nearest datacenter. For instance some geographical locations which historically showed such cases are India and Australia. On Cloudflare free plan visitors from India or Australia maybe routed to Cloudflare’s Singapore or Los Angeles data centers due to lower cost. As you moved up the Cloudflare plan tiers to more paid plans, the higher probability of getting routed closer to Cloudflare datacenters in the same region for such high cost countries. My experience with using Cloudflare for past 11yrs on CF Free, Pro, Business and Enterprise plans, is if you want Australian visitors routed to Cloudflare Australian datacenters, you’d need at least Cloudflare Business for higher probability of such. But for Indian visitors to be routed to Cloudflare India datacenters, you’d need Cloudflare Enterprise plan for higher probability. I say higher probability as it still isn’t guaranteed. Though over the years, the probability has gotten better as peering arrangements have improved I suspect Network performance update: Platform Week. The link at https://cloudflare-test.judge.sh/ is a good guide to how peering is for different Cloudflare plan based sites too. Only focus on the reported data center served from various plans and not the actual metrics as variance can be due to many factors - including whether or not each site on each CF plan has already implemented some of the below mentioned TTFB optimizations which will impact the reported metrics. There’s also an extended explanation at Explanation · judge2020/cloudflare-connectivity-test Wiki · GitHub

Users have also reported for Vietnam visitors, they also maybe routed elsewhere like Hong Kong or Singapore due the above mentioned peering agrement/costs Cloudflare at vietnam - #5 by soldier_21. This seems to be the case even for Cloudflare Enterprise.

As it relates to TTFB improvements:

Older thread cover some of the page speed metrics at Performance Tutorials - Google PageSpeed & Webpagetest.org. Another guide I wrote outlined for Google Lighthouse/Pagesped Insight for my Centmin Mod community users which is same engine GTMetrix has switched to at https://community.centminmod.com/threads/google-page-speed-insights-and-google-core-web-vital-metrics.20735/.

To fully optimise you need to optimize 3 segments.

  1. segment 1 - connection between visitor and CF edge server i.e. CDN cache/CDN cache control, Cache Rules, WAF, Firewall, Page Rules, Mirage, Polish webP, HTTP/2, HTTP/3, CF Workers (i.e. custom/advanced caching) etc. Cloudflare CDN level caching effectively reduces your origin server resource usage loads for CPU/Memory etc as request load is offloaded to Cloudflare CDN edge servers in an Anycast manner to the closest CF datacenter to the visitor.

  2. segment 2 - connection between CF edge server and your origin i.e. TLSv1.3 origin server support, Argo, Railgun, Full SSL/ECDSA SSL certificates origin served, pre-compressed/dynamically compressed gzip and/or brotli encoded asset served from origin, Tiered Caching and Cache Reserve eventually once out of Beta (right now TTFB ends up slower with Cache Reserve beta enabled).

  3. segment 3 - your origin server’s performance/optimisations i.e. web server, PHP, MySQL server optimisations and server hardware specs. Origin server side caching, PHP Zend Opcache caching, MySQL’s various buffer caching and various other origin server side caching can also be used.

Cloudflare can only help for segments 1 & 2 for cached guest/non-logged based visitors will easily scale. Now for Cloudflare CDN cache miss/bypass and logged in user for web apps like forums/wordpress, performance will be determined by segment 3. Which is the default with Cloudflare as dynamically generated HTML pages aren’t cached by default so cache miss means, whatever performance you have is measuring your origin server’s response time.

15 Likes

To add additional info for TTFB, Cloudflare Speed Week 2023, has also added GraphQL Timings Insight API where you can query your Cloudflare orange cloud enabled proxied web site’s TTFB metric as outlined in Cloudflare blog post at Introducing Timing Insights: new performance metrics via our GraphQL API. FYI, the blog’s GraphQL API queries are incorrect though and will error out if you try them.

Below are some working examples. Instead of blog GraphQL query

query TTFBQuantiles($zoneTag: string) {
  viewer {
    zones(filter: {zoneTag: $zoneTag}) {
      httpRequestsAdaptiveGroups {
        quantiles {
          edgeTimeToFirstByteMsP50
          edgeTimeToFirstByteMsP95
          edgeTimeToFirstByteMsP99
        }
      }
    }
  }
}

Try this one

query TTFBQuantiles($zoneTag: string) {
  viewer {
    zones(filter: {zoneTag: $zoneTag}) {
      httpRequestsAdaptiveGroups(
			limit: 10, filter: {date_gt: "2023-06-19"}
			) {
        quantiles {
          edgeTimeToFirstByteMsP50
          edgeTimeToFirstByteMsP95
          edgeTimeToFirstByteMsP99
        }
      }
    }
  }
}

which will give example output

{
	"data": {
		"viewer": {
			"zones": [
				{
					"httpRequestsAdaptiveGroups": [
						{
							"quantiles": {
								"edgeTimeToFirstByteMsP50": 45,
								"edgeTimeToFirstByteMsP95": 110,
								"edgeTimeToFirstByteMsP99": 219
							}
						}
					]
				}
			]
		}
	},
	"errors": null
}

And instead of blog GraphQL query

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
        }
      }
    }
  }
}

Try this one instead - I added additional dimensions to query too. You can change the order by from sum sum_edgeTimeToFirstByteMs_DESC to averages avg_edgeTimeToFirstByteMs_DESC and replace YOURDOMAIN.com with your domain and the desired greater than date date_gt and limit results value.

I filtered only on edgeResponseContentTypeName = html MIME content type to know my web page’s TTFB

query slowestURLs($zoneTag: string) {
  viewer {
    zones(filter: {zoneTag: $zoneTag}) {
      httpRequestsAdaptiveGroups(
				limit: 2, 
				filter: {
					date_gt: "2023-06-20",
					cacheStatus_in: [ "miss", "hit", "dynamic" ],
					edgeTimeToFirstByteMs_gt: 200,
					edgeResponseStatus: 200,
					clientRequestHTTPHost_in: [ "YOURDOMAIN.com" ],
					edgeResponseContentTypeName: "html",
					clientCountryName_neq: "XX"
				},
				orderBy: [ sum_edgeTimeToFirstByteMs_DESC ]
			) 
			  {
				avg {
          edgeDnsResponseTimeMs
					edgeTimeToFirstByteMs
					originResponseDurationMs
				}
				count
				quantiles {
          edgeTimeToFirstByteMsP50
          edgeTimeToFirstByteMsP95
          edgeTimeToFirstByteMsP99
					edgeDnsResponseTimeMsP50
					edgeDnsResponseTimeMsP95
					edgeDnsResponseTimeMsP99
					originResponseDurationMsP50
					originResponseDurationMsP95
					originResponseDurationMsP99
				}
        sum {
          edgeTimeToFirstByteMs
        }
        dimensions {
					cacheReserveUsed
					cacheStatus
					clientCountryName
					clientRequestPath
					coloCode
					clientDeviceType
					edgeResponseContentTypeName
					upperTierColoName
        }
      }
    }
  }
}

Outputs the slowest url is for / web root for a German visitor on desktop with Cloudflare cache status = dynamic not cached that hit Cloudflare colo datacenter in France where my Cloudflare Tiered Cache upper colocode is SFO-DOG for San Francisco which is expected as my origin server is in US West coast.

The average response time metrics for this request for edgeDnsResponseTimeMs is 0.38ms, edgeTimeToFirstByteMs is 433.46ms and originResponseDurationMs is 381.9047619047619 ms with 99% quantile edgeTimeToFirstByteMsP99 being 700ms and 99% quantile originResponseDurationMsP99 is 624ms.

{
  "data": {
    "viewer": {
      "zones": [
        {
          "httpRequestsAdaptiveGroups": [
            {
              "avg": {
                "edgeDnsResponseTimeMs": 0.38,
                "edgeTimeToFirstByteMs": 433.46,
                "originResponseDurationMs": 381.9047619047619
              },
              "count": 50,
              "dimensions": {
                "cacheReserveUsed": 0,
                "cacheStatus": "dynamic",
                "clientCountryName": "DE",
                "clientDeviceType": "desktop",
                "clientRequestPath": "/",
                "coloCode": "FRA",
                "edgeResponseContentTypeName": "html",
                "upperTierColoName": "SFO-DOG"
              },
              "quantiles": {
                "edgeDnsResponseTimeMsP50": 0,
                "edgeDnsResponseTimeMsP95": 2,
                "edgeDnsResponseTimeMsP99": 2,
                "edgeTimeToFirstByteMsP50": 408,
                "edgeTimeToFirstByteMsP95": 591,
                "edgeTimeToFirstByteMsP99": 700,
                "originResponseDurationMsP50": 335,
                "originResponseDurationMsP95": 496,
                "originResponseDurationMsP99": 624
              },
              "sum": {
                "edgeTimeToFirstByteMs": 21673
              }
            },
            {
              "avg": {
                "edgeDnsResponseTimeMs": 0,
                "edgeTimeToFirstByteMs": 369.2972972972973,
                "originResponseDurationMs": -1
              },
              "count": 37,
              "dimensions": {
                "cacheReserveUsed": 0,
                "cacheStatus": "hit",
                "clientCountryName": "IN",
                "clientDeviceType": "desktop",
                "clientRequestPath": "/",
                "coloCode": "BOM",
                "edgeResponseContentTypeName": "html",
                "upperTierColoName": "SFO-DOG"
              },
              "quantiles": {
                "edgeDnsResponseTimeMsP50": 0,
                "edgeDnsResponseTimeMsP95": 0,
                "edgeDnsResponseTimeMsP99": 0,
                "edgeTimeToFirstByteMsP50": 241,
                "edgeTimeToFirstByteMsP95": 1221,
                "edgeTimeToFirstByteMsP99": 1221,
                "originResponseDurationMsP50": 0,
                "originResponseDurationMsP95": 0,
                "originResponseDurationMsP99": 0
              },
              "sum": {
                "edgeTimeToFirstByteMs": 13664
              }
            }
          ]
        }
      ]
    }
  },
  "errors": null
}