What WebOps is
Link to section “What WebOps is”In 30 seconds
Link to section “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.
For web and CMS engineers, marketing site teams, agencies, and leaders deciding who owns “the website”.
At a glance
Link to section “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).[6] |
| 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, SEO Operations; editorial workflow → ContentOps; application internals → AppOps; release strategy → ReleaseOps; telemetry platform → ObservabilityOps; security depth → DevSecOps, CloudSecOps; compute at remote sites → EdgeOps |
| Prerequisites | HTTP basics; SysOps helps |
| Key outputs | Site register, cache policy, monitored certificates, edge change procedure, SLIs, runbooks |
The problem it solves
Link to section “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.[7]
- One WAF rule with a backtracking regular expression exhausted CPU across Cloudflare’s network for 27 minutes.[8]
- 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.[9]
| 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[10] | 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 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.[11]
What WebOps is not: the naming problem and the boundaries
Link to section “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.[12][5]
- 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”.[6] 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
Link to section “Where it comes from and where it sits”Origins and evolution
Link to section “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”[2] | One web performance and operations community |
| 2009 | Allspaw and Hammond’s Velocity talk “10+ Deploys A Day: Devs & Ops Cooperation at Flickr”[13] | Feeds the DevOps movement |
| 2010 | Web Operations, edited by John Allspaw and Jesse Robbins: “Web Operations is work that matters”[1] | The engineering meaning in print |
| 2019 | Pantheon calls itself “the leading website operations platform (WebOps)”[3] | The vendor-category meaning |
| 2026–2029 | Certificate lifetimes fall in steps from 398 to 47 days (ballot passed 2025)[14] | Certificate work becomes continuous |
Glossary and mental model
Link to section “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[15] |
| Stale controls | stale-while-revalidate (serve stale while refreshing) and stale-if-error (serve stale on errors)[16] |
| Purge, surrogate key | Removing cached objects early, by URL, prefix or tag (Fastly’s “surrogate keys”)[17] |
| Fingerprinted asset | A file whose URL contains a content hash, cached for a year and replaced by a new URL[18] |
| ACME, ARI | Automated certificate issuance,[19] and the CA’s suggested renewal window[20] |
| HSTS | A header telling browsers to use only HTTPS for a host for a period[21] |
| CAA | A DNS record naming the certificate authorities allowed to issue for a domain[22] |
| Certificate Transparency (CT) | Public, append-only logs of issued certificates that anyone can search[23] |
| Dangling DNS | A record pointing at a deleted resource that an attacker may claim[24] |
| 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[25] |
| p75 | The 75th-percentile value: three in four visits were at least this good[26] |
| Field and lab data | Field data (real-user monitoring, RUM) measures real visits; lab (synthetic) tests load pages under fixed conditions[27] |
| 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?
Diagram source (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.[28][9]
How WebOps relates to other Ops
Link to section “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 | CI/CD | Delivery practices, DORA metrics | DNS, CDN, TLS, browsers |
| SRE | SLOs, on-call | Error budgets, reliability engineering | Web SLIs; provider failures |
| AppOps | Running the application | Resilience inside the code | Everything between the code and the visitor |
| ReleaseOps | Rollout, rollback | Release strategy, flags, canaries | Purges, atomic deploys, content releases |
| ObservabilityOps | Telemetry | The telemetry platform | Which web signals to read |
| SEOps, SEO Operations | Core Web Vitals, robots.txt | Their effect on search | Measuring and fixing speed; serving crawlers safely |
| ContentOps | Publishing | Editorial lifecycle | Publishing as a production change |
| MarTechOps | Page tags | The marketing stack | Tags as speed and security risk |
| CloudSecOps, DevSecOps | WAF, TLS | Security in depth | WAF basics; certificate and DNS hygiene |
Principles and operating model
Link to section “Principles and operating model”Charter and principles
Link to section “Charter and principles”There is no official WebOps manifesto; this charter is this Atlas’s proposal (one-page version in the 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.
- The visitor’s experience is the measure. Field data at the 75th percentile, per template and device, beats a lab score.[25][27]
- Every change to the path is a production change: code deploys, content publishes, cache rules, WAF and bot rules, DNS records and certificates.
- Know what each layer remembers (the mental model above).
- Shared caches never store personalised responses. Decide public and private per content class.[15][7]
- Automate everything with an expiry date: certificates, domains, DNSSEC signatures.
- 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;[29] in 2021, losing DNS “broke many of the internal tools” Facebook’s engineers needed.[30]
- Stage global configuration. Rule and feature-file changes at the edge reach every location; Cloudflare’s 2019 and 2025 outages both spread that way.[8][31]
Non-goals: a perfect lab score; owning the CMS’s editorial process; building a CDN.
Building blocks of the request path
Link to section “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[24] | DNS as code, review, low TTL before changes |
| CDN edge | TLS, caching, WAF and bot rules | Bad global rule; provider outage[8][32] | 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
Link to section “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”.[33] 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
Link to section “Lifecycle: Day 0, Day 1, Day 2”| Phase | Activities | Exit criteria |
|---|---|---|
| Day 0: design | Hosting model (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
Link to section “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
Link to section “Architecture and workflow”Reference architecture
Link to section “Reference architecture”Diagram source (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). Alternatives: a Jamstack site, “prebuilt into highly optimized static pages and assets during a build process”,[34] has no runtime origin; a managed CMS platform hides the proxy and application layers.
Caching, freshness and purge
Link to section “Caching, freshness and purge”Caching brings most of the speed gains and the worst mistakes. Three rules:
- Separate freshness by audience.
max-ageapplies to all caches,s-maxageoverrides it for shared caches,[15] and theCDN-Cache-Controlheader (RFC 9213) targets CDNs alone.[35] HTML can live for minutes at the edge and seconds in browsers, because you can purge the edge but not the browser. - Never cache what differs per visitor in a shared cache. The cache key is “at a minimum, the request method and target
URI”;[15] 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”.[7] By default Nginx does not cache a
response that includes
Set-Cookie;proxy_ignore_headerscan turn that off.[36] The attacker’s versions of the same mistake are web cache poisoning and web cache deception.[37][38] - Prefer new URLs and tags to broad purges. Fingerprinted assets never need purging;[18] pages can carry tags so a publish purges only what changed.[17][39]
Diagram source (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.[36] 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).[40][39]
End-to-end workflow: code and content to the edge
Link to section “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.[41]
Diagram source (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).
Practices, standards and tools
Link to section “Practices, standards and tools”Core practices and playbooks
Link to section “Core practices and playbooks”Four playbooks cover the riskiest recurring work; the RACI names owners, the register records outputs, and Lab 2 verifies cache policy in CI.
Playbook: certificates under shrinking lifetimes
Link to section “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.[14] Let’s Encrypt plans 45-day certificates by February 2028.[42]
- 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”.[42][20]
- Renew at about two-thirds of the lifetime if your client lacks ARI.[42]
- Monitor from outside every hostname, including CDN-managed ones; set the alert threshold relative to lifetime (worked calculation). Do not rely on CA e-mails.[10]
- Publish CAA records naming the CAs you use.[22]
- 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-challengeby CNAME to a separate validation zone.[43] - 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.[23][44] 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.
- Check revocation assumptions. Let’s Encrypt turned its OCSP responders off on 6 August 2025,[45][46] so its certificates have nothing to staple, although Mozilla’s v6.0 intermediate profile still lists OCSP stapling.[47]
- Private CAs are outside these rules, which cover certificates “intended to be used for authenticating servers accessible through the Internet”.[48]
Playbook: changing DNS safely
Link to section “Playbook: changing DNS safely”- Lower the record’s TTL and wait at least the old TTL before changing the value.
- Change through review; keep the old target serving until the old TTL has passed again.
- Verify from several public resolvers, as Slack’s monitoring did.[9]
- Delete records when you delete what they point to; a CNAME to a deprovisioned resource invites subdomain takeover.[24]
- 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.[9]
Playbook: edge configuration changes
Link to section “Playbook: edge configuration changes”- Keep WAF, bot, cache and redirect rules as code with review.
- 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.[8]
- Roll out to one hostname or a small share of traffic first where the provider allows, then widen.
- 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”.[31]
- 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.[49]
Playbook: fixing Core Web Vitals
Link to section “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.[50]
| 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[51] | 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[51] |
| INP | Input delay, processing duration, presentation delay[52] | 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[52] |
| CLS | Unexpected shifts during load and after interactions | Images, ads, embeds and iframes without reserved space; late-injected content; web fonts swapping[53] | 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[53] |
Verify in the field: your RUM within days, CrUX (a 28-day window) after a month,[27] and a CI budget against regressions. Web.dev finds load duration rarely the bottleneck, so check subparts before buying image tooling.[51] Govern third-party tags with MarTechOps.
Methods, standards and frameworks
Link to section “Methods, standards and frameworks”Normative: HTTP caching (RFC 9111, with RFCs 5861, 8246 and 9213);[15] 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”;[54] and the CA/Browser Forum Baseline Requirements, binding on public CAs and so on your renewals.[14] Recommendations: Core Web Vitals thresholds;[25] Mozilla’s TLS configurations (intermediate: TLS 1.2 and 1.3; modern: 1.3 only);[47] HSTS preload rules;[28] OWASP CRS;[49] Google’s SRE guidance on black-box monitoring and SLOs;[55][56] the W3C’s draft Web Sustainability Guidelines.[57]
Tooling landscape
Link to section “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)[9] | 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.[58] 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.[50]
Automation and AI
Link to section “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.[8] 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.[59] Verify claimed crawlers by reverse and forward DNS, not by user agent.[60] 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).[61]
WebOps in practice
Link to section “WebOps in practice”Reference implementation
Link to section “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.
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[62] |
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;[63] send the stale directives in CDN-Cache-Control to confine
them to the CDN.[35] Likewise no-cache “does not mean ‘don’t cache’”: it requires revalidation.[64]
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.[36] 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.[65]
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.[66] A regular expression containing } or ; must be quoted.[67]
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.
# /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.[28][47] Redirect HTTP to HTTPS and compress text responses.[57] 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;[68] guard the login path with
mod_reqtimeout timeouts and the CRS rules;[69][49] 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).[70] Keep
Header always set so errors and redirects carry HSTS,[71] 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.[72]
Example status: YAML syntax-checked; not run against Prometheus. The 20-day threshold suits 90-day certificates (worked calculation 4).
# 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]+"}
# 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"}
# 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.[50] 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.
// 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.[73][74][75]
Example status: JSON syntax-checked; not run in CI.
{
"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
Link to section “Hands-on labs”Lab 1 (beginner): walk the request path
Link to section “Lab 1 (beginner): walk the request path”Objective: see each layer of Figure 1 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.
#!/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
== 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.[22] curl timings are cumulative from the start.[76]
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
Link to section “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.
"""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:]))
$ 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;
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”[15]), 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
Link to section “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.
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
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
"""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])
$ 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), 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
Link to section “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
Link to section “Measure, secure and pay”Metrics and KPIs
Link to section “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[25] | RUM; CrUX | Averages; one global number |
| Probe availability | Successful probes ÷ all probes, per location, over 28 days[56] | 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[77] | 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
Link to section “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.[26]
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.[42] 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).
Security, privacy and compliance
Link to section “Security, privacy and compliance”| Risk | Signal | Mitigation | Owner |
|---|---|---|---|
| Personalised response cached[7] | Session cookie with public |
private, no-store; session bypass; Lab 2 in CI |
Web platform |
| Web cache poisoning or deception[37][38] | Unkeyed inputs; path tricks | Consistent cache keys and routing; explicit cache rules | Web platform, security |
| Subdomain takeover[24] | 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[78] | 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[43] | Web platform |
| ACME account or key misuse | Unexpected issuance, seen by a CT monitor[23] | CAA; protect ACME keys[79] | Web platform |
| Downgrade to HTTP | HTTP responses | HSTS ramp; preload last[28] | Web platform |
| Third-party script compromise | New script domains | Tag governance; review (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, audit evidence to ComplianceOps.
Cost, capacity and sustainability
Link to section “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.
- 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.[80] The W3C’s draft Web Sustainability Guidelines include caching and compression among hosting practices.[57] See GreenOps.
Assurance of the operating model
Link to section “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
Link to section “When things go wrong”Common failures and troubleshooting
Link to section “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) |
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[7] | Bypass cache; purge all | Lab 2 in CI; private for sessions |
| 5xx from every page at once | Provider incident or a global edge rule[8][31] | 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)[81] | 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 | Budgets in CI; tag review |
| Legitimate visitors blocked | WAF false positive after raising sensitivity | Rule exclusion; lower paranoia level[49] | Log-only stage first |
Diagram source (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
Link to section “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.
- Detect and declare from the probe alert; name an incident lead (IncidentOps).
- Ask what changed: the change log shows the bot rule at 10:02.
- Revert first, diagnose later. Roll back the rule or use the kill switch; Cloudflare’s 2019 recovery came from a global WAF termination.[8] If the provider itself is failing, follow the fallback plan in the register.
- Verify from outside with probes and Lab 2; watch origin load, since a cold cache sends a surge.
- Communicate on a status page hosted away from your own infrastructure (principle 6).
- Learn: add a staged rollout and size or shape validation for that rule type.[31]
Anti-patterns and myths
Link to section “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[27] | 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[54] | Rate limits, bot rules, authentication |
Deciding and adopting
Link to section “Deciding and adopting”When to use and when not to use
Link to section “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, SRE).
Decision framework
Link to section “Decision framework”Diagram source (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
Link to section “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
Link to section “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
Link to section “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
Link to section “Toolkit”Checklists
Link to section “Checklists”Launch readiness
Link to section “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
Link to section “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
Link to section “Templates”One-page WebOps charter
Link to section “One-page WebOps charter”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
Link to section “Edge or DNS change request”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
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”.[33]
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).[66]
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”;[82] search practice belongs to 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.[14]
Should marketing or IT own the website? Either can; the duties in the RACI must have named owners.
Going further
Link to section “Going further”Advanced topics and emerging direction
Link to section “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.[79] 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.[83][84]
- Post-quantum TLS. Mozilla’s v6.0 intermediate profile lists the hybrid group X25519MLKEM768 first.[47]
- Single-page apps. The
web-vitalslibrary can report Core Web Vitals for soft navigations in Chromium 151 and later when you opt in withreportSoftNavs: true, which also changes how the first page load is reported.[50]
Learning path
Link to section “Learning path”- Beginner (one day): read to the reference architecture; run Lab 1 on two sites; read web.dev’s Web Vitals guide.[25]
- 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 and ReleaseOps.
- Leader or architect: the decision framework and judgement call; set targets and decision rights; continue with SRE and 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
Link to section “Wrap-up”Executive recap and cheat sheet
Link to section “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?
Link to section “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),[32][31] 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,[31] 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
Link to section “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.
- 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?
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.
- A template’s LCP values average 2,452 ms and the 75th percentile is 2,610 ms. Does it pass?
Show answer for question 2
No. Core Web Vitals are assessed at the 75th percentile, and “good” LCP is 2,500 ms or less.[25] The mean hides a slow tail (worked calculation).
- Your monitoring pages at 30 days before expiry. You move to 45-day certificates renewed at two-thirds of their lifetime. What happens?
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.[42]
- A plugin update adds
Cache-Control: publicto logged-in account pages behind your CDN. Which controls stop a leak?
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);[36] 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.[7]
- A colleague wants to add the
preloaddirective to HSTS at launch “for security”. What do you say?
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”.[28]
Conclusion
Link to section “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
Link to section “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).
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).
Web Operations: Keeping the Data on Time (press release) — O’Reilly, 2010-06-22. Checked 2026-10-18. ↩︎ ↩︎
Velocity Conference: Call For Participation Open — O’Reilly, 2007-11-15. Checked 2026-10-18. ↩︎ ↩︎
Pantheon Acquires StagingPilot To Extend WebOps Offering — Pantheon press release, GlobeNewswire, 2019-05-29. Checked 2026-10-18. ↩︎ ↩︎
What is WebOps and Why Every Modern Website Needs It — Pantheon, undated. Checked 2026-10-18. ↩︎
WebOps Explained — Matt Wetmore, Acquia, undated. Checked 2026-10-18. ↩︎ ↩︎
WebOps: A DevOps for Websites, but the Tools Let It Down — Richard MacManus, The New Stack, 2022-08-17. Checked 2026-10-18. ↩︎ ↩︎
Update on Christmas Issues — Valve, 2015-12-30. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Details of the Cloudflare outage on July 2, 2019 — John Graham-Cumming, Cloudflare, 2019-07-12. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
What Happened During Slack’s DNSSEC Rollout — Rafael Elvira and Laura Nolan, Slack Engineering, 2021-11-29. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Ending Support for Expiration Notification Emails — Josh Aas, Let’s Encrypt, 2025-01-22. Checked 2026-10-18. ↩︎ ↩︎
Web Almanac 2025: Performance — HTTP Archive, 2026-01-15 (updated 2026-05-05). Checked 2026-10-18. ↩︎
WebOps: maximize value by driving velocity — Pantheon, undated. Checked 2026-10-18. ↩︎
Velocity: Flickr on Devs, Ops and Teamwork — Data Center Knowledge, 2009-06-25. Checked 2026-10-18. ↩︎
TLS Baseline Requirements v2.3.1, sections 1.2.2 and 6.3.2 — CA/Browser Forum, 2026-10-04. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎
RFC 9111: HTTP Caching — IETF, 2022-06. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
RFC 5861: HTTP Cache-Control Extensions for Stale Content — IETF, 2010-05. Checked 2026-10-18. ↩︎
Purging with surrogate keys — Fastly documentation. Checked 2026-10-18. ↩︎ ↩︎
Prevent unnecessary network requests with the HTTP Cache — web.dev, updated 2018-11-05. Checked 2026-10-18. ↩︎ ↩︎
RFC 8555: Automatic Certificate Management Environment (ACME) — IETF, 2019-03. Checked 2026-10-18. ↩︎
RFC 9773: ACME Renewal Information (ARI) Extension — IETF, 2025-06. Checked 2026-10-18. ↩︎ ↩︎
RFC 6797: HTTP Strict Transport Security — IETF, 2012-11. Checked 2026-10-18. ↩︎
RFC 8659: DNS Certification Authority Authorization (CAA) Resource Record — IETF, 2019-11. Checked 2026-10-18. ↩︎ ↩︎ ↩︎
How CT works — Certificate Transparency project (certificate.transparency.dev). Checked 2026-10-18. ↩︎ ↩︎ ↩︎
Prevent dangling DNS entries and avoid subdomain takeover — Microsoft Learn, updated 2026-07-24. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎
Web Vitals — web.dev (Google), updated 2024-10-31. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Defining the Core Web Vitals metrics thresholds — web.dev, updated 2025-05-07. Checked 2026-10-18. ↩︎ ↩︎
Why lab and field data can be different — web.dev, updated 2022-07-18. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎
HSTS Preload List Submission — hstspreload.org (Chromium project). Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region — Amazon Web Services, 2017. Checked 2026-10-18. ↩︎
More details about the October 4 outage — Santosh Janardhan, Engineering at Meta, 2021-10-05. Checked 2026-10-18. ↩︎
Cloudflare outage on November 18, 2025 — Matthew Prince, Cloudflare, 2025-11-18. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Summary of June 8 outage — Nick Rockwell, Fastly, 2021-06-08. Checked 2026-10-18. ↩︎ ↩︎
Pantheon bets on WebOps as it charts a course to an IPO — Frederic Lardinois, TechCrunch, 2020-01-27. Checked 2026-10-18. ↩︎ ↩︎
What is Jamstack? — jamstack.org. Checked 2026-10-18. ↩︎
RFC 9213: Targeted HTTP Cache Control — IETF, 2022-06. Checked 2026-10-18. ↩︎ ↩︎
Module ngx_http_proxy_module — nginx.org. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎
Web cache poisoning — PortSwigger Web Security Academy. Checked 2026-10-18. ↩︎ ↩︎
Web cache deception — PortSwigger Web Security Academy. Checked 2026-10-18. ↩︎ ↩︎
Purge cache — Cloudflare docs, updated 2026-09-29. Checked 2026-10-18. ↩︎ ↩︎
Pay for file invalidation — Amazon CloudFront Developer Guide. Checked 2026-10-18. ↩︎
Pantheon WebOps Workflow — Pantheon documentation. Checked 2026-10-18. ↩︎
Decreasing Certificate Lifetimes to 45 Days — Matthew McPherrin, Let’s Encrypt, 2025-12-02. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Challenge Types — Let’s Encrypt, updated 2026-02-12. Checked 2026-10-18. ↩︎ ↩︎
Certificate Transparency (CT) Logs — Let’s Encrypt, updated 2026-09-04. Checked 2026-10-18. ↩︎
Ending OCSP Support in 2025 — Josh Aas, Let’s Encrypt, 2024-12-05. Checked 2026-10-18. ↩︎
Ending OCSP Support in 2025 (forum thread, post of 2025-08-06) — Matthew McPherrin, Let’s Encrypt Community. Checked 2026-10-18. ↩︎
Server Side TLS guidelines, version 6.0 (JSON) — Mozilla. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎
Ballot SC081v3: Introduce Schedule of Reducing Validity and Data Reuse Periods — CA/Browser Forum, 2025-04-11. Checked 2026-10-18. ↩︎
Paranoia levels — OWASP CRS documentation. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎
web-vitals library README — Google Chrome team, GitHub (version 6.2.3 on npm). Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎
Optimize Largest Contentful Paint — web.dev, updated 2025-03-31. Checked 2026-10-18. ↩︎ ↩︎ ↩︎
Optimize Interaction to Next Paint — web.dev, updated 2025-09-02. Checked 2026-10-18. ↩︎ ↩︎
Optimize Cumulative Layout Shift — web.dev, updated 2025-02-07. Checked 2026-10-18. ↩︎ ↩︎
RFC 9309: Robots Exclusion Protocol — IETF, 2022-09. Checked 2026-10-18. ↩︎ ↩︎
Monitoring Distributed Systems — Rob Ewaschuk, Site Reliability Engineering (Google, O’Reilly, 2016). Checked 2026-10-18. ↩︎
Implementing SLOs — The Site Reliability Workbook (Google, O’Reilly, 2018). Checked 2026-10-18. ↩︎ ↩︎
Web Sustainability Guidelines (WSG) — W3C Sustainable Web Interest Group, Group Note Draft, 2026-10-16 (read through WebFetch; automated download blocked). Checked 2026-10-18. ↩︎ ↩︎ ↩︎
CrUX methodology — Chrome for Developers, updated 2024-06-20. Checked 2026-10-18. ↩︎
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. ↩︎
Verify requests from Google crawlers and fetchers — Google, updated 2026-03-20. Checked 2026-10-18. ↩︎
Pantheon Launches P1, an AI-Native CMS and Web Operations Platform — Dom Nicastro and Brice Dunwoodie, VKTR, 2026-10-06. Checked 2026-10-18. ↩︎
RFC 8246: HTTP Immutable Responses — IETF, 2017-09. Checked 2026-10-18. ↩︎
Keeping things fresh with stale-while-revalidate — web.dev, updated 2019-07-18. Checked 2026-10-18. ↩︎
Cache-Control — MDN Web Docs. Checked 2026-10-18. ↩︎
Module ngx_http_headers_module — nginx.org. Checked 2026-10-18. ↩︎
Module ngx_http_realip_module — nginx.org. Checked 2026-10-18. ↩︎ ↩︎
Module ngx_http_rewrite_module — nginx.org (quoting rule under
if). Checked 2026-10-18. ↩︎Apache Module mod_remoteip — Apache HTTP Server 2.4 documentation. Checked 2026-10-19. ↩︎
Apache Module mod_reqtimeout — Apache HTTP Server 2.4 documentation. Checked 2026-10-19. ↩︎
Apache Module mod_http2 — Apache HTTP Server 2.4 documentation. Checked 2026-10-19. ↩︎
Apache Module mod_headers — Apache HTTP Server 2.4 documentation. Checked 2026-10-18. ↩︎
Blackbox exporter (README, CONFIGURATION.md, prober source) — Prometheus project, release v0.29.0 (2026-10-07). Checked 2026-10-18. ↩︎
Lighthouse CI configuration — Google Chrome team, GitHub. Checked 2026-10-18. ↩︎
Interaction to Next Paint (INP) — web.dev, updated 2025-09-02. Checked 2026-10-18. ↩︎
Total Blocking Time (TBT) — web.dev, updated 2024-11-04. Checked 2026-10-18. ↩︎
curl man page — curl project. Checked 2026-10-18. ↩︎
Time to First Byte (TTFB) — web.dev, updated 2025-11-28. Checked 2026-10-18. ↩︎
EPP Status Codes: What Do They Mean, and Why Should I Know? — ICANN. Checked 2026-10-18. ↩︎
DNS-PERSIST-01: A New Model for DNS-based Challenge Validation — Samantha Frank, Let’s Encrypt, 2026-02-18. Checked 2026-10-18. ↩︎ ↩︎
Web Almanac 2025: Page Weight — HTTP Archive. Checked 2026-10-18. ↩︎
Cloudflare 5xx errors — Cloudflare docs, updated 2026-10-15. Checked 2026-10-18. ↩︎
Understanding Core Web Vitals and Google search results — Google Search Central, updated 2025-12-10. Checked 2026-10-18. ↩︎
Dns-persist-01 deployment status and timeline — Let’s Encrypt Community, staff posts of 2026-05-07 and 2026-06-25. Checked 2026-10-18. ↩︎
Record should include client-computed information (issue 64) — IETF ACME working group draft repository, GitHub, closed 2026-09-23. Checked 2026-10-18. ↩︎