---
title: "WebOps"
slug: webops
category: delivery
term_status: ambiguous
evidence_status: verified
summary: "WebOps is a loose label for running websites: the path from DNS through CDN, TLS and web servers to the browser, operated by developers and marketers who share one site. No body defines it; vendors also use it as a product category."
audience: [Beginners, Web and CMS engineers, Site and marketing-technology teams, Agencies, Engineering leaders]
last_verified: 2026-10-19
key_takeaways:
  - "WebOps has two documented meanings: the engineering of running large websites (Velocity from 2008, O'Reilly's Web Operations in 2010) and a vendor category for cross-functional website teams (Pantheon, Acquia). No standard defines either."
  - "This page's emphasis, an editorial choice, is the request path from DNS to the browser and the mixed team of developers and editors that changes it."
  - "Every layer remembers: browsers, resolvers, CDNs, HSTS policies and certificates each keep state for a time you chose earlier. Plan changes around what you can purge and what you must wait out."
  - "Content publishes, cache rules, WAF rules and DNS records are production changes. Edge configuration can reach every location in minutes, so stage it and keep a kill switch."
  - "A shared cache must never store a personalised response: decide the cache key and the public or private policy per content class."
  - "Publicly trusted certificates now last at most 200 days, falling to 100 in 2027 and 47 in 2029: automate renewal and set alert thresholds relative to lifetime."
  - "Judge speed with field data at the 75th percentile, per template and device; averages and lab scores hide the slow tail."
related: [appops, seops, contentops, observabilityops, releaseops, cloudsecops, martechops, sre, devops]
---

## What WebOps is

### In 30 seconds

**WebOps** (web operations) is the work of keeping a website fast, secure, available and safely changeable: the domain and DNS,
the CDN and its caches, TLS certificates, web servers and reverse proxies, the build or CMS that produces pages, and the
measurements that show whether real visitors get a good experience.

**In one line: this page treats WebOps as operating the request path from DNS to browser for a site that both code and content
change every day.** That scope is an editorial choice, because the label is not standardised.

::: note Two meanings in circulation
1. **Web operations as engineering.** The engineers who ran large websites in the late 2000s;[^oreilly2010] O'Reilly's Velocity
   conference (from 2008) had two tracks, "Performance and Operations".[^velocity2007] [DevOps](../devops/) and [SRE](../sre/)
   now teach most of it.
2. **WebOps as a vendor category for website teams**, in use by 2019.[^pantheon2019] Pantheon defines it as an approach "that
   unites development, operations and content teams around shared workflows, tools and responsibilities";[^pantheonlc] Acquia
   calls it "the convergence of development, operations, and marketing teams".[^acquia]

This page takes meaning 1's technical scope and meaning 2's mixed team.
:::

**For** web and CMS engineers, marketing site teams, agencies, and leaders deciding who owns "the website".

### At a glance

| Item | Summary |
|---|---|
| Term status | **Ambiguous.** Two documented meanings; no standards body, foundation or vendor-neutral certification found (as of October 2026). Trade press has asked what it is: "it's still unclear to me what, exactly, WebOps is" (The New Stack, 2022).[^newstack2022] |
| Goals | Fast pages for real visitors, availability, secure transport, safe changes, known cost |
| In scope | Domains and DNS; CDN caching and purge; TLS certificate lifecycle; web servers and reverse proxies; static, Jamstack and CMS deployment; synthetic and real-user monitoring; WAF basics and bot traffic |
| Out of scope | Search impact → [SEOps](../seops/), [SEO Operations](../seo-operations/); editorial workflow → [ContentOps](../contentops/); application internals → [AppOps](../appops/); release strategy → [ReleaseOps](../releaseops/); telemetry platform → [ObservabilityOps](../observabilityops/); security depth → [DevSecOps](../devsecops/), [CloudSecOps](../cloudsecops/); compute at remote sites → [EdgeOps](../edgeops/) |
| Prerequisites | HTTP basics; [SysOps](../sysops/) helps |
| Key outputs | Site register, cache policy, monitored certificates, edge change procedure, SLIs, runbooks |

### The problem it solves

A visitor sees one website; behind it sit a registrar, a DNS provider, a CDN, a certificate authority, a host, a CMS or build
pipeline, third-party tags and the people who publish. Serious failures happen at the joins, and several are documented:

- During an attack, a Steam caching configuration "incorrectly cached web traffic for authenticated users"; pages for about
  34,000 users may have been shown to others.[^steam2015]
- One WAF rule with a backtracking regular expression exhausted CPU across Cloudflare's network for 27 minutes.[^cf2019]
- Slack's third attempt to enable DNSSEC caused a 24-hour outage for under 1% of users; a parent-zone record that resolvers
  cache for a day made its rollback worse.[^slack2021]

