GitHub Packages · MIT · Node ≥ 18

cache-pulse

Check that your static hosting config caches the right things: fingerprinted assets for a year, HTML revalidated, and nothing stale after a deploy.

Check static hosting caching for performance: long max-age + immutable on fingerprinted assets, revalidation on HTML, no long cache on unhashed files, Vary and Set-Cookie pitfalls, and precompressed .br/.gz files that match. Reads _headers, vercel.json, netlify.toml, firebase.json. SARIF, Action. Zero deps.

  1. 1 Point the @mrlouiswright scope at GitHub Packages in .npmrc

    @mrlouiswright:registry=https://npm.pkg.github.com
  2. 2 Install

    npm i -D @mrlouiswright/cache-pulse
Installing from GitHub Packages needs a token

Published to GitHub Packages, not npmjs.com. GitHub Packages needs a GitHub token with read:packages to install, even for public packages: add //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} to .npmrc or run npm login --registry=https://npm.pkg.github.com.

~/project — zsh
$ npx cache-pulse test/fixtures/badcache-pulse — /workspace/cache-pulse/test/fixtures/bad5 file(s) (2 fingerprinted), 4 header rule(s) HIGH     assets/app.3f2a9c1b.js.gz assets/app.3f2a9c1b.js.gz does not decompress to the current assets/app.3f2a9c1b.js [cache-precompressed-stale]HIGH     vercel.json:3 HTML /index.html is cached for 86400s (public, max-age=86400) [cache-html-long]HIGH     vercel.json:4 Fingerprinted /assets/app.3f2a9c1b.js is cached only 3600s (public, max-age=3600) (+1 more file(s) with the same rule) [cache-hashed-short]HIGH     vercel.json:5 Unfingerprinted /styles.css is cached immutable (public, max-age=31536000, immutable) [cache-unhashed-immutable]MEDIUM   styles.css styles.css (3 KB) has no .br or .gz sibling [cache-precompressed-missing]MEDIUM   vercel.json:4 Vary: * on /assets/app.3f2a9c1b.js (+1 more file(s) with the same rule) [cache-vary-star]
Sample output from the cache-pulse README.

Why it exists

Caching is the cheapest performance win and the easiest to get backwards. Fingerprinted bundles (index-BdT9x2Qa.js) should be cached for a year with immutable; HTML and unfingerprinted files (/styles.css, /logo.png) must revalidate, or visitors keep yesterday's site. cache-pulse reads the header rules in your build output or repo (_headers, vercel.json, netlify.toml, firebase.json, serve.json), resolves the Cache-Control that applies to every file, and reports the mistakes, grouped by the header rule that causes them.

CLI, library and GitHub Action. Node.js 18+, zero runtime dependencies. Works on any static build output (Astro, Next.js “output: 'export'”, Eleventy, Hugo, Jekyll, Docusaurus, VitePress, plain HTML) and runs offline: point it at the folder your build writes. By Louis Wright.

Features

Fingerprinted assets

content-hashed file names and framework output folders (_next/static, _astro, _app/immutable, _nuxt, CRA static/js) need max-age=31536000, immutable; short, missing, no-cache and no-store policies are reported.

HTML

cached without revalidation or immutable is high severity; s-maxage for the CDN with max-age=0 for browsers is accepted.

Unfingerprinted files

immutable or a max-age over a week on /styles.css or /app.js means users do not see deploys; missing policies are noted.

Pitfalls

Vary: *, Set-Cookie on static files (no CDN caching), Content-Encoding on images and fonts.

Compression

when the build ships .br/.gz files, every text asset over 1.4 kB should have one, and each is decompressed and compared with the original to catch stale copies.

Usage

# Example

 npx cache-pulse dist

# Example

 npx cache-pulse . --include '/assets/**'

# Example

 npx cache-pulse public --expect-precompressed --fail-on medium

CI / GitHub Action

permissions:
  contents: read
  security-events: write   # only needed for the SARIF upload
jobs:
  cache-pulse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with: { persist-credentials: false }
      - run: npm ci && npm run build   # produce the static site first
      - uses: MrLouisWright/cache-pulse@v1
        with:
          path: dist
          fail-on: high
          sarif: cache-pulse.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: cache-pulse.sarif }

Options & flags

FlagDescription
Site
--base-url <url>Public URL of the site, e.g. https://example.com (default: inferred from canonical/og:url)
--include <globs>Only check these pages (file or URL path globs, comma-separated)
--exclude <globs>Skip these pages
--clean-urlsPages are served without .html (about.html -> /about)
Caching
--expect-precompressedRequire .br/.gz siblings for text assets even if none exist yet
Common
-c, --config <file>JSON config (default: .cache-pulse.json or "cachePulse" in package.json)
-f, --format <fmt>text (default) | json | sarif | markdown
--json <file>Also write a JSON report
--sarif <file>Also write SARIF 2.1.0
--markdown <file>Also write Markdown (e.g. for a job summary)
--fail-on <sev>critical | high | medium | low | info | none (default: high)
--list-rulesPrint rules and default severities
--no-fixesHide fix suggestions in text output
--no-colorDisable colours

Full reference, config file format and API in the README.

Support this project

Support open-source security tools

I build free, open-source tools that help developers ship safer websites. Every coffee helps keep them maintained, documented and free for everyone.

Buy me a coffee (opens Stripe in a new tab)

Secure checkout by Stripe

Portrait of Louis Wright

About the author

Louis Wright

Cyber security researcher and software engineer based in the UK.

More tools by Louis Wright

58 tools

Sentinels security scanners & CI gates · 20