Tag Archives: CDN

Office 365 CDN – Some Notes and Sample Scripts

The Office 365 CDN (Content Delivery Networks) may be activated to host SharePoint Online files in a more globally accessible manner. The general premise behind this is that static assets can be served to users from a location more local to them than the data centre in which the Office 365 tenant is located.

I won’t go into the real benefits of this beyond to say that my limited testing at this point leads me to believe that the performance impact of using a CDN will be negligible for the vast majority of users/organisations. This is because the volume of data which can be served via the CDN is not a significant proportion of the data impacting page load speed.

Regardless, the documentation around how to get started with the Office 365 CDN is decent. A good place to start is this link.

Private CDN with auto-rewrite
Private CDN with auto-rewrite. Image credit to Microsoft (https://dev.office.com/blogs/general-availability-of-office-365-cdn)

A couple of gotchas I’ve noticed

  • Fetching an image rendition using the width query string parameter does NOT correctly return the image rendition as configured. It simply scales the image to the specified width (i.e. no cropping or positioning is performed).
  • If all users are located in the same region as the Office 365 tenant, turning on the CDN may reduce performance due to CDN priming (replication of files to the CDN) and will complicate updates to files which are replicated (e.g. JavaScript in the Style Library).
  • Search web parts must be configured for ‘Loading Behaviour’ – ‘Sync option: Issue query from the server’ in order for the auto rewrite of CDN hosted files to occur. This is true for display templates as well as the value of the PublishingImage managed property

Office 365 CDN PowerShell Samples

I’ve got some sample PowerShell below showing how to activate the Office 365 CDN (there’s private and public, you can use either or both) and associate origins with it (an origin is a document library which will be replicated to the CDN).

I’ve also got a simple sample of how to remove all origins as there is not a single cmdlet for this. It is worth noting that although an enabled CDN with no origins is functionally identical to a disabled CDN (i.e. no files are being replicated) they are not the same from a configuration perspective.

Please note that these are just sample scripts and have not been parameterised as you may require.

Calling the PowerShell functions:

$cdnType = "Private" # Private or Public
$serverRelSiteCollectionUrl = "/sites/mysite" # site collection URL or * for all site collections


Set-CdnConfiguration $serverRelSiteCollectionUrl $cdnType

#Remove-CdnConfiguration $cdnType # This removes all origins but the CDN is still enabled
#Set-SPOTenantCdnEnabled -CdnType $cdnType -Enable $false # This disables the CDN

The PowerShell functions:

Function Authenticate-PowerShell() {
	[string]$tenantUrl = "https://TENANT-admin.sharepoint.com"
	[string]$adminUsername = "USER@TENANT.onmicrosoft.com"
	[string]$adminPassword = "PASSWORD"

	# Ensure module is loaded
	if ((Get-Module Microsoft.Online.SharePoint.PowerShell).Count -eq 0) {
		Import-Module Microsoft.Online.SharePoint.PowerShell -DisableNameChecking

	$secureAdminPassword = $(convertto-securestring $adminPassword -asplaintext -force)
	$cred = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist $adminUsername, $secureAdminPassword
	Connect-SPOService -Url $tenantUrl -credential $cred

Function Set-CdnConfiguration($serverRelSiteCollectionUrl, $cdnType){
    #LogWaiting "Configuring CDN"
    #LogInfo ""

    $cdnOrigins = @(
        "$serverRelSiteCollectionUrl/style library",  

    # Enable cdn WITHOUT default origins
    $supressOutput = Set-SPOTenantCdnEnabled -CdnType $cdnType -Enable $true -NoDefaultOrigins -Confirm:$false

    # Configure cdn origins (incl ensure default origins)
    Ensure-CdnOrigin $cdnType $cdnOrigins

    # Extend list of file types
    $supressOutput = Set-SPOTenantCdnPolicy -CdnType $cdnType -PolicyType IncludeFileExtensions -PolicyValue $fileTypes

    #LogSuccess "done"

    # Print status
    Get-SPOTenantCdnOrigins -CdnType $cdnType

Function Ensure-CdnOrigin($cdnType, $originUrls){
  $originUrls | ForEach {
    $oUrl = $_
    try {
      #LogWaiting "Adding origin: $oUrl"
      $supressOutput = Add-SPOTenantCdnOrigin -CdnType $cdnType -OriginUrl $oUrl -Confirm:$false
    catch {
      if($Error[0].Exception.ToString().Contains("The library is already registered as a CDN origin")) {
        # aleady present, do nothing
      else {
        #LogError $Error[0]
    #LogSuccess "done"

Function Remove-CdnConfiguration($cdnType){
	(Get-SPOTenantCdnOrigins -CdnType $cdnType)	| ForEach { $_ | ForEach { $supress = Remove-SPOTenantCdnOrigin -CdnType $cdnType -OriginUrl $_ -Confirm:$false }}


Azure CDN integration with SharePoint, cache control headers max-age, s-maxage

After recently implementing an Azure-based solution to mitigate SharePoint Online’s poor image rendition performance by utilising Azure CDN (see Chris O’Brien’s post on this issue, see Fran R’s post on other Image Rendition issues) I’ve reached a few conclusions regarding setting appropriate cache control headers. It is important to reach a practical balance between performance and receiving updates to files.

Azure CDN logo

Before continuing it is important to understand the fundamental building blocks when using a CDN. At any time a file can be present in three location types: the blob or source file, the CDN endpoint(s), and users’ browser caches. In the case of Azure CDN, the source file must be a blob in Azure Blob Storage. Depending on the CDN/configuration it is likely that the file may be cached at many (dozens) of CDN endpoints dispersed around the globe. Without a CDN the only consideration is the cache timeout for files stored at the user’s browser cache. When considering a CDN we must also consider the cache timeout between the CDN endpoint and the source file.

Another important point to call out is that CDNs generally only push content to an endpoint when is it first requested: on-demand. This will incur a delay for the first user to request that asset from a given endpoint, while source blob is transferred to the endpoint. The impact of this will differ depending on the distance between the source blob and the CDN endpoint and the file size. It is this process that increasing the s-maxage header prevents (discussed below).

Relevant cache control headers


  • max-age : Defines the period which, until reached, the client will used the cached file without contacting the server. ‘Client’ refers to a user’s browser cache as well as a CDN.
  • s-maxage : If provided, overrides max-age for CDNs only
  • public : Explicitly marks the file as not user specific
  • no-transform : Proxy servers may compress or encode images to improve performance or reduce bandwidth traffic. This header prevents this for occurring. It is preferable to avoid this header assuming that you can spare the effort to ensure the files being served are not affected adversely.

A good summary of the many remaining cache control headers that I didn’t feel were relevant to this post can be found here:
A beginners guide to HTTP cache headers

In practice

  • For an image that has been previously requested:
    • When s-maxage has not expired and max-age has not expired, server responds with 200 (OK), the file is not downloaded again [0ms]
    • When s-maxage has not expired but max-age has expired, server responds with 304 (not modified), the file is not downloaded again [<100ms]
    • When s-maxage has expired but max-age has not expired, server responds with 200 (OK), the file is not downloaded again [0ms]
    • When s-maxage has expired and max-age has expired and the blob has not changed, server responds with 304 (not modified), the file is not downloaded again [<100ms]
    • When s-maxage has expired and max-age has expired and the blob has changed, server responds with 200 (OK), the file is downloaded again [download image]
  • A request for an image will return 200 (OK) until max-age has expired and then 304 (not modified) for every subsequent request until the blob is updated. Once updated, this process repeats
  • If an existing image is updated, the longest a user can wait to see the updated image is
    • Without clearing browser cache: max-age + s-maxage
    • With clearing browser cache: s-maxage
  • If an user views an image from the CDN for the first time, it is only guaranteed to be the latest version of that image if the blob hasn’t been updated in the last s-maxage
  • SharePoint library images are served with a max-age of 24 hours
  • As SharePoint library images are not served via a CDN they have an effective s-maxage of 0

My recommendations

Keeping all of the above in mind, I feel that the most important factor is to replicate the experience that users expect from images being served from the SharePoint environment. This can presented as a couple of simple rules:

  1. max-age + s-maxage = 24 hours = 86400 seconds
  2. s-maxage is as low as possible whilst satisfying bandwidth and performance targets (especially for locations most distant to the source blob)

For a recent SharePoint/CDN, I used the following cache control headers:

  • max-age: 23 hours
  • s-maxage: 1 hour
  • public
  • no-transform

Which looks like this:

Setting the cache headers served by Azure CDN and Azure Blob Storage

When working with cache control headers in Azure, they are set on the blob itself. It is not a CDN configuration setting.