| Operating reality | Without an owned practice | With one |
|---|---|---|
| Who changes production | Developers, an agency, every editor; no shared record | Named change paths; one change log |
| Certificates | Renewed when someone remembers; Let's Encrypt stopped expiry e-mails on 4 June 2025[^leemail] | Automated renewal, checked from outside |
| Caching | Provider defaults; "purge everything" after each publish | A policy per content class; purge by tag |
| Speed | One lab score at launch | Field [p75](#glossary-and-mental-model) per template; budgets in CI |
| Incidents | "Is it us or the CDN?" | Layer-by-layer runbook; independent probes |

In the 2025 Web Almanac, only 48% of mobile and 56% of desktop websites had good Core Web Vitals.[^almanacperf]

### What WebOps is not: the naming problem and the boundaries

- **Not a standard, a certification or one vendor's product**, although Pantheon and Acquia use the word for their platforms and
  services.[^pantheonwebops][^acquia]
- **Not just hosting.** The failures above were configuration, not servers.
- **Not simply DevOps renamed, though close.** The New Stack's Richard MacManus wrote that "in some ways, it feels like WebOps is
  just a trendy term for a long-familiar process in website content management".[^newstack2022] What differs: editors and
  marketers change production daily, and much of the path is rented from providers.

## Where it comes from and where it sits

### Origins and evolution

| Year | Milestone | What it shows |
|---|---|---|
| 2008 | First Velocity conference, billed as "where to learn how to build websites that are fast, scalable, resilient, and highly-available"[^velocity2007] | One web performance and operations community |
| 2009 | Allspaw and Hammond's Velocity talk "10+ Deploys A Day: Devs & Ops Cooperation at Flickr"[^dck2009] | Feeds the [DevOps](../devops/) movement |
| 2010 | *Web Operations*, edited by John Allspaw and Jesse Robbins: "Web Operations is work that matters"[^oreilly2010] | The engineering meaning in print |
| 2019 | Pantheon calls itself "the leading website operations platform (WebOps)"[^pantheon2019] | The vendor-category meaning |
| 2026–2029 | Certificate lifetimes fall in steps from 398 to 47 days (ballot passed 2025)[^br] | Certificate work becomes continuous |

### Glossary and mental model

| Term | Meaning on this page |
|---|---|
| **Origin** | The server or storage that produces the authoritative response |
| **CDN edge** | A provider location that terminates TLS, applies rules and serves cached copies |
| **Cache key** | What makes two requests "the same" for a cache: method and URI, plus headers named in `Vary`[^rfc9111] |
| **Stale controls** | `stale-while-revalidate` (serve stale while refreshing) and `stale-if-error` (serve stale on errors)[^rfc5861] |
| **Purge, surrogate key** | Removing cached objects early, by URL, prefix or tag (Fastly's "surrogate keys")[^fastlypurge] |
| **Fingerprinted asset** | A file whose URL contains a content hash, cached for a year and replaced by a new URL[^webdevcache] |
| **ACME, ARI** | Automated certificate issuance,[^rfc8555] and the CA's suggested renewal window[^rfc9773] |
| **HSTS** | A header telling browsers to use only HTTPS for a host for a period[^rfc6797] |
| **CAA** | A DNS record naming the certificate authorities allowed to issue for a domain[^rfc8659] |
| **Certificate Transparency (CT)** | Public, append-only logs of issued certificates that anyone can search[^cthow] |
| **Dangling DNS** | A record pointing at a deleted resource that an attacker may claim[^msdangling] |
| **Core Web Vitals** | LCP (loading), INP (responsiveness), CLS (visual stability); "good" means LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1 at the 75th percentile[^vitals] |
| **p75** | The 75th-percentile value: three in four visits were at least this good[^cwvthresh] |
| **Field and lab data** | Field data (real-user monitoring, RUM) measures real visits; lab (synthetic) tests load pages under fixed conditions[^labfield] |
| **WAF** | Web application firewall: rules that inspect and block requests |

**Mental model: every layer remembers.** Each layer keeps state for a time you chose earlier: a DNS TTL, a browser `max-age`, an
HSTS policy, a CDN `s-maxage`, a certificate's expiry. You can purge your CDN and proxy, not a visitor's browser or a public
resolver. So ask of every change: **what will remember the old state, for how long, and can I recall it?**

::: figure What each layer of the request path remembers, and whether you can recall it
```mermaid
%%{init: {"flowchart": {"wrappingWidth": 140, "nodeSpacing": 20, "rankSpacing": 44}}}%%
flowchart TB
  subgraph Client["In the visitor's browser: you cannot recall"]
    B1["HTTP cache: kept until max-age ends"]
    B2["HSTS policy: kept until max-age ends; preload takes months to undo"]
  end
  subgraph Resolvers["In public and ISP resolvers: you wait"]
    R1["DNS answers: kept until the record TTL ends"]
    R2["DS record at .com: cached up to 24 h, set by the parent zone"]
  end
  subgraph Yours["At your edge and origin: you can act"]
    E1["CDN cache: purge by URL or tag"]
    P1["Proxy cache: clear or bypass"]
    C1["Certificate: replace before expiry"]
    O1["Origin: source of truth"]
  end
  B1 --> R1
  B2 ~~~ R2
  R1 --> E1
  R1 ~~~ P1
  R2 ~~~ C1
  R2 ~~~ O1
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class B1,B2 risk
  class R1,R2 decision
  class E1,P1,C1 core
  class O1 neutral
```
Text equivalent: The visitor's browser keeps a cached copy and an HSTS policy until each `max-age` ends (preload-list removal
takes months); you cannot recall either (red). Resolvers keep DNS answers until the TTL ends and, for .com, a DS record for up to
24 hours set by the parent zone; you can only wait (amber). At your edge and origin you can purge the CDN, clear the proxy cache
and replace a certificate before expiry (green); the origin is the source of truth.
:::

Hence three habits: lower TTLs **before** a change, version asset URLs instead of purging browsers, and ramp long-lived
policies (HSTS, preload) in stages.[^hstspreload][^slack2021]

### How WebOps relates to other Ops

These boundaries are **editorial distinctions** of this Atlas, not industry standards.

| Neighbouring page | Shared scope | Its unique focus | WebOps's unique focus |
|---|---|---|---|
| [DevOps](../devops/) | CI/CD | Delivery practices, DORA metrics | DNS, CDN, TLS, browsers |
| [SRE](../sre/) | SLOs, on-call | Error budgets, reliability engineering | Web SLIs; provider failures |
| [AppOps](../appops/) | Running the application | Resilience inside the code | Everything between the code and the visitor |
| [ReleaseOps](../releaseops/) | Rollout, rollback | Release strategy, flags, canaries | Purges, atomic deploys, content releases |
| [ObservabilityOps](../observabilityops/) | Telemetry | The telemetry platform | Which web signals to read |
| [SEOps](../seops/), [SEO Operations](../seo-operations/) | Core Web Vitals, robots.txt | Their effect on search | Measuring and fixing speed; serving crawlers safely |
| [ContentOps](../contentops/) | Publishing | Editorial lifecycle | Publishing as a production change |
| [MarTechOps](../martechops/) | Page tags | The marketing stack | Tags as speed and security risk |
| [CloudSecOps](../cloudsecops/), [DevSecOps](../devsecops/) | WAF, TLS | Security in depth | WAF basics; certificate and DNS hygiene |

## Principles and operating model

### Charter and principles

There is no official WebOps manifesto; this charter is **this Atlas's proposal** (one-page version in the [templates](#templates)).
**Mission:** every visitor gets a fast, secure, correct page, and every change to the path, from code, content or configuration,
is safe to make and quick to undo.

1. **The visitor's experience is the measure.** Field data at the 75th percentile, per template and device, beats a lab
   score.[^vitals][^labfield]
2. **Every change to the path is a production change**: code deploys, content publishes, cache rules, WAF and bot rules, DNS
   records and certificates.
3. **Know what each layer remembers** (the [mental model](#glossary-and-mental-model) above).
4. **Shared caches never store personalised responses.** Decide public and private per content class.[^rfc9111][^steam2015]
5. **Automate everything with an expiry date**: certificates, domains, DNSSEC signatures.
6. **Watch from outside, and keep the status page and runbooks off the systems they describe.** AWS could not update its status
   dashboard during the 2017 S3 disruption because the dashboard depended on S3;[^awss3] in 2021, losing DNS "broke many of the
   internal tools" Facebook's engineers needed.[^fb2021]
7. **Stage global configuration.** Rule and feature-file changes at the edge reach every location; Cloudflare's 2019 and 2025
   outages both spread that way.[^cf2019][^cf2025]

**Non-goals:** a perfect lab score; owning the CMS's editorial process; building a CDN.

### Building blocks of the request path

| Layer | Job | Typical failure | Main control |
|---|---|---|---|
| Registrar and domain | Holds the name and its delegation | Expired domain; lost registrar account | Auto-renew, account MFA, two named owners |
| Authoritative DNS | Answers for your names | Bad record or delegation; dangling records[^msdangling] | DNS as code, review, low TTL before changes |
| CDN edge | TLS, caching, WAF and bot rules | Bad global rule; provider outage[^cf2019][^fastly2021] | Staged rules, kill switch, fallback plan |
| Reverse proxy | TLS, routing, caching, rate limits | Config error; private data cached | Config test; session bypass |
| Origin: CMS or static build | Produces pages and assets | Slow pages, broken deploy, content error | Atomic deploys, preview, rollback |
| Page in the browser | Renders; runs scripts and tags | Slow LCP, shifts, slow interactions | Budgets, RUM, tag governance |

### Roles, ownership and RACI

A typical site has a **site owner** (often marketing), a **web platform engineer** or small team (the WebOps role), **developers**
(in-house or an agency), **editors** who publish, **security**, and an IT or network team that often holds the registrar and DNS
accounts. In TechCrunch's report, Elissa Fink, Tableau's former CMO, said marketing earned the trust to take over Tableau's site
partly because it "had a reliable WebOps platform, with a reliable high-quality team".[^techcrunch2020] Ownership moved; the
duties remained.

**RACI for recurring duties** (R responsible, A accountable, C consulted, I informed):

| Duty | Site owner | Web platform | Developers | Editors | Security |
|---|---|---|---|---|---|
| Domain renewal, registrar access | A | R | — | — | C |
| DNS record changes | I | R/A | C | — | C |
| Certificate automation and monitoring | I | R/A | — | — | C |
| CDN cache rules and purge design | C | R/A | C | I | — |
| WAF and bot rules | I | R | — | — | A |
| Code deploys | I | C | R/A | I | — |
| Content publishing | A | I | — | R | — |
| Core Web Vitals targets | A | R | R | C | — |
| Third-party tags | A | C | R | — | C |
| Site outage response | I | R/A | R | I | C |

### Lifecycle: Day 0, Day 1, Day 2

| Phase | Activities | Exit criteria |
|---|---|---|
| **Day 0: design** | Hosting model ([decision framework](#decision-framework)); cache policy; certificate and DNS automation; measurement plan | Register drafted; owners named |
| **Day 1: launch** | DNS cut-over with lowered TTLs; monitored certificates; probes and RUM; short HSTS `max-age` | Launch checklist passed; rollback rehearsed |
| **Day 2: operate** | Weekly field data; monthly register review; staged edge changes; quarterly game day | Targets met or plan agreed |
| **Retire** | Delete DNS records before the resources they point to; then CDN configuration and certificates | No dangling records |

### Operating model and governance

- **Change classes.** *Standard* (pre-approved): publish, purge by tag, certificate renewal. *Normal*: code deploy, cache rule,
  redirect, WAF exclusion. *High risk*: DNS delegation or DNSSEC, HSTS preload, WAF to blocking mode, CDN or host switch; these
  need a written plan, a rollback that allows for what layers remember, and a quiet slot.
- **One change log** for code, edge configuration and DNS: "what changed?" has one answer.
- **Escalation:** site on-call → web platform → CDN or host support (contacts in the register).
- **Proportionality:** a brochure site needs a register, automated certificates and one probe; a revenue site with daily
  publishing needs change classes, staged edge rules and field-data SLOs.

## Architecture and workflow

### Reference architecture

::: figure Reference architecture: the data path and the control loops a web team runs
```mermaid
%%{init: {"flowchart": {"wrappingWidth": 150, "nodeSpacing": 28, "rankSpacing": 40}}}%%
flowchart TB
  V["Visitors and bots"] --> EDGE["CDN edge: TLS, WAF, bot rules, cache"]
  V --> DNS["Authoritative DNS"]
  CTL["CI, config as code and ACME client"] --> DNS
  CTL --> EDGE
  DNS ~~~ EDGE
  EDGE --> FW["Origin firewall: CDN addresses only"]
  EDGE -. "logs and RUM beacons" .-> MON["Monitoring: probes, RUM, edge logs"]
  FW --> STATIC[("Static build output")]
  FW --> PROXY["Reverse proxy: cache, rate limits"]
  PROXY --> APP["CMS application"]
  APP --> DB[("Database and media")]
  APP -. "purge by tag" .-> EDGE
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class V,DNS,EDGE,STATIC,PROXY,APP,DB neutral
  class CTL,MON core
  class FW risk
```
Text equivalent: Visitors and bots resolve names through authoritative DNS and reach the CDN edge, which terminates TLS, applies
WAF and bot rules and serves from cache. Misses pass a trust boundary (red), an origin firewall that admits only CDN addresses,
then go to static build output or, for dynamic pages, to a reverse proxy and the CMS application with its database and media.
CI, configuration as code and an ACME client (green) manage DNS and the CDN's rules and certificates. The edge sends logs and RUM
beacons to monitoring (green), which also runs outside probes. The CMS purges changed pages from the edge by tag.
:::

**Trust boundaries.** The edge terminates visitor TLS and opens its own connection to the origin. An origin that accepts traffic
from anywhere lets attackers bypass the WAF and cache, so admit only the provider's published ranges, or mutual TLS from the CDN
where offered ([Nginx example](#reference-implementation)). **Alternatives:** a Jamstack
site, "prebuilt into highly optimized static pages and assets during a build process",[^jamstack] has no runtime origin; a
managed CMS platform hides the proxy and application layers.

### Caching, freshness and purge

Caching brings most of the speed gains and the worst mistakes. Three rules:

1. **Separate freshness by audience.** `max-age` applies to all caches, `s-maxage` overrides it for shared caches,[^rfc9111] and the
   `CDN-Cache-Control` header (RFC 9213) targets CDNs alone.[^rfc9213] HTML can live for minutes at the edge and seconds in browsers,
   because you can purge the edge but not the browser.
2. **Never cache what differs per visitor in a shared cache.** The cache key is "at a minimum, the request method and target
   URI";[^rfc9111] if a response depends on a session cookie the key ignores, the next visitor gets someone else's page, as when
   a Steam configuration "incorrectly cached web traffic for authenticated users".[^steam2015] By default Nginx does not cache a
   response that includes `Set-Cookie`; `proxy_ignore_headers` can turn that off.[^nginxproxy] The attacker's versions of the same
   mistake are web cache poisoning and web cache deception.[^pswcp][^pswcd]
3. **Prefer new URLs and tags to broad purges.** Fingerprinted assets never need purging;[^webdevcache] pages can carry tags so a
   publish purges only what changed.[^fastlypurge][^cloudflarepurge]

::: figure How a CDN serves fresh, stale and purged content
```mermaid
sequenceDiagram
  participant B as Browser
  participant E as CDN edge
  participant O as Origin CMS
  Note over E: s-maxage 300, stale-while-revalidate 60, stale-if-error 86400
  B->>E: GET /news/x
  E-->>B: 200 from cache (HIT)
  Note over E: 300 s later the copy is stale
  B->>E: GET /news/x
  E-->>B: 200 stale copy at once
  E->>O: Revalidate with If-None-Match
  O-->>E: 304 Not Modified
  Note over O: Later the origin fails
  B->>E: GET /news/x
  E->>O: Revalidate
  O-->>E: 503
  E-->>B: 200 stale copy (stale-if-error)
  Note over O: Origin recovers, editor publishes
  O->>E: Purge tag article-x
  B->>E: GET /news/x
  E->>O: GET (MISS)
  O-->>E: 200 new version
  E-->>B: 200 new version
```
Text equivalent: With `s-maxage` 300 s, `stale-while-revalidate` 60 s and `stale-if-error` one day, the edge first serves a cached
copy. Once the copy is stale, the edge returns it at once and revalidates with the origin in the background (304, still current).
When the origin later fails with a 503, the edge serves the stale copy instead of the error. After an editor publishes, the CMS
purges the page's tag; the next request misses, fetches the new version and caches it.
:::

Support for stale controls varies by provider; Nginx honours both in the origin's `Cache-Control` header.[^nginxproxy] Purges
have limits and prices: CloudFront charges per invalidation path beyond 1,000 a month (a wildcard or tag counts as one), and
Cloudflare rate-limits purges by plan (as of October 2026).[^cfinval][^cloudflarepurge]

### End-to-end workflow: code and content to the edge

Code and content both reach production. Pantheon's documented pattern is to "move code up from Dev to Test to Live and content
down from Live to Test to Dev", so code is tested against real content.[^pantheonwf]

::: figure Publishing and deploying, with the exception path
```mermaid
flowchart TB
  T1["Code change: pull request"] --> CI["CI: build, tests, performance budget"]
  CI --> ST["Deploy to Test with a copy of Live content"]
  ST --> G{"Checks pass?"}
  G -- "No" --> FX["Fix and resubmit"]
  FX --> CI
  G -- "Yes" --> LV["Atomic deploy to Live"]
  T2["Content change: editor drafts"] --> PV["Preview"]
  PV --> PB["Publish"]
  LV --> PU["Purge changed pages by tag"]
  PB --> PU
  PU --> W{"Probes and RUM healthy?"}
  W -- "Yes" --> DONE["Record the change"]
  W -- "No" --> RB["Roll back to previous build or revision"]
  RB --> PU
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  class T1,T2,CI,ST,LV,PV,PB,PU,DONE core
  class G,W decision
  class FX,RB risk
```
Text equivalent: A code change passes CI (build, tests, performance budget) and is deployed to Test with a copy of Live content;
failing checks send it back for a fix, passing ones lead to an atomic deploy to Live. A content change is previewed and published.
Both then purge the changed pages by tag. If probes and RUM stay healthy the change is recorded; if not, the team rolls back to the
previous build or content revision and purges again.
:::

**Exception path.** An atomic deploy makes rollback a pointer change; a CMS restores a content revision. Neither recalls what
browsers cached, hence short browser freshness for HTML. Copying Live content down also copies personal data: mask it or restrict
access ([PrivacyOps](../privacyops/)).

## Practices, standards and tools

### Core practices and playbooks

Four playbooks cover the riskiest recurring work; the [RACI](#roles-ownership-and-raci) names owners, the
[register](#reference-implementation) records outputs, and [Lab 2](#hands-on-labs) verifies cache policy in CI.

#### Playbook: certificates under shrinking lifetimes

Under the CA/Browser Forum Baseline Requirements, publicly trusted certificates issued from 15 March 2026 may last at most 200
days, from 15 March 2027 at most 100, and from 15 March 2029 at most 47; domain-validation reuse falls to 10 days in
2029.[^br] Let's Encrypt plans 45-day certificates by February 2028.[^le45]

1. **Automate with ACME** and enable ACME Renewal Information where your client supports it; Let's Encrypt recommends ARI and says
   a hard-coded 60-day renewal interval "will no longer be sufficient".[^le45][^rfc9773]
2. **Renew at about two-thirds of the lifetime** if your client lacks ARI.[^le45]
3. **Monitor from outside** every hostname, including CDN-managed ones; set the alert threshold relative to lifetime
   ([worked calculation](#metrics-and-kpis)). Do not rely on CA e-mails.[^leemail]
4. **Publish CAA records** naming the CAs you use.[^rfc8659]
5. **Scope DNS-01 credentials.** Let's Encrypt issues wildcards only through DNS-01 and warns that full DNS API credentials on a
   web server raise a breach's impact: give only the renewal job a token limited to the challenge records, or delegate
   `_acme-challenge` by CNAME to a separate validation zone.[^lechal]
6. **Expect public logs.** Browsers such as Chrome and Safari help enforce Certificate Transparency, and Let's Encrypt logs every
   certificate it issues; anyone can search the logs.[^cthow][^lect] Names in public certificates are public, so use a private CA or
   a wildcard for internal hosts, and subscribe to a CT monitor to catch issuance you did not request.
7. **Check revocation assumptions.** Let's Encrypt turned its OCSP responders off on 6 August 2025,[^leocsp][^leocspend] so its
   certificates have nothing to staple, although Mozilla's v6.0 intermediate profile still lists OCSP stapling.[^mozguide]
8. **Private CAs are outside these rules**, which cover certificates "intended to be used for authenticating servers accessible
   through the Internet".[^sc081]

#### Playbook: changing DNS safely

1. Lower the record's TTL and wait at least the **old** TTL before changing the value.
2. Change through review; keep the old target serving until the old TTL has passed again.
3. Verify from several public resolvers, as Slack's monitoring did.[^slack2021]
4. Delete records when you delete what they point to; a CNAME to a deprovisioned resource invites subdomain takeover.[^msdangling]
5. Treat DNSSEC as high risk. Slack learned that the .com zone tells resolvers to cache a DS record "for 24h by default (non
   modifiable by zone owners)", and that it had not tested a domain with a wildcard record before slack.com.[^slack2021]

#### Playbook: edge configuration changes

1. Keep WAF, bot, cache and redirect rules as code with review.
2. Test cost, not only matches: Cloudflare's 2019 rule was deployed in "simulate" mode, yet still had to execute, and its regular
   expression exhausted CPU.[^cf2019]
3. Roll out to one hostname or a small share of traffic first where the provider allows, then widen.
4. Validate generated configuration (IP lists, bot feature files) for size and shape before it propagates; after its 2025
   outage, Cloudflare committed to hardening ingestion of its own generated files and "Enabling more global kill switches for
   features".[^cf2025]
5. Start WAF rule sets at low sensitivity: OWASP CRS paranoia level 1 rules "hardly ever trigger a false alarm"; higher levels
   detect more but need rule exclusions for false positives.[^crspl]

#### Playbook: fixing Core Web Vitals

Start from field data: find the template and device that fail at p75, then the part of the metric that is slow. The `web-vitals`
attribution build reports LCP's four subparts and INP's three phases from real visits.[^webvitalslib]

| Metric | Where the time goes | Common causes | First fixes |
|---|---|---|---|
| LCP | Time to first byte, resource load delay, resource load duration, render delay; on a well-optimised page each delay is under 10% of LCP[^optlcp] | Uncached or slow HTML; redirect chains; a hero image found late (CSS background, inserted by script, `loading="lazy"`); render-blocking CSS and scripts; A/B-testing scripts that hide content | Cache HTML at the edge; cut redirects; put the LCP image in the HTML with `fetchpriority="high"` (or preload it) and never lazy-load it; right-size images in modern formats[^optlcp] |
| INP | Input delay, processing duration, presentation delay[^optinp] | Long tasks from script evaluation, first- or third-party; heavy event handlers; very large DOMs | Reduce long script-evaluation tasks during load; split long tasks and yield to the main thread; paint the visual update first and defer other work[^optinp] |
| CLS | Unexpected shifts during load and after interactions | Images, ads, embeds and iframes without reserved space; late-injected content; web fonts swapping[^optcls] | `width` and `height` or `aspect-ratio` on media; reserved slots for ads and embeds; preloaded fonts with `font-display: optional` or matched fallback metrics; pages eligible for the back/forward cache[^optcls] |

**Verify in the field:** your RUM within days, CrUX (a 28-day window) after a month,[^labfield] and a
[CI budget](#reference-implementation) against regressions. Web.dev finds load duration rarely the bottleneck, so check subparts
before buying image tooling.[^optlcp] Govern third-party tags with [MarTechOps](../martechops/).

### Methods, standards and frameworks

**Normative:** HTTP caching (RFC 9111, with RFCs 5861, 8246 and 9213);[^rfc9111] HSTS (RFC 6797); ACME (RFC 8555) and ARI
(RFC 9773); CAA (RFC 8659); the Robots Exclusion Protocol (RFC 9309), whose rules "are not a form of access
authorization";[^rfc9309] and the CA/Browser Forum Baseline Requirements, binding on public CAs and so on your renewals.[^br]
**Recommendations:** Core Web Vitals thresholds;[^vitals] Mozilla's TLS configurations (intermediate: TLS 1.2 and 1.3; modern: 1.3
only);[^mozguide] HSTS preload rules;[^hstspreload] OWASP CRS;[^crspl] Google's SRE guidance on black-box monitoring and
SLOs;[^sremon][^sreslo] the W3C's draft Web Sustainability Guidelines.[^wsg]

### Tooling landscape

Examples are representative, not endorsements (as of October 2026).

| Job | Representative options | Selection criteria | Lock-in to watch |
|---|---|---|---|
| Authoritative DNS | Cloud and CDN DNS services (Slack used Amazon Route 53 and NS1)[^slack2021] | API and IaC; DNSSEC; secondary DNS | Proprietary record types |
| CDN, WAF, bots | Akamai, Amazon CloudFront, Cloudflare, Fastly | Purge by tag; staged config; logs; origin protection | Edge code and rule languages |
| Web server, proxy | Nginx, Apache httpd, HAProxy | Team skill; caching and TLS features | Low |
| Certificates | ACME clients (Certbot, acme.sh, lego) with public CAs; CDN-managed certificates | ARI support; DNS-01 for wildcards; reload hooks | Keys you cannot export |
| Website platforms | Managed CMS platforms (Pantheon and Acquia market theirs as WebOps); static hosts | Workflow; purge integration; export | Platform workflows |
| Measurement | CrUX and PageSpeed Insights; `web-vitals` with your collector; Lighthouse CI; blackbox exporter; commercial RUM | Coverage, privacy, raw-data export | Proprietary scores |

**Know your field data's blind spots.** CrUX includes only opted-in Chrome users on desktop and Android (not Chrome on iOS or
other Chromium browsers), only public, sufficiently popular pages, and attributes single-page-app route changes to the first
page view.[^crux] Your own RUM fills the gaps, but `onCLS()` in the `web-vitals` library works only in Chromium (INP and LCP
also in Firefox and Safari) as of October 2026.[^webvitalslib]

### Automation and AI

Automate what has a clear right answer: certificate renewal, DNS and CDN configuration as code, purge on publish, budgets in CI,
register checks. AI assistants can summarise RUM regressions or draft a WAF exclusion, but a generated rule is still an edge
change to stage: one bad regular expression took down a global network.[^cf2019] AI also changes the traffic: the vendor
Imperva's 2026 Bad Bot Report says bots made up over 53% of web traffic and AI-driven bot attacks surged 12.5 times.[^imperva2026]
Verify claimed crawlers by reverse and forward DNS, not by user agent.[^googlebot] The vendor meaning is moving the same way: in
October 2026 Pantheon launched P1, which VKTR reports as its "agentic content and web operations platform" (vendor launch, not
independent evidence).[^vktr2026]

## WebOps in practice

### Reference implementation

**Assumptions (illustrative):** `www.example.org`, a CMS marketing site behind a CDN, with an Nginx reverse proxy on the origin
and Prometheus for probes.

**1. The site register**, kept in version control and diffed monthly against live DNS and CDN state.

_Example status: YAML syntax-checked; illustrative values._

```yaml
site: www.example.org
owners: {business: head-of-digital, technical: web-platform-team}
domains:
  - name: example.org
    registrar: example-registrar
    auto_renew: true
    account_mfa: true
    locks: [registrar transfer lock, registry lock]
    people_with_access: [alice, bob]
dns:
  provider: example-dns
  managed_as_code: dns/example.org.yaml
  records:
    - {name: www, type: CNAME, target: www.example-cdn.net, owner: web-platform}
    - {name: campaign2025, type: CNAME, target: old-landing.example-host.net,
       owner: marketing, review: "delete if the target is gone"}
certificates:
  - {hosts: [example.org, www.example.org], issuer: public-acme-ca,
     lifetime_days: 90, renewal: acme-ari, challenge: http-01,
     monitored_by: blackbox web_page}
  - {hosts: ["*.example.org"], issuer: public-acme-ca, lifetime_days: 90,
     challenge: dns-01, dns_token: "_acme-challenge only; held by the renewal job"}
cdn: {provider: example-cdn, config_repo: edge/, purge: tags,
      kill_switch: docs/edge-kill.md}
cache_policy: docs/cache-policy.md
monitoring: {probes: 3 locations every 60 s, rum_sample: 0.1, slo: docs/slo.md}
```

**2. Cache policy per content class.**

| Content class | Example | Cache-Control sent by the origin | Purge |
|---|---|---|---|
| Fingerprinted assets | `/assets/app.3f9c2a1b.js` | `public, max-age=31536000, immutable`[^rfc8246] | Never: a new build changes the URL |
| Anonymous HTML | `/news/x` | `public, max-age=0, s-maxage=300, stale-while-revalidate=60, stale-if-error=86400` | Tag on publish |
| Personalised HTML | `/account` | `private, no-store` | Not cached |
| Media uploads | `/uploads/2026/10/a.jpg` | `public, max-age=86400` | URL purge on replacement |
| Errors (404) | Any missing page | `public, max-age=0, s-maxage=60` | Expire |

`max-age=0` makes browsers revalidate HTML while the CDN keeps it five minutes, but browsers that support `stale-while-revalidate`
(Chrome 75+, Firefox 68+) may show a copy up to 60 s old;[^webdevswr] send the stale directives in `CDN-Cache-Control` to confine
them to the CDN.[^rfc9213] Likewise `no-cache` "does not mean 'don't cache'": it requires revalidation.[^mdncc]

**3. Origin proxy.** Nginx caches anonymous HTML briefly, skips sessions, serves stale on errors and rate-limits the login page.
Origin headers take priority over `proxy_cache_valid`.[^nginxproxy] An `add_header` in a `location` cancels inheritance of
server-level `add_header` lines (unless Nginx 1.29.3's `add_header_inherit` is used), so the HSTS header is repeated.[^nginxheaders]
Behind a CDN every connection comes from a CDN address, so the realip module restores the visitor's address from the CDN's
header, trusted only from the provider's ranges: without it the rate limit throttles whole edge locations, and trusting the header
from anyone lets clients spoof addresses. `allow` and `deny` would see the restored address, so the origin check reads
`$realip_remote_addr`.[^nginxrealip] A regular expression containing `}` or `;` must be quoted.[^nginxrewrite]

_Example status: `nginx -t` passed (nginx 1.18.0; paths and port remapped) and behaviour checked on localhost on 2026-10-18: one
visitor's seventh login request got 429 while another passed; a connection from outside the trusted ranges was closed._

```nginx
# /etc/nginx/conf.d/www.example.org.conf (excerpt)
proxy_cache_path /var/cache/nginx/www levels=1:2 keys_zone=www:50m
                 max_size=5g inactive=7d use_temp_path=off;

# Every connection comes from the CDN. Trust its client-address header only
# from the CDN's published ranges (placeholders; keep them current).
set_real_ip_from 192.0.2.0/24;
set_real_ip_from 2001:db8::/32;
real_ip_header    X-Forwarded-For;
real_ip_recursive on;

# The connecting address, before realip: 1 only for the CDN ranges above
geo $realip_remote_addr $from_cdn {
    default        0;
    192.0.2.0/24   1;
    2001:db8::/32  1;
}

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;  # per visitor
limit_req_status 429;

map $http_cookie $skip_cache {
    default                   0;
    "~*wordpress_logged_in_"  1;
    "~*SESS[a-z0-9]+="        1;
}

server {
    listen 443 ssl;
    server_name www.example.org;
    ssl_certificate     /etc/ssl/www.example.org/fullchain.pem;
    ssl_certificate_key /etc/ssl/www.example.org/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;    # protocols of Mozilla "intermediate"
    # HSTS ramp: 300, then 604800, then 2592000 s; wait a full max-age each step
    add_header Strict-Transport-Security "max-age=300; includeSubDomains" always;

    # Origin accepts only the CDN; a firewall rule or CDN-to-origin mTLS is
    # stronger. "allow"/"deny" would see the visitor's address after realip.
    if ($from_cdn = 0) { return 444; }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_cache www;
        proxy_cache_key $scheme$host$request_uri;
        proxy_cache_bypass $skip_cache;
        proxy_no_cache $skip_cache;
        proxy_cache_valid 200 301 5m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;
        add_header X-Cache-Status $upstream_cache_status;
        add_header Strict-Transport-Security "max-age=300; includeSubDomains" always;
    }

    location ~* "\.[0-9a-f]{8,}\.(js|css|woff2)$" {
        root /srv/www/current/public;
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header Strict-Transport-Security "max-age=300; includeSubDomains" always;
    }

    location = /wp-login.php {
        limit_req zone=login burst=5 nodelay;
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
    }
}
```

**Beyond the excerpt.** End the HSTS ramp at a long-term `max-age`: preload requires one year, Mozilla's v6.0 guidelines use
two.[^hstspreload][^mozguide] Redirect HTTP to HTTPS and compress text responses.[^wsg] On Apache httpd: `mod_remoteip` with
`RemoteIPTrustedProxy` restores the visitor's address, which is then what `Require ip` sees — so put the CDN-only origin check in
the firewall or an expression on `CONN_REMOTE_ADDR`, not in `Require ip`;[^apacheremoteip] guard the login path with
`mod_reqtimeout` timeouts and the CRS rules;[^apachereqtimeout][^crspl] `mod_http2` speaks HTTP/2, and the Apache module
documentation lists no HTTP/3 module (as of October 2026; Lab 1 prints the negotiated version).[^apachehttp2] Keep
`Header always set` so errors and redirects carry HSTS,[^apacheheaders] and run `apachectl configtest` before a graceful reload.

**4. Probes and alerts.** The blackbox exporter (v0.29.0, October 2026) probes from outside; `probe_ssl_earliest_cert_expiry`
gives the certificate expiry as a Unix time.[^bbexporter]

_Example status: YAML syntax-checked; not run against Prometheus. The 20-day threshold suits 90-day certificates ([worked
calculation 4](#metrics-and-kpis))._

```yaml
# blackbox.yml
modules:
  web_page:
    prober: http
    timeout: 10s
    http:
      method: GET
      valid_status_codes: [200]
      follow_redirects: false
      fail_if_not_ssl: true
      preferred_ip_protocol: ip4      # the exporter defaults to ip6
      fail_if_body_not_matches_regexp: ["<title>[^<]+</title>"]
      fail_if_header_not_matches:
        - {header: Strict-Transport-Security, regexp: "max-age=[0-9]+"}
```

```yaml
# prometheus.yml (excerpt): run one exporter per probe location
rule_files: [rules/webops.yml]
scrape_configs:
  - job_name: web_probes
    metrics_path: /probe
    params: {module: [web_page]}
    static_configs:
      - targets: ["https://www.example.org/", "https://www.example.org/news/"]
    relabel_configs:
      - {source_labels: [__address__], target_label: __param_target}
      - {source_labels: [__param_target], target_label: instance}
      - {target_label: __address__, replacement: "127.0.0.1:9115"}
```

```yaml
# rules/webops.yml
groups:
  - name: webops
    rules:
      - alert: SiteFailingExternalChecks
        expr: avg_over_time(probe_success{job="web_probes"}[5m]) < 0.8
        for: 5m
        labels: {severity: page}
        annotations:
          summary: "{{ $labels.instance }} fails external probes"
          runbook_url: https://wiki.example.org/runbooks/site-down
      - alert: CertificateRenewalOverdue
        expr: (probe_ssl_earliest_cert_expiry{job="web_probes"} - time()) / 86400 < 20
        for: 1h
        labels: {severity: page}
        annotations:
          summary: "{{ $labels.instance }} certificate expires in under 20 days"
          runbook_url: https://wiki.example.org/runbooks/certificate-renewal
```

**5. Field data.** A 10% sample of page views sends Core Web Vitals, batched and flushed when the page is hidden, as the
`web-vitals` documentation recommends.[^webvitalslib] It sends the template name, not the URL, to keep query strings out.

_Example status: JavaScript syntax-checked with `node --check`; not run in a browser._

```js
// rum.js: Core Web Vitals from a 10% sample of page views (web-vitals 6.x)
import {onCLS, onINP, onLCP} from 'web-vitals';

const SAMPLE_RATE = 0.1;
const queue = new Set();
const template = document.body.dataset.template || 'unknown';

function addToQueue(metric) {
  queue.add({name: metric.name, value: metric.value, rating: metric.rating,
    id: metric.id, navigationType: metric.navigationType, template});
}

function flushQueue() {
  if (queue.size === 0) return;
  navigator.sendBeacon('/rum', JSON.stringify([...queue]));
  queue.clear();
}

if (Math.random() < SAMPLE_RATE) {
  onCLS(addToQueue);
  onINP(addToQueue);
  onLCP(addToQueue);
  addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') flushQueue();
  });
}
```

**6. Budgets in CI.** Lighthouse CI fails the build when lab values regress. INP needs real interactions, so the lab uses Total
Blocking Time (good: under 200 ms on average mobile hardware) as a proxy.[^lhci][^inp][^tbt]

_Example status: JSON syntax-checked; not run in CI._

```json
{
  "ci": {
    "collect": {
      "url": ["http://localhost:8080/", "http://localhost:8080/news/sample/"],
      "numberOfRuns": 3
    },
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", {"maxNumericValue": 2500,
                                               "aggregationMethod": "median"}],
        "cumulative-layout-shift": ["error", {"maxNumericValue": 0.1,
                                              "aggregationMethod": "median"}],
        "total-blocking-time": ["warn", {"maxNumericValue": 200,
                                         "aggregationMethod": "median"}]
      }
    },
    "upload": {"target": "filesystem", "outputDir": "./lhci-reports"}
  }
}
```

### Hands-on labs

#### Lab 1 (beginner): walk the request path

**Objective:** see each layer of [Figure 1](#glossary-and-mental-model) for a real host. **Prerequisites:** a Linux or macOS shell
with `dig`, `curl` and `openssl`; read-only, no cost; use your own site or `www.example.com`.

_Example status: executed on 2026-10-18 (curl 7.68.0, OpenSSL 1.1.1f); the output below is the actual output._

```bash
#!/usr/bin/env bash
# walk-path.sh: look at each layer of the request path for one host.
set -euo pipefail
HOST="${1:-www.example.com}"
APEX="${HOST#www.}"
echo "== DNS"; dig +noall +answer "$HOST" A; dig +noall +answer "$APEX" NS
echo "== CAA (empty means any CA may issue)"; dig +noall +answer "$APEX" CAA
echo "== Timing (three runs)"
FMT='dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s'
FMT+=' ttfb=%{time_starttransfer}s http=%{http_version} status=%{http_code}\n'
for _ in 1 2 3; do curl -sS -o /dev/null -w "$FMT" "https://$HOST/"; done
echo "== Caching and security headers"
HDRS='^(cache-control|age|vary|etag|last-modified|strict-transport-security'
HDRS+='|server|cf-cache-status|x-cache):'
# "|| true": a header list with no match must not stop the script (set -e)
curl -sSI "https://$HOST/" | grep -i -E "$HDRS" || true
echo "== Certificate"
CERT=$(echo | openssl s_client -connect "$HOST:443" -servername "$HOST" 2>/dev/null)
echo "$CERT" | openssl x509 -noout -issuer -enddate
if echo "$CERT" | openssl x509 -noout -checkend $((14 * 86400)) >/dev/null; then
  echo "OK: valid for at least 14 more days"
else
  echo "WARN: expires within 14 days"
fi
```

```text
== DNS
www.example.com.    227   IN  A   104.20.23.154
www.example.com.    227   IN  A   172.66.147.243
example.com.        60780 IN  NS  elliott.ns.cloudflare.com.
example.com.        60780 IN  NS  hera.ns.cloudflare.com.
== CAA (empty means any CA may issue)
== Timing (three runs)
dns=0.001877s connect=0.003082s tls=0.012815s ttfb=0.023129s http=2 status=200
dns=0.000741s connect=0.002175s tls=0.012142s ttfb=0.018288s http=2 status=200
dns=0.000788s connect=0.001931s tls=0.011989s ttfb=0.017335s http=2 status=200
== Caching and security headers
server: cloudflare
last-modified: Tue, 13 Oct 2026 22:05:48 GMT
age: 4940
cf-cache-status: HIT
== Certificate
issuer=C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
notAfter=Dec 25 22:56:35 2026 GMT
OK: valid for at least 14 more days
```

**Read it layer by layer.** The A-record TTL counts down between runs: it is the resolver's memory, which is also why DNS took
about a millisecond. No CAA record means any CA may issue.[^rfc8659] `curl` timings are cumulative from the start.[^curlman]
`age: 4940` with a cache HIT means a copy about 82 minutes old; there is no `Cache-Control` and no HSTS header.
**Validation:** you can name the DNS provider, CDN, certificate issuer and expiry, and whether the page came from cache.
**Troubleshooting:** `dig: command not found` → install DNS utilities; an empty certificate section usually means port 443 is
blocked. **Teardown:** none.

#### Lab 2 (intermediate): an edge check you can run in CI

**Objective:** a scripted check of status, caching headers, HSTS and certificate expiry that fails the build on dangerous
findings. **Prerequisites:** Python 3.8+; read-only requests to your own URLs (an HTML page, a fingerprinted asset, a logged-in
page); for the logged-in page, a test account's session cookie in a CI secret, never a real user's.

_Example status: executed with Python 3.8.10 on 2026-10-18 against the public hosts below (actual output) and against a local
HTTPS test server for `Connection: close`, `max-age` with `Set-Cookie`, heuristic caching and logged-in pages._

```python
"""Check the edge of a website: status, caching headers, HSTS and certificate expiry.

Usage: python3 edgecheck.py [--min-days N] [--cookie-env VAR] URL [URL ...]
Standard library only (Python 3.8+). One GET per URL, no redirects followed.
--cookie-env VAR sends the Cookie header stored in environment variable VAR
(never on the command line, never printed) to check logged-in pages.
Exit code 1 if any check FAILs, 0 otherwise (WARN does not fail).
"""
import argparse
import http.client
import os
import re
import ssl
import sys
import time
from urllib.parse import urlsplit

HASHED = re.compile(r"[.-][0-9a-f]{8,}\.(js|css|woff2?|png|jpe?g|webp|avif|svg)$")
# Status codes a cache may store without explicit freshness (RFC 9110, 15.1)
HEURISTIC = {200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501}


def fetch(url, cookie=None, timeout=10.0):
    parts = urlsplit(url)
    ctx = ssl.create_default_context()
    conn = http.client.HTTPSConnection(parts.hostname, parts.port or 443,
                                       timeout=timeout, context=ctx)
    path = (parts.path or "/") + ("?" + parts.query if parts.query else "")
    req_headers = {"User-Agent": "edgecheck/1.1"}
    if cookie:
        req_headers["Cookie"] = cookie
    started = time.monotonic()
    conn.connect()  # TLS handshake; read the certificate before any request
    cert, tls = conn.sock.getpeercert(), conn.sock.version()
    handshake = time.monotonic() - started
    conn.request("GET", path, headers=req_headers)
    resp = conn.getresponse()  # may close the socket on "Connection: close"
    waited = time.monotonic() - started - handshake
    resp.read(65536)
    conn.close()
    headers = {k.lower(): v for k, v in resp.getheaders()}
    return resp.status, headers, cert, tls, handshake, waited


def directives(value):
    out = {}
    for item in (value or "").split(","):
        name, _, arg = item.strip().partition("=")
        if name:
            out[name.lower()] = arg.strip('"')
    return out


def shared_storable(status, cc, h):
    """May a shared cache store this response? (RFC 9111, section 3)"""
    if "no-store" in cc or "private" in cc:
        return False
    explicit = {"public", "max-age", "s-maxage"} & cc.keys() or "expires" in h
    return bool(explicit) or status in HEURISTIC


def check(url, min_days, cookie):
    findings = []
    status, h, cert, tls, handshake, waited = fetch(url, cookie)
    days = (ssl.cert_time_to_seconds(cert["notAfter"]) - time.time()) / 86400
    cc = directives(h.get("cache-control"))
    storable = shared_storable(status, cc, h)
    path = urlsplit(url).path
    if days < min_days:
        findings.append(("FAIL", f"certificate expires in {days:.1f} days"))
    if "set-cookie" in h and storable:
        findings.append(("FAIL", "Set-Cookie on a response shared caches may store"))
    if cookie and storable:
        findings.append(("FAIL", "logged-in response without private or no-store"))
    if "strict-transport-security" not in h:
        findings.append(("WARN", "no Strict-Transport-Security header"))
    if HASHED.search(path) and int(cc.get("max-age", "0") or 0) < 31536000:
        findings.append(("WARN", "fingerprinted asset without a one-year max-age"))
    if "cache-control" not in h:
        findings.append(("WARN", "no Cache-Control: caches fall back to defaults"))
    print(f"{url}\n  status={status} {tls} handshake={handshake * 1000:.0f} ms"
          f" response={waited * 1000:.0f} ms cert-days-left={days:.1f}")
    for name in ("cache-control", "age", "vary", "cf-cache-status", "x-cache"):
        if name in h:
            print(f"  {name}: {h[name]}")
    for level, text in findings or [("OK", "no findings")]:
        print(f"  {level}: {text}")
    return any(level == "FAIL" for level, _ in findings)


def main(argv):
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--min-days", type=float, default=20.0)
    parser.add_argument("--cookie-env", metavar="VAR")
    parser.add_argument("urls", nargs="+")
    args = parser.parse_args(argv)
    cookie = os.environ.get(args.cookie_env, "") if args.cookie_env else None
    if args.cookie_env and not cookie:
        parser.error(f"environment variable {args.cookie_env} is empty")
    failed = False
    for url in args.urls:
        try:
            failed |= check(url, args.min_days, cookie)
        except (OSError, ssl.SSLError, http.client.HTTPException) as exc:
            print(f"{url}\n  FAIL: {type(exc).__name__}: {exc}")
            failed = True
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
```

```text
$ python3 edgecheck.py https://www.example.com/ https://www.iana.org/
https://www.example.com/
  status=200 TLSv1.3 handshake=12 ms response=6 ms cert-days-left=68.7
  age: 7772
  cf-cache-status: HIT
  WARN: no Strict-Transport-Security header
  WARN: no Cache-Control: caches fall back to defaults
https://www.iana.org/
  status=200 TLSv1.3 handshake=287 ms response=11 ms cert-days-left=32.7
  cache-control: public, max-age=3600
  age: 3295
  vary: Accept-Encoding
  cf-cache-status: HIT
  OK: no findings
$ echo $?
0
$ python3 edgecheck.py --min-days 90 https://www.example.com/ > /dev/null; echo $?
1
$ python3 edgecheck.py https://expired.badssl.com/
https://expired.badssl.com/
  FAIL: SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify
  failed: certificate has expired (_ssl.c:1145)
```

**Steps.** Run it on your URLs, then in CI or on a schedule with `--min-days` matching the [alert rule](#reference-implementation);
check logged-in pages in a second run: `SITE_COOKIE="$TEST_SESSION" python3 edgecheck.py --cookie-env SITE_COOKIE
https://staging.example.org/account`. **Expected output:** exit 0 with `OK` or `WARN` lines for a healthy site; exit 1 for a
certificate inside the threshold, a cookie on a response shared caches may store (no `private` or `no-store`, and explicit
freshness or a heuristically cacheable status such as 200: `Set-Cookie` "does not inhibit caching"[^rfc9111]), a logged-in page
without `private` or `no-store`, or a TLS failure (the last run uses a deliberately expired test host; its error line is wrapped
here). **Validation:** a staging page sending `Set-Cookie` with `Cache-Control: max-age=300` must FAIL.
**Troubleshooting:** `CERTIFICATE_VERIFY_FAILED` on a healthy site often means a missing intermediate certificate.
**Teardown:** none; requests are read-only.

#### Lab 3 (advanced): turn edge logs and RUM into SLIs

**Objective:** compute availability, cache hit ratio, byte offload and Core Web Vitals at p75 from raw data, and see why averages
mislead. **Prerequisites:** Python 3.8+; the two small illustrative files below (export the same columns from your CDN logs and
RUM collector for real data).

_Example status: executed with Python 3.8.10 on 2026-10-18 on the illustrative data; output is actual._

```csv
status,cache,bytes
200,HIT,48000
200,HIT,48000
200,HIT,1200000
200,MISS,48000
200,HIT,250000
200,HIT,48000
200,STALE,48000
200,HIT,90000
200,HIT,48000
200,BYPASS,52000
200,HIT,250000
200,MISS,1200000
200,HIT,48000
200,HIT,90000
503,MISS,1500
200,HIT,48000
200,HIT,48000
200,HIT,250000
200,HIT,48000
200,HIT,90000
```

```csv
metric,template,device,value
LCP,article,mobile,1200
LCP,article,mobile,1350
LCP,article,mobile,1480
LCP,article,mobile,1600
LCP,article,mobile,1720
LCP,article,mobile,1810
LCP,article,mobile,1900
LCP,article,mobile,2010
LCP,article,mobile,2100
LCP,article,mobile,2200
LCP,article,mobile,2290
LCP,article,mobile,2380
LCP,article,mobile,2450
LCP,article,mobile,2520
LCP,article,mobile,2610
LCP,article,mobile,2740
LCP,article,mobile,2980
LCP,article,mobile,3400
LCP,article,mobile,4100
LCP,article,mobile,6200
INP,article,mobile,80
INP,article,mobile,96
INP,article,mobile,120
INP,article,mobile,136
INP,article,mobile,152
INP,article,mobile,184
INP,article,mobile,232
INP,article,mobile,488
CLS,article,mobile,0
CLS,article,mobile,0.01
CLS,article,mobile,0.02
CLS,article,mobile,0.03
CLS,article,mobile,0.05
CLS,article,mobile,0.08
CLS,article,mobile,0.12
CLS,article,mobile,0.31
```

```python
"""Turn edge logs and RUM beacons into WebOps SLIs.

Usage: python3 webslis.py edge.csv rum.csv
edge.csv columns: status,cache,bytes   (cache = HIT, STALE, MISS, BYPASS, ...)
rum.csv columns:  metric,template,device,value   (LCP and INP in ms, CLS unitless)
Standard library only. Percentiles use the nearest-rank method.
"""
import csv
import math
import sys
from collections import defaultdict

# Core Web Vitals thresholds at the 75th percentile: (good if <=, poor if >)
THRESHOLDS = {"LCP": (2500, 4000), "INP": (200, 500), "CLS": (0.1, 0.25)}
SERVED_FROM_CACHE = {"HIT", "STALE"}
CACHE_LOOKUPS = {"HIT", "STALE", "MISS", "EXPIRED"}


def p75(values):
    ordered = sorted(values)
    return ordered[math.ceil(0.75 * len(ordered)) - 1]


def edge_slis(path):
    with open(path, newline="", encoding="utf-8") as fh:
        rows = list(csv.DictReader(fh))
    total = len(rows)
    good = sum(1 for r in rows if int(r["status"]) < 500)
    lookups = [r for r in rows if r["cache"] in CACHE_LOOKUPS]
    hits = sum(1 for r in lookups if r["cache"] in SERVED_FROM_CACHE)
    all_bytes = sum(int(r["bytes"]) for r in rows)
    cached_bytes = sum(int(r["bytes"]) for r in rows if r["cache"] in SERVED_FROM_CACHE)
    print(f"requests={total} availability={good / total:.2%} (non-5xx share)")
    print(f"cache hit ratio={hits / len(lookups):.1%}"
          f" of {len(lookups)} cacheable lookups")
    print(f"byte offload={cached_bytes / all_bytes:.1%} of {all_bytes} bytes")


def rum_slis(path):
    groups = defaultdict(list)
    with open(path, newline="", encoding="utf-8") as fh:
        for r in csv.DictReader(fh):
            groups[(r["template"], r["device"], r["metric"])].append(float(r["value"]))
    verdicts = defaultdict(list)
    for (template, device, metric), values in sorted(groups.items()):
        good, poor = THRESHOLDS[metric]
        value = p75(values)
        rating = "good" if value <= good else "poor" if value > poor else "needs work"
        verdicts[(template, device)].append(rating == "good")
        print(f"{template}/{device} {metric} p75={value:g} n={len(values)} -> {rating}")
    for (template, device), oks in sorted(verdicts.items()):
        result = "PASS" if len(oks) == 3 and all(oks) else "FAIL"
        print(f"{template}/{device} Core Web Vitals assessment: {result}")


if __name__ == "__main__":
    if len(sys.argv) != 3:
        sys.exit("usage: python3 webslis.py edge.csv rum.csv")
    edge_slis(sys.argv[1])
    rum_slis(sys.argv[2])
```

```text
$ python3 webslis.py edge.csv rum.csv
requests=20 availability=95.00% (non-5xx share)
cache hit ratio=84.2% of 19 cacheable lookups
byte offload=67.1% of 3953500 bytes
article/mobile CLS p75=0.08 n=8 -> good
article/mobile INP p75=184 n=8 -> good
article/mobile LCP p75=2610 n=20 -> needs work
article/mobile Core Web Vitals assessment: FAIL
```

**Expected output** as above. **Validation:** recompute one number by hand ([worked calculations](#metrics-and-kpis)), then
change the 6,200 ms LCP value to 2,400 and rerun: p75 drops to 2,520 ms and still fails. **Troubleshooting:** `KeyError: 'status'`
means a header mismatch; CDN logs name the cache field differently. **Teardown:** delete the files. With few samples, p75 swings
widely.

### Use cases and scenarios

These are **illustrative scenarios**; documented incidents are cited throughout the page.

| Scenario | Context | Method | Trade-off | Success measure |
|---|---|---|---|---|
| **1. Campaign launch on a CMS site** | Agency templates; editors publish at 09:00; traffic triples | Code freeze two days before; pre-warm key pages; `s-maxage=300` with `stale-if-error`; purge by tag; probes on campaign URLs | Five minutes of staleness versus origin load | Field LCP p75 ≤ 2.5 s; zero 5xx minutes |
| **2. Static documentation site** | Built from Git; no runtime origin | Fingerprinted assets with `immutable`; atomic deploys; purge only HTML | Slower builds as the site grows | Deploy under 10 minutes; rollback under one |
| **3. Agency running 40 client sites** | DNS in clients' accounts; certificates on many hosts | Register per client; external expiry checks for every hostname; quarterly dangling-record sweep | Registrar access is hard to get | No expiry incident; no orphaned record |

## Measure, secure and pay

### Metrics and KPIs

| KPI | Definition / formula | Data source | Pitfall |
|---|---|---|---|
| Core Web Vitals pass rate | Share of key templates (per device) with LCP, INP and CLS all "good" at p75[^vitals] | RUM; CrUX | Averages; one global number |
| Probe availability | Successful probes ÷ all probes, per location, over 28 days[^sreslo] | Blackbox probes | Probing only the home page |
| Edge availability | Non-5xx responses ÷ all responses | CDN logs | Counting bot traffic |
| Cache hit ratio | (HIT + STALE) ÷ cacheable lookups | CDN logs | Ignores bytes |
| Byte offload | Bytes served from cache ÷ all bytes | CDN logs | Confused with hit ratio |
| TTFB p75 | Field time to first byte; good ≤ 0.8 s[^ttfb] | RUM | Lab TTFB from one location |
| Certificate headroom | Minimum days to expiry across hostnames | Probes | A fixed threshold for all lifetimes |
| Publish-to-edge time | Publish to first edge response with the new version | Synthetic check after purge | Measured from the CMS only |
| Change failure rate | Changes (code, content, edge, DNS) needing rollback ÷ all changes | Change log | Excluding edge and DNS changes |

Review field data weekly, the rest monthly. Vanity traps: a Lighthouse score, "uptime 100%" from one probe, page views.

#### Worked calculations

**1. Why p75, not the mean (Lab 3 data).** The 20 LCP values average 2,452 ms, which looks "good"; the median is 2,245 ms. Nearest-rank
p75 is the 15th value, 2,610 ms: only 65% of these visits (13 of 20) were within 2.5 s, short of the three in four that p75 at
2.5 s requires.[^cwvthresh]

**2. Hit ratio versus offload.** 16 of 19 cacheable lookups were served from cache (84.2%), but one 1.2 MB image miss means only
67.1% of bytes came from cache (2,652,000 of 3,953,500). Origin bandwidth follows bytes.

**3. Probe availability and error budget.** Three locations probing every 60 seconds for 28 days make 3 × 1,440 × 28 = 120,960
probes. A 99.9% SLO allows about 121 failures: some 40 minutes of an outage that all three locations see, or more partial ones.

**4. Certificate alert thresholds (this Atlas's rule of thumb).** If a client renews at two-thirds of lifetime L, about L ÷ 3 days
remain at renewal.[^le45] Page when headroom falls below L ÷ 3 minus m, the days you let automatic retries run: 90-day
certificates with m = 10 give 20 days (the alert rule above); 45-day certificates with m = 5 give 10. A fixed 30-day rule fails
for 45-day certificates ([question 3](#learning-objectives-and-mastery-check)).

### Security, privacy and compliance

| Risk | Signal | Mitigation | Owner |
|---|---|---|---|
| Personalised response cached[^steam2015] | Session cookie with `public` | `private, no-store`; session bypass; [Lab 2](#hands-on-labs) in CI | Web platform |
| Web cache poisoning or deception[^pswcp][^pswcd] | Unkeyed inputs; path tricks | Consistent cache keys and routing; explicit cache rules | Web platform, security |
| Subdomain takeover[^msdangling] | CNAME to deleted resource | Delete records with resources; sweeps | Web platform |
| Domain or DNS account hijack | Unexpected record or registrar changes | MFA, few named admins, change alerts; registrar transfer lock, and registry lock for key domains[^icannepp] | IT, security |
| DNS-01 token leak | Full-zone token on web servers | Token limited to challenge records, held by the renewal job; or a delegated validation zone[^lechal] | Web platform |
| ACME account or key misuse | Unexpected issuance, seen by a CT monitor[^cthow] | CAA; protect ACME keys[^lepersist] | Web platform |
| Downgrade to HTTP | HTTP responses | HSTS ramp; preload last[^hstspreload] | Web platform |
| Third-party script compromise | New script domains | Tag governance; review ([MarTechOps](../martechops/)) | Site owner |

**Privacy.** RUM and logs hold personal data in URLs, IP addresses and identifiers: sample, drop query strings, set retention.
Legal bases and consent belong to [PrivacyOps](../privacyops/), audit evidence to [ComplianceOps](../complianceops/).

### Cost, capacity and sustainability

- **Offload is the main lever:** every hit avoids origin compute and egress; track bytes, not just hits.
- **Capacity is the uncached share:** size the origin for misses during purges, campaigns and bot surges; purges also have
  [prices and limits](#caching-freshness-and-purge).
- **People cost dominates small sites:** a managed platform's fee replaces patching, backups and on-call.
- **Weight is cost and carbon.** The median mobile home page weighed 2,559 KB in July 2025.[^almanacpw] The W3C's draft Web
  Sustainability Guidelines include caching and compression among hosting practices.[^wsg] See [GreenOps](../greenops/).

### Assurance of the operating model

- **Register drift check** monthly: live DNS, CDN rules and certificates against the register; every difference is a finding.
- **Change-log completeness:** for sampled incidents, was the triggering change logged?
- **Risk indicators** (this Atlas's thresholds): certificate headroom under the alert threshold; a template failing Core Web
  Vitals for four weeks; a dangling record; an edge change without a rollback.

## When things go wrong

### Common failures and troubleshooting

| Symptom | Likely cause and diagnosis | Remediation | Prevention |
|---|---|---|---|
| Browser certificate warning | Expired or wrong certificate on one hostname; check with `openssl` ([Lab 1](#hands-on-labs)) | Renew; verify every edge | Automation; outside monitoring |
| Some visitors fail after a DNS change | Resolvers hold the old answer until the TTL ends | Keep the old target serving | Lower TTLs first |
| A user sees another user's page | Shared cache stored a personalised response[^steam2015] | Bypass cache; purge all | Lab 2 in CI; `private` for sessions |
| 5xx from every page at once | Provider incident or a global edge rule[^cf2019][^cf2025] | Revert the rule; kill switch; fallback plan | Staged edge changes |
| CDN error page, origin healthy from an allowed vantage point | CDN cannot connect or complete TLS to the origin: allow-list missing CDN ranges, expired or mismatched origin certificate, timeout (Cloudflare's 521–526)[^cf5xx] | Fix the CDN-to-origin path | Monitor origin certificates; refresh allow-lists from the provider |
| Editors' changes not visible | Purge failed, or browsers hold long-lived HTML | Purge by tag; check `Age` | Short browser freshness for HTML |
| Field LCP worsens after a release | Heavier image, new third-party script, slower TTFB | Roll back, or [fix the slow subpart](#playbook-fixing-core-web-vitals) | Budgets in CI; tag review |
| Legitimate visitors blocked | WAF false positive after raising sensitivity | Rule exclusion; lower paranoia level[^crspl] | Log-only stage first |

::: figure Troubleshooting a site that fails for some visitors, layer by layer
```mermaid
%%{init: {"flowchart": {"wrappingWidth": 130, "nodeSpacing": 18, "rankSpacing": 36}}}%%
flowchart TB
  S["Errors or wrong pages for some visitors"] --> Q1{"Resolves from several resolvers?"}
  Q1 -- "No" --> D["DNS: delegation, DNSSEC, recent record change"]
  Q1 -- "Yes" --> Q2{"TLS handshake valid?"}
  Q2 -- "No" --> C["Certificate: expired, wrong name, chain"]
  Q2 -- "Yes" --> Q3{"Error page from the CDN?"}
  Q3 -- "Yes" --> Q5{"CDN cannot reach origin? (52x, 502, 504)"}
  Q5 -- "Yes" --> CO["CDN to origin: allow-list, origin certificate, timeouts"]
  Q5 -- "No" --> E["Edge: provider status, recent WAF, bot or cache rule"]
  Q3 -- "No" --> Q4{"Origin healthy when probed directly?"}
  Q4 -- "No" --> O["Origin: proxy, CMS, database, capacity"]
  Q4 -- "Yes" --> K["Cache: wrong key, stale or poisoned entry"]
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class Q1,Q2,Q3,Q4,Q5 decision
  class D,C,CO,E,O,K risk
  class S neutral
```
Text equivalent: If the name does not resolve from several resolvers, suspect DNS (delegation, DNSSEC, a recent change). If the
TLS handshake fails, suspect the certificate. If the CDN's error page says it cannot reach the origin (52x, 502, 504), check the
CDN-to-origin path: allow-lists, origin certificate, timeouts; if the CDN itself fails, check provider status and recent WAF, bot
or cache rules. Otherwise probe the origin directly: unhealthy means proxy, CMS, database or capacity; healthy means the cache
holds a wrong, stale or poisoned entry.
:::

#### Worked runbook: errors after an edge rule change

**Illustrative scenario:** at 10:02 a new bot rule goes live; at 10:05 probes fail everywhere and the CDN returns 5xx.

1. **Detect and declare** from the probe alert; name an incident lead ([IncidentOps](../incidentops/)).
2. **Ask what changed**: the change log shows the bot rule at 10:02.
3. **Revert first, diagnose later.** Roll back the rule or use the kill switch; Cloudflare's 2019 recovery came from a global WAF
   termination.[^cf2019] If the provider itself is failing, follow the fallback plan in the register.
4. **Verify from outside** with probes and Lab 2; watch origin load, since a cold cache sends a surge.
5. **Communicate** on a status page hosted away from your own infrastructure ([principle 6](#charter-and-principles)).
6. **Learn:** add a staged rollout and size or shape validation for that rule type.[^cf2025]

### Anti-patterns and myths

| Anti-pattern or myth | Why it fails | Do instead |
|---|---|---|
| "Purge everything" after each publish | Cold caches, origin surges, purge limits | Purge by tag; fingerprint assets |
| "The CDN handles security" | Origin reachable directly; cache rules can leak data | Restrict origin; review cache rules |
| "Our Lighthouse score is 98" | Lab data is one device and network[^labfield] | Field p75 per template |
| Content is not a change | Editors can break layout, links and speed | Preview, revision history, rollback |
| robots.txt keeps bots out | It is advisory[^rfc9309] | Rate limits, bot rules, authentication |

## Deciding and adopting

### When to use and when not to use

**Invest in a named WebOps practice when** the site earns money by the hour, several groups change it, it runs behind a CDN with
custom rules, or you run many domains. **Keep it minimal when** a hosted site builder suffices: own the domain, DNS, content and
exit. **Do not create a separate team when** a platform or SRE team already runs the site among many products; name a web-path
owner there ([PlatformOps](../platformops/), [SRE](../sre/)).

### Decision framework

::: figure Decision tree: choosing a hosting model for a website
```mermaid
flowchart TB
  S["A website to run"] --> Q1{"A hosted site builder meets the needs?"}
  Q1 -- "Yes" --> SB["Use it: own the domain, DNS, content and exit"]
  Q1 -- "No" --> Q2{"Same pages for every visitor, changed through code?"}
  Q2 -- "Yes" --> JS["Static or Jamstack build on a CDN"]
  Q2 -- "No" --> Q3{"Team can run servers: patching, backups, on-call?"}
  Q3 -- "No" --> MP["Managed CMS platform behind a CDN"]
  Q3 -- "Yes" --> Q4{"Needs that platforms cannot meet?"}
  Q4 -- "No" --> MP
  Q4 -- "Yes" --> SM["Self-managed CMS behind a CDN"]
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  class S,SB,JS,MP,SM core
  class Q1,Q2,Q3,Q4 decision
```
Text equivalent: If a hosted site builder meets the needs, use it and keep ownership of the domain, DNS, content and exit path.
Otherwise, a site whose pages are the same for every visitor and change through code fits a static or Jamstack build on a CDN. A
site edited through a CMS goes to a managed CMS platform unless the team can run servers 24/7 and has needs that platforms cannot
meet, in which case it runs a self-managed CMS behind a CDN.
:::

#### Comparing hosting models with weights

When two options are close, score them (illustrative weights; scores 1 poor to 5 good; total = Σ weight × score ÷ 5):

| Criterion (weight) | Static on a CDN | Managed CMS platform | Self-managed CMS |
|---|---|---|---|
| Editors publish without developers (25) | 2 | 5 | 5 |
| Operations effort the team can carry (25) | 5 | 4 | 2 |
| Control over stack and data location (20) | 4 | 3 | 5 |
| Exit cost (15) | 5 | 3 | 4 |
| Attack surface (15) | 5 | 4 | 2 |
| **Score (of 100)** | **81** | **78** | **73** |

Weight the first row at 40 (others 25, 15, 10, 10) and the managed platform wins with 83, against 77 and 73; record weights.

### Maturity model

These levels are **this Atlas's model**; no official one exists.

| Level | Observable behaviour | Exit criteria |
|---|---|---|
| 0. "The agency handles it" | Nobody can list the domains, DNS records or certificate dates | A register for every site |
| 1. Inventoried | Register and owners; manual renewals | Automated certificates; external probes |
| 2. Automated | ACME, DNS and CDN as code; probes; purge on publish | RUM per template; one change log |
| 3. Measured | Field p75 SLOs; cache SLIs; change failure rate for all change types | Staged edge rollouts; budgets in CI |
| 4. Engineered | Staged edge changes with kill switches; tested provider fallback; quarterly game days | Sustain; review after provider changes |

Most marketing sites need Level 2; revenue-critical sites Level 3 or 4.

### Adoption roadmap: 30/60/90 days

| Window | Goals | Activities | Metrics at the end |
|---|---|---|---|
| Days 1–30: see the path | Know what exists | Register for top sites; Lab 1 on each; certificate dates; dangling-record sweep | Register coverage; certificate headroom |
| Days 31–60: automate and watch | No manual expiries; outside view | ACME everywhere; probes in three locations; RUM on key templates; Lab 2 in CI | Probe availability; field p75 baseline |
| Days 61–90: make change safe | Fewer bad changes | Cache policy per content class; purge by tag; staged edge changes; one change log; first game day | Change failure rate; publish-to-edge time |

## Toolkit

### Checklists

#### Launch readiness

- [ ] Register complete; two people can reach the registrar.
- [ ] TTLs lowered at least one old TTL before cut-over; rollback target kept serving.
- [ ] Certificates automated and monitored from outside for every hostname; CAA published.
- [ ] Cache policy per content class verified with Lab 2; session pages `private, no-store`.
- [ ] HSTS at a short `max-age`; ramp scheduled; no preload yet.
- [ ] Probes from three locations; RUM on key templates; status page hosted elsewhere.

#### Before any edge or DNS change

- [ ] Change logged with owner, rollback and what will remember the old state.
- [ ] Staged rollout or log-only mode planned; kill switch known.
- [ ] Probes and dashboards open during and after the change.

### Templates

#### One-page WebOps charter

```text
WEBOPS CHARTER — <site or estate>                 Owner: <name>   Review: quarterly
Mission and principles: as in "Charter and principles"; note local exceptions here
Scope:      <domains and hostnames>; providers: <registrar, DNS, CDN, CA, host>
Out:        editorial process (ContentOps), application internals (AppOps)
Targets:    CWV good at p75 on <templates> · probe availability >= <x>% · zero
            certificate incidents · change failure rate <= <y>%
Decision rights: <who approves DNS, WAF, cache, HSTS and provider changes>
Escalation: site on-call -> web platform -> <CDN and host support contacts>
```

#### Edge or DNS change request

```text
CHANGE — <title>                                   Date/time: <YYYY-MM-DD hh:mm>
Type:        DNS | CDN cache rule | WAF/bot rule | redirect | certificate | HSTS
Owner / reviewer:
What changes (before -> after):
What will remember the old state, for how long (TTL, max-age, caches):
Rollout:     log-only | one hostname | % of traffic | global       Kill switch:
Verification: probes, Lab 2 run, error and block rates, RUM after <n> hours
Rollback:    exact steps; when it stops being possible
```


### FAQ

**Is WebOps just DevOps for websites?** In technique, mostly; the people (editors change production) and the rented path differ.
Pantheon's CEO called it "quite literally DevOps for the web".[^techcrunch2020]

**Can the origin trust `X-Forwarded-For`?** Only from the CDN's published ranges; otherwise any client can claim any address and
dodge rate limits ([Nginx example](#reference-implementation)).[^nginxrealip]

**Do good Core Web Vitals guarantee search rankings?** No. Google says good Core Web Vitals, with other page experience aspects,
"aligns with what our core ranking systems seek to reward";[^searchcwv] search practice belongs to [SEOps](../seops/).

**Do we still need certificate monitoring if renewal is automated?** Yes. Automation fails silently (DNS credentials, rate limits,
a host that never reloads), and lifetimes are shrinking.[^br]

**Should marketing or IT own the website?** Either can; the duties in the [RACI](#roles-ownership-and-raci) must have named owners.

## Going further

### Advanced topics and emerging direction

- **Certificates without renewal friction.** Let's Encrypt is implementing DNS-PERSIST-01, a standing DNS authorisation bound to one
  ACME account, which keeps DNS credentials out of renewal but makes the ACME account key central.[^lepersist] Production,
  targeted for Q2 2026, waited on an IETF draft issue that closed in September; no launch was announced on its blog by
  2026-10-18.[^leforum][^persistissue]
- **Post-quantum TLS.** Mozilla's v6.0 intermediate profile lists the hybrid group X25519MLKEM768 first.[^mozguide]
- **Single-page apps.** The `web-vitals` library can report Core Web Vitals for soft navigations in Chromium 151 and later when you
  opt in with `reportSoftNavs: true`, which also changes how the first page load is reported.[^webvitalslib]

### Learning path

- **Beginner (one day):** read to the [reference architecture](#reference-architecture); run Lab 1 on two sites; read web.dev's
  Web Vitals guide.[^vitals]
- **First week:** run Lab 2 on one site's key URLs; trace its slowest template through the LCP subparts.
- **Practitioner (30 days):** build the register; Lab 2 in CI; RUM on two templates; Lab 3 on real data; continue with
  [ObservabilityOps](../observabilityops/) and [ReleaseOps](../releaseops/).
- **Leader or architect:** the decision framework and judgement call; set targets and decision rights; continue with
  [SRE](../sre/) and [DevOps](../devops/).
- **Portfolio project:** for a site you run, publish its register, a Lab 2 check in CI and a before-and-after field-data note.

## Wrap-up

### Executive recap and cheat sheet

| Topic | What to remember |
|---|---|
| Definition | No standard; this page: operating the DNS-to-browser path of a site changed by code and content |
| Mental model | Every layer remembers; you can purge your edge, not browsers or resolvers |
| Essential practices | Register; cache policy per content class; ACME with outside monitoring; staged edge changes; one change log |
| Metrics | Field CWV at p75 per template; probe and edge availability; byte offload; certificate headroom; change failure rate |
| Watch-outs | Personalised pages in shared caches; global rules pushed at once; fixed 30-day certificate alerts; preload too early |
| Next actions | Run Lab 1 on your top sites; build the register; put Lab 2 in CI |

### A judgement call: a second CDN or a tested fallback?

**Situation (illustrative).** A retailer serves everything through one CDN. After reading about provider outages (Fastly 2021:
95% recovered within 49 minutes; Cloudflare 2025: core traffic largely normal about three hours after onset),[^fastly2021][^cf2025]
leadership asks for "multi-CDN". Assumptions: peak online revenue of 20,000 currency units an hour; one three-hour provider
outage every two years.

**Options.** A: active multi-CDN with DNS steering. B: one CDN plus a tested fallback (DNS switched to a standby path or the
origin) and graceful degradation.

**Arithmetic.** Expected exposure: 1.5 outage hours a year × 20,000 = **30,000 a year** at peak rates. Option A duplicates WAF,
cache and redirect rules, certificates and purges, lowers hit ratios and adds a steering layer; this page assumes a third to half
of an engineer, 40,000 to 60,000 a year at an illustrative loaded cost of 120,000, before the second contract.

**Why the attractive choice can be wrong.** Hand-configured CDNs drift apart, and drift causes incidents; DNS steering inherits
DNS's memory, so failover waits for TTLs. B is real only if rehearsed and its preconditions hold: DNS you can change while the
provider fails (in Cloudflare's 2025 outage most users could not log in to its dashboard,[^cf2025] so host the records you would
flip independently, or keep a tested API path); an origin allow-list you can open quickly; a valid certificate for the public
name; capacity for a cold cache; a WAF in front; a low TTL set in advance.

**Recommendation.** Here, B, drilled twice a year without the provider's console: A costs more than the exposure it removes.
Choose A when outage hours cost more than two CDNs and both configurations come from one source as code. **Uncertainty:** a few
public incidents cannot give a provider's outage rate; revisit after each major one.

### Learning objectives and mastery check

You should now be able to **explain** the label's ambiguity, **map** what each layer remembers, **design** a cache policy and
certificate automation, **measure** field speed, availability and cache efficiency, and **decide** a hosting model and outage
strategy.

1. A team lowers a DNS record's TTL from 86,400 to 60 seconds at the moment it switches the record to a new host. Some visitors
   reach the old host for a day. Why?

::: answer Show answer for question 1
Resolvers that cached the record before the change keep the old answer until the **old** TTL (one day) ends. Lower the TTL first,
wait at least the old TTL, then change the value.
:::

2. A template's LCP values average 2,452 ms and the 75th percentile is 2,610 ms. Does it pass?

::: answer Show answer for question 2
No. Core Web Vitals are assessed at the 75th percentile, and "good" LCP is 2,500 ms or less.[^vitals] The mean hides a slow tail
([worked calculation](#metrics-and-kpis)).
:::

3. Your monitoring pages at 30 days before expiry. You move to 45-day certificates renewed at two-thirds of their lifetime. What
   happens?

::: answer Show answer for question 3
From day 15 until renewal at day 30 each certificate has fewer than 30 days left, so the alert fires half the time and gets
ignored. Set the threshold relative to lifetime, about 10 days here, or alert on failed ARI-guided renewals.[^le45]
:::

4. A plugin update adds `Cache-Control: public` to logged-in account pages behind your CDN. Which controls stop a leak?

::: answer Show answer for question 4
Cache bypass at the CDN and proxy for session cookies; `private, no-store` set late in the response path; Nginx's default refusal
to cache responses with `Set-Cookie` (unless overridden);[^nginxproxy] and a CI check such as Lab 2 that fetches the page with a
test account's cookie and fails without `private` or `no-store`. Steam's 2015 incident shows the cost of missing them.[^steam2015]
:::

5. A colleague wants to add the `preload` directive to HSTS at launch "for security". What do you say?

::: answer Show answer for question 5
Ramp `max-age` in stages first and check every subdomain serves HTTPS; preload requires `includeSubDomains` and a one-year
`max-age`, inclusion reaches stable Chrome over months, and removal "tends to be slow and painful".[^hstspreload]
:::


### Conclusion

"WebOps" has two owners: the web-operations engineers of the 2000s, who passed their craft to DevOps and SRE, and the platform
vendors who gave the word to marketing and content teams. Neither defines a standard.

The work under the word is concrete: a chain of mostly rented systems, each remembering old state for a while, changed every day
by people who are not all engineers. Whatever the team is called, someone must own that chain from DNS to the browser.

## Sources and further reading

Every source below was opened on 2026-10-18, except where a footnote records a later check; dates are the source's own.
Claim-level support is in the Atlas evidence ledger (`research/webops-ledger.md`).

[^oreilly2010]: [Web Operations: Keeping the Data on Time (press release)](https://www.oreilly.com/lpt/pr/2590) — O'Reilly, 2010-06-22. Checked 2026-10-18.
[^velocity2007]: [Velocity Conference: Call For Participation Open](https://app.oreilly.com/pub/pr/1870) — O'Reilly, 2007-11-15. Checked 2026-10-18.
[^dck2009]: [Velocity: Flickr on Devs, Ops and Teamwork](https://www.datacenterknowledge.com/business/velocity-flickr-on-devs-ops-and-teamwork) — Data Center Knowledge, 2009-06-25. Checked 2026-10-18.
[^pantheon2019]: [Pantheon Acquires StagingPilot To Extend WebOps Offering](https://www.globenewswire.com/news-release/2019/05/29/1856412/0/en/Pantheon-Acquires-StagingPilot-To-Extend-WebOps-Offering.html) — Pantheon press release, GlobeNewswire, 2019-05-29. Checked 2026-10-18.
[^techcrunch2020]: [Pantheon bets on WebOps as it charts a course to an IPO](https://techcrunch.com/2020/01/27/pantheon-bets-on-webops-as-it-charts-a-course-to-an-ipo/) — Frederic Lardinois, TechCrunch, 2020-01-27. Checked 2026-10-18.
[^newstack2022]: [WebOps: A DevOps for Websites, but the Tools Let It Down](https://thenewstack.io/webops-a-devops-for-websites-but-the-tools-let-it-down/) — Richard MacManus, The New Stack, 2022-08-17. Checked 2026-10-18.
[^pantheonlc]: [What is WebOps and Why Every Modern Website Needs It](https://pantheon.io/learning-center/webops/what-is-it) — Pantheon, undated. Checked 2026-10-18.
[^pantheonwebops]: [WebOps: maximize value by driving velocity](https://pantheon.io/about/webops) — Pantheon, undated. Checked 2026-10-18.
[^acquia]: [WebOps Explained](https://www.acquia.com/glossary/webops) — Matt Wetmore, Acquia, undated. Checked 2026-10-18.
[^pantheonwf]: [Pantheon WebOps Workflow](https://docs.pantheon.io/pantheon-workflow) — Pantheon documentation. Checked 2026-10-18.
[^vitals]: [Web Vitals](https://web.dev/articles/vitals) — web.dev (Google), updated 2024-10-31. Checked 2026-10-18.
[^cwvthresh]: [Defining the Core Web Vitals metrics thresholds](https://web.dev/articles/defining-core-web-vitals-thresholds) — web.dev, updated 2025-05-07. Checked 2026-10-18.
[^labfield]: [Why lab and field data can be different](https://web.dev/articles/lab-and-field-data-differences) — web.dev, updated 2022-07-18. Checked 2026-10-18.
[^inp]: [Interaction to Next Paint (INP)](https://web.dev/articles/inp) — web.dev, updated 2025-09-02. Checked 2026-10-18.
[^tbt]: [Total Blocking Time (TBT)](https://web.dev/articles/tbt) — web.dev, updated 2024-11-04. Checked 2026-10-18.
[^ttfb]: [Time to First Byte (TTFB)](https://web.dev/articles/ttfb) — web.dev, updated 2025-11-28. Checked 2026-10-18.
[^crux]: [CrUX methodology](https://developer.chrome.com/docs/crux/methodology) — Chrome for Developers, updated 2024-06-20. Checked 2026-10-18.
[^optlcp]: [Optimize Largest Contentful Paint](https://web.dev/articles/optimize-lcp) — web.dev, updated 2025-03-31. Checked 2026-10-18.
[^optinp]: [Optimize Interaction to Next Paint](https://web.dev/articles/optimize-inp) — web.dev, updated 2025-09-02. Checked 2026-10-18.
[^optcls]: [Optimize Cumulative Layout Shift](https://web.dev/articles/optimize-cls) — web.dev, updated 2025-02-07. Checked 2026-10-18.
[^webvitalslib]: [web-vitals library README](https://github.com/GoogleChrome/web-vitals) — Google Chrome team, GitHub (version 6.2.3 on npm). Checked 2026-10-18.
[^searchcwv]: [Understanding Core Web Vitals and Google search results](https://developers.google.com/search/docs/appearance/core-web-vitals) — Google Search Central, updated 2025-12-10. Checked 2026-10-18.
[^lhci]: [Lighthouse CI configuration](https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/configuration.md) — Google Chrome team, GitHub. Checked 2026-10-18.
[^almanacperf]: [Web Almanac 2025: Performance](https://almanac.httparchive.org/en/2025/performance) — HTTP Archive, 2026-01-15 (updated 2026-05-05). Checked 2026-10-18.
[^almanacpw]: [Web Almanac 2025: Page Weight](https://almanac.httparchive.org/en/2025/page-weight) — HTTP Archive. Checked 2026-10-18.
[^rfc9111]: [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111.html) — IETF, 2022-06. Checked 2026-10-18.
[^rfc5861]: [RFC 5861: HTTP Cache-Control Extensions for Stale Content](https://www.rfc-editor.org/rfc/rfc5861.html) — IETF, 2010-05. Checked 2026-10-18.
[^rfc8246]: [RFC 8246: HTTP Immutable Responses](https://www.rfc-editor.org/rfc/rfc8246.html) — IETF, 2017-09. Checked 2026-10-18.
[^rfc9213]: [RFC 9213: Targeted HTTP Cache Control](https://www.rfc-editor.org/rfc/rfc9213.html) — IETF, 2022-06. Checked 2026-10-18.
[^webdevswr]: [Keeping things fresh with stale-while-revalidate](https://web.dev/articles/stale-while-revalidate) — web.dev, updated 2019-07-18. Checked 2026-10-18.
[^mdncc]: [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) — MDN Web Docs. Checked 2026-10-18.
[^webdevcache]: [Prevent unnecessary network requests with the HTTP Cache](https://web.dev/articles/http-cache) — web.dev, updated 2018-11-05. Checked 2026-10-18.
[^cfinval]: [Pay for file invalidation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PayingForInvalidation.html) — Amazon CloudFront Developer Guide. Checked 2026-10-18.
[^cloudflarepurge]: [Purge cache](https://developers.cloudflare.com/cache/how-to/purge-cache/) — Cloudflare docs, updated 2026-09-29. Checked 2026-10-18.
[^fastlypurge]: [Purging with surrogate keys](https://www.fastly.com/documentation/guides/full-site-delivery/purging/purging-with-surrogate-keys/) — Fastly documentation. Checked 2026-10-18.
[^nginxproxy]: [Module ngx_http_proxy_module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) — nginx.org. Checked 2026-10-18.
[^nginxheaders]: [Module ngx_http_headers_module](https://nginx.org/en/docs/http/ngx_http_headers_module.html) — nginx.org. Checked 2026-10-18.
[^nginxrealip]: [Module ngx_http_realip_module](https://nginx.org/en/docs/http/ngx_http_realip_module.html) — nginx.org. Checked 2026-10-18.
[^nginxrewrite]: [Module ngx_http_rewrite_module](https://nginx.org/en/docs/http/ngx_http_rewrite_module.html) — nginx.org (quoting rule under `if`). Checked 2026-10-18.
[^apacheheaders]: [Apache Module mod_headers](https://httpd.apache.org/docs/2.4/mod/mod_headers.html) — Apache HTTP Server 2.4 documentation. Checked 2026-10-18.
[^apacheremoteip]: [Apache Module mod_remoteip](https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html) — Apache HTTP Server 2.4 documentation. Checked 2026-10-19.
[^apachereqtimeout]: [Apache Module mod_reqtimeout](https://httpd.apache.org/docs/2.4/mod/mod_reqtimeout.html) — Apache HTTP Server 2.4 documentation. Checked 2026-10-19.
[^apachehttp2]: [Apache Module mod_http2](https://httpd.apache.org/docs/2.4/mod/mod_http2.html) — Apache HTTP Server 2.4 documentation. Checked 2026-10-19.
[^pswcp]: [Web cache poisoning](https://portswigger.net/web-security/web-cache-poisoning) — PortSwigger Web Security Academy. Checked 2026-10-18.
[^pswcd]: [Web cache deception](https://portswigger.net/web-security/web-cache-deception) — PortSwigger Web Security Academy. Checked 2026-10-18.
[^steam2015]: [Update on Christmas Issues](https://store.steampowered.com/oldnews/19852) — Valve, 2015-12-30. Checked 2026-10-18.
[^br]: [TLS Baseline Requirements v2.3.1, sections 1.2.2 and 6.3.2](https://github.com/cabforum/servercert/blob/main/docs/BR.md) — CA/Browser Forum, 2026-10-04. Checked 2026-10-18.
[^sc081]: [Ballot SC081v3: Introduce Schedule of Reducing Validity and Data Reuse Periods](https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/) — CA/Browser Forum, 2025-04-11. Checked 2026-10-18.
[^le45]: [Decreasing Certificate Lifetimes to 45 Days](https://letsencrypt.org/2025/12/02/from-90-to-45) — Matthew McPherrin, Let's Encrypt, 2025-12-02. Checked 2026-10-18.
[^leemail]: [Ending Support for Expiration Notification Emails](https://letsencrypt.org/2025/01/22/ending-expiration-emails) — Josh Aas, Let's Encrypt, 2025-01-22. Checked 2026-10-18.
[^leocsp]: [Ending OCSP Support in 2025](https://letsencrypt.org/2024/12/05/ending-ocsp) — Josh Aas, Let's Encrypt, 2024-12-05. Checked 2026-10-18.
[^leocspend]: [Ending OCSP Support in 2025 (forum thread, post of 2025-08-06)](https://community.letsencrypt.org/t/ending-ocsp-support-in-2025/229786) — Matthew McPherrin, Let's Encrypt Community. Checked 2026-10-18.
[^lepersist]: [DNS-PERSIST-01: A New Model for DNS-based Challenge Validation](https://letsencrypt.org/2026/02/18/dns-persist-01) — Samantha Frank, Let's Encrypt, 2026-02-18. Checked 2026-10-18.
[^leforum]: [Dns-persist-01 deployment status and timeline](https://community.letsencrypt.org/t/dns-persist-01-deployment-status-and-timeline/246468) — Let's Encrypt Community, staff posts of 2026-05-07 and 2026-06-25. Checked 2026-10-18.
[^persistissue]: [Record should include client-computed information (issue 64)](https://github.com/ietf-wg-acme/draft-ietf-acme-dns-persist/issues/64) — IETF ACME working group draft repository, GitHub, closed 2026-09-23. Checked 2026-10-18.
[^lechal]: [Challenge Types](https://letsencrypt.org/docs/challenge-types/) — Let's Encrypt, updated 2026-02-12. Checked 2026-10-18.
[^cthow]: [How CT works](https://certificate.transparency.dev/howctworks/) — Certificate Transparency project (certificate.transparency.dev). Checked 2026-10-18.
[^lect]: [Certificate Transparency (CT) Logs](https://letsencrypt.org/docs/ct-logs/) — Let's Encrypt, updated 2026-09-04. Checked 2026-10-18.
[^rfc8555]: [RFC 8555: Automatic Certificate Management Environment (ACME)](https://www.rfc-editor.org/rfc/rfc8555.html) — IETF, 2019-03. Checked 2026-10-18.
[^rfc9773]: [RFC 9773: ACME Renewal Information (ARI) Extension](https://www.rfc-editor.org/rfc/rfc9773.html) — IETF, 2025-06. Checked 2026-10-18.
[^rfc6797]: [RFC 6797: HTTP Strict Transport Security](https://www.rfc-editor.org/rfc/rfc6797.html) — IETF, 2012-11. Checked 2026-10-18.
[^hstspreload]: [HSTS Preload List Submission](https://hstspreload.org/) — hstspreload.org (Chromium project). Checked 2026-10-18.
[^rfc8659]: [RFC 8659: DNS Certification Authority Authorization (CAA) Resource Record](https://www.rfc-editor.org/rfc/rfc8659.html) — IETF, 2019-11. Checked 2026-10-18.
[^mozguide]: [Server Side TLS guidelines, version 6.0 (JSON)](https://ssl-config.mozilla.org/guidelines/6.0.json) — Mozilla. Checked 2026-10-18.
[^curlman]: [curl man page](https://curl.se/docs/manpage.html) — curl project. Checked 2026-10-18.
[^fb2021]: [More details about the October 4 outage](https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/) — Santosh Janardhan, Engineering at Meta, 2021-10-05. Checked 2026-10-18.
[^slack2021]: [What Happened During Slack's DNSSEC Rollout](https://slack.engineering/what-happened-during-slacks-dnssec-rollout/) — Rafael Elvira and Laura Nolan, Slack Engineering, 2021-11-29. Checked 2026-10-18.
[^icannepp]: [EPP Status Codes: What Do They Mean, and Why Should I Know?](https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en) — ICANN. Checked 2026-10-18.
[^msdangling]: [Prevent dangling DNS entries and avoid subdomain takeover](https://learn.microsoft.com/en-us/azure/security/fundamentals/subdomain-takeover) — Microsoft Learn, updated 2026-07-24. Checked 2026-10-18.
[^googlebot]: [Verify requests from Google crawlers and fetchers](https://developers.google.com/crawling/docs/crawlers-fetchers/verify-google-requests) — Google, updated 2026-03-20. Checked 2026-10-18.
[^cf2019]: [Details of the Cloudflare outage on July 2, 2019](https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019/) — John Graham-Cumming, Cloudflare, 2019-07-12. Checked 2026-10-18.
[^cf2025]: [Cloudflare outage on November 18, 2025](https://blog.cloudflare.com/18-november-2025-outage/) — Matthew Prince, Cloudflare, 2025-11-18. Checked 2026-10-18.
[^fastly2021]: [Summary of June 8 outage](https://www.fastly.com/blog/summary-of-june-8-outage) — Nick Rockwell, Fastly, 2021-06-08. Checked 2026-10-18.
[^cf5xx]: [Cloudflare 5xx errors](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/) — Cloudflare docs, updated 2026-10-15. Checked 2026-10-18.
[^crspl]: [Paranoia levels](https://coreruleset.org/docs/2-how-crs-works/2-2-paranoia_levels/) — OWASP CRS documentation. Checked 2026-10-18.
[^rfc9309]: [RFC 9309: Robots Exclusion Protocol](https://www.rfc-editor.org/rfc/rfc9309.html) — IETF, 2022-09. Checked 2026-10-18.
[^imperva2026]: [The 2026 Bad Bot Report is here](https://community-cybersec.thalesgroup.com/blogs/tim-chang/2026/05/05/the-2026-bad-bot-report-is-here) — Tim Chang, Imperva (Thales community; the former community.imperva.com address redirects here), 2026-05-05. Checked 2026-10-19.
[^vktr2026]: [Pantheon Launches P1, an AI-Native CMS and Web Operations Platform](https://www.vktr.com/digital-experience/pantheon-launches-p1-an-ainative-cms-and-web-operations-platform/) — Dom Nicastro and Brice Dunwoodie, VKTR, 2026-10-06. Checked 2026-10-18.
[^sremon]: [Monitoring Distributed Systems](https://sre.google/sre-book/monitoring-distributed-systems/) — Rob Ewaschuk, Site Reliability Engineering (Google, O'Reilly, 2016). Checked 2026-10-18.
[^sreslo]: [Implementing SLOs](https://sre.google/workbook/implementing-slos/) — The Site Reliability Workbook (Google, O'Reilly, 2018). Checked 2026-10-18.
[^bbexporter]: [Blackbox exporter (README, CONFIGURATION.md, prober source)](https://github.com/prometheus/blackbox_exporter) — Prometheus project, release v0.29.0 (2026-10-07). Checked 2026-10-18.
[^awss3]: [Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region](https://aws.amazon.com/message/41926/) — Amazon Web Services, 2017. Checked 2026-10-18.
[^jamstack]: [What is Jamstack?](https://jamstack.org/what-is-jamstack/) — jamstack.org. Checked 2026-10-18.
[^wsg]: [Web Sustainability Guidelines (WSG)](https://www.w3.org/TR/web-sustainability-guidelines/) — W3C Sustainable Web Interest Group, Group Note Draft, 2026-10-16 (read through WebFetch; automated download blocked). Checked 2026-10-18.

**Further reading:** John Allspaw and Jesse Robbins (eds.), *Web Operations: Keeping the Data on Time* (O'Reilly, 2010).

**Changelog.** 2026-10-18, r0: first draft by the managing author (research, outline, ledger, labs executed); awaiting independent
review. 2026-10-18, r1: revised after independent review r1 (7 major, 17 minor findings; all addressed, see
`reviews/webops-r1-response.md`): Nginx example fixed and tested (quoted regex, realip, origin restriction), Lab 2 rewritten and
re-run, Core Web Vitals fixing playbook, Certificate Transparency, DNS-01 credentials and registry lock, CDN-to-origin
troubleshooting branch, DNS control-plane preconditions in the judgement call, Figures 2 and 5 redrawn, 16 sources added.
2026-10-19, r2: answered the re-review (`reviews/webops-r1.md`; 0 critical, 0 major, 3 minor): Apache mapping added
(`mod_remoteip`, `CONN_REMOTE_ADDR`, `mod_reqtimeout`, HTTP/2 only), Imperva footnote URL updated to the canonical Thales address,
redundant rows cut; the remaining length overage is a deliberate exception documented in the ledger. 2026-10-19, post-score
polish: every finding in `reviews/webops-score.md` re-verified against its primary source (Apache `mod_remoteip`, `mod_reqtimeout`,
`mod_http2` and the 2.4 module index for the HTTP/3 absence; the Thales URL and its 53% / 12.5× figures; the length exception) —
no defect remained, so no body text changed (`research/webops-ledger.md`, section N).
