npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

prerender-redis-cache-ng

v2.0.0

Published

Prerender plugin for Redis caching - Next Generation with ES6, comprehensive tests, and auto-reconnection

Readme

prerender-redis-cache-ng

Tests codecov npm version Node.js Version

Next Generation Prerender plugin for Redis caching with modern ES6+, comprehensive tests, and production-ready features.

⚠️ Note: This is a modernized fork of prerender-redis-cache with significant improvements and new features.

✨ What's New (NG)

  • 🚀 ES6+ Refactored: Modern JavaScript with arrow functions, const/let, template literals
  • 🗑️ Cache Invalidation: DELETE requests for single URL and pattern-based deletion
  • 🔄 Auto-Reconnection: Backs off to a 5s cap and retries indefinitely — a Redis restart never leaves the process permanently cacheless
  • ⚡ Non-Blocking Invalidation: SCAN instead of KEYS, and UNLINK per batch instead of one bulk DEL, so a large purge never stalls the server
  • 🗜️ Zstd Compression: cache entries are zstd-compressed — measured 8x less Redis memory on a real page, for 0.18ms on the read path
  • 🔑 Protocol-Agnostic Keys: http:// and https:// share one cache entry
  • 🛡️ Enhanced Error Handling: Graceful degradation, validation, defensive programming
  • ✅ Tested to 100%: 141 tests at 100% coverage and a 100% mutation score (Stryker), combining real-Redis integration tests with fast unit tests
  • 🔧 CI/CD Ready: GitHub Actions + Codecov, with lint, coverage and mutation gates enforced on every push
  • 🐛 Bug Fixes: Fixed TTL=0 handling, header validation, JSON parsing errors

Prerender plugin for Redis caching, to be used with the prerender node application from https://github.com/prerender/prerender.

How it works

This plugin stores pages returned through prerender in a redis instance. Currently, it caches the pages for 1 day then expires them. This can be overridden by specifying the env variable "process.env.PAGE_TTL" in seconds. To never expire you should set the PAGE_TTL variable to 0.

📦 Installation

Requires Node.js >= 22.15.0 — that is where node:zlib gained zstd support. On Node 20 or older, use [email protected], which stores entries with gzip instead.

npm install prerender-redis-cache-ng --save

🚀 Quick Start

In your prerender server.js:

const prerender = require('prerender');
const server = prerender();

// Use the Redis cache plugin
server.use(require('prerender-redis-cache-ng'));

server.start();

Configuration

By default it will connect to your Redis instance running on localhost and the default redis port with no authentication, and the default database number (normally 0). You can overwrite this by setting the REDISTOGO_URL, REDISCLOUD_URL, REDISGREEN_URL or REDIS_URL (in the format redis://user:password@host:port/databaseNumber). This currently covers all heroku add-ons for Redis to support quick start.

Automatic Reconnection

The plugin reconnects on its own and never gives up:

  • Backs off by 100ms per attempt, capped at 5 seconds, retrying indefinitely
  • Gracefully bypasses the cache while Redis is unavailable — requests are rendered as normal, never failed
  • Connection events and every failed attempt are logged for monitoring

Retrying forever is deliberate: giving up after a fixed number of attempts left the process permanently cacheless after any Redis restart that outlasted them, while the only cost of retrying is a single socket.

Environment Variables

  • PAGE_TTL: Cache expiration in seconds (default: 86400 = 1 day)
    • Set to 0 for no expiration
    • Invalid values automatically fall back to the default with a warning
  • PAGE_COMPRESS: Compress cache entries with zstd (default: on)
    • Set to 0, false, off or no to store plain JSON instead
    • Safe to change at any time — see below

Compression

Cache entries are compressed with zstd before being written. Rendered HTML compresses hard, and Redis memory is the binding cost of a cache — measured on react.dev/learn (259 KB of HTML) with Redis MEMORY USAGE:

| | Redis memory | vs uncompressed | |---|---|---| | Uncompressed | 320 KB | — | | gzip (1.2.x) | 48 KB | 6.7x | | zstd (2.0.0) | 40 KB | 8.0x |

zstd is smaller and cheaper than gzip on both sides: ~17% less memory, and decompression on a cache hit costs ~0.18ms versus gzip's ~0.33ms.

Reads auto-detect the format by checking magic bytes, so the cache migrates itself across all three eras:

| Written by | Stored as | Still readable | |---|---|---| | 2.0.0+ | zstd | ✅ | | 1.2.x | gzip | ✅ | | < 1.2 | plain JSON | ✅ |

Upgrading needs no migration and no cache flush, and PAGE_COMPRESS can be flipped either way at any time without stranding anything already cached.

Turn it off if something outside this plugin reads the cache directly. Those readers will see compressed bytes rather than JSON. Compression is otherwise transparent — the plugin is the only thing that needs to understand the format.

Cache Invalidation

The plugin supports cache invalidation via DELETE requests. This allows you to manually clear cached pages when content is updated.

Clear a single URL

Send a DELETE request to the prerender service with the URL to clear:

curl -X DELETE http://localhost:3000/render?url=http://example.com/page

Response:

{
  "message": "Cache cleared successfully",
  "url": "http://example.com/page",
  "deleted": 1
}

Clear multiple URLs with a pattern

Use wildcards (*) to clear multiple URLs at once:

curl -X DELETE http://localhost:3000/render?url=http://example.com/*

This will clear all cached URLs matching the pattern.

Response:

{
  "message": "Cache cleared successfully",
  "pattern": "http://example.com/*",
  "deleted": 15
}

Deletion streams: each SCAN batch is UNLINKed as it arrives, so neither the match set nor the delete is ever handled in one blocking chunk — a purge of 100,000 keys will not stall the server.

Because deletion is incremental, a failure part-way through reports how many keys were already removed:

{
  "error": "Failed to delete cache entries",
  "message": "...",
  "deleted": 2000
}

Note: URLs are cached under protocol-agnostic keys, so deleting http://example.com/page also clears the https:// entry.

Acknowledgements

Thanks to the following for making branches with changes which were merged with the 0.2.0 release.

Fantastic Prerender team.

@nelsonkopliku

@eddietio

@irnc

Testing

Two suites run under Jest: real Redis integration tests for end-to-end behavior, and unit tests with a faked client for the connection lifecycle and error paths that a healthy server never reaches.

Both are enforced at 100% coverage and a 100% mutation score (Stryker).

Prerequisites

The integration tests require a running Redis server:

# Using Docker (recommended)
docker run -d -p 6379:6379 valkey/valkey:9-alpine

# Or use your local Redis instance

Run tests

npm test                # both suites
npm run test:coverage   # fails below 100% on any metric
npm run test:mutation   # Stryker; fails below a 100% mutation score

Note: Integration tests use Redis database 15 to avoid conflicts with production data and clean up after each test. The unit tests need no Redis.

Changelog

See CHANGELOG.md for version history and detailed changes.

Recent Updates

v2.0.0 (2026-09-17)

  • Breaking: requires Node >= 22.15.0 (node:zlib zstd support)
  • Changed: cache entries are zstd instead of gzip — 8x less Redis memory and ~2x faster decompression
  • Reads still understand gzip and plain-JSON entries, so no migration is needed

v1.2.0 (2026-09-10)

  • Added: gzip compression of cache entries — 6.7x less Redis memory, auto-detected on read so no migration or cache flush is needed
  • Added: PAGE_COMPRESS to disable it (e.g. if another service reads the cache directly)

v1.1.1 (2026-09-10)

  • Docs: corrected the reconnection section, which still described the 10-attempt / 1-hour limits that v1.1.0 removed

v1.1.0 (2026-09-10)

  • Fixed: reconnection gave up after ~5.5s, leaving the process permanently cacheless after any Redis restart longer than that
  • Fixed: bodyless responses (204/301/410) threw and hung the request
  • Fixed: _closeConnection() hung while the client was reconnecting
  • Performance: wildcard invalidation now UNLINKs each SCAN batch instead of buffering every match into one blocking DEL
  • Added: oxlint, 100% coverage gate, and 100% mutation score gate (Stryker)

v1.0.4 (2025-11-07)

  • 🔧 CI/CD: Fixed Jest hanging issue with --forceExit flag
  • 🔧 CI/CD: Added Redis service to publish workflow
  • 🔧 Testing: Added _closeConnection() method for proper cleanup
  • 📦 Consistency: Unified Redis image to valkey/valkey:9-alpine across all workflows

v1.0.3 (2025-11-07)

  • ⚡ Testing Infrastructure: Migrated from redis-mock to real Redis integration tests
  • 🚀 Performance: 3x faster test execution (~5s vs ~15s)
  • 🐛 Bug Fix: Fixed Redis v5.x SCAN API cursor type (must be string, not number)
  • 🐛 Bug Fix: Fixed async/await handling in DELETE operations
  • 🔧 CI/CD: Added Redis service to GitHub Actions workflow
  • 📦 Dependencies: Removed redis-mock dependency

v1.0.2 (2025-11-07)

  • 🐛 Critical Bug Fix: Fixed Redis 5.x API compatibility (Promise-based API)

v1.0.1 (2025-11-07)

  • 🐛 Critical Bug Fix: Fixed ClientClosedError on startup by moving Redis auth/select to ready event handler

v1.0.0 (2025-01-07)

  • ✅ Cache Invalidation: DELETE requests for single URL and pattern-based deletion
  • ✅ ES6 Refactoring: Modern JavaScript with arrow functions, const/let, template literals
  • ✅ Comprehensive Tests: 33+ tests with 75% coverage
  • ✅ CI/CD: GitHub Actions + Codecov integration
  • ✅ SCAN Command: Non-blocking pattern matching (production-safe)
  • ✅ Enhanced Error Handling: Automatic reconnection, validation, graceful degradation
  • ✅ Better Documentation: Testing guide, development docs, API examples

Known Limitations

  • Cache keys carry no prefix. A pattern deletion of * will clear every key in the target Redis database, not just prerender's. Give the plugin its own database (redis://host:6379/1) if you share the instance.
  • DELETE is silently skipped while Redis is offline. The request falls through and renders the page, so a purge can return 200 without having invalidated anything.
  • Cache writes are awaited. pageLoaded waits for the Redis write before responding, adding that round-trip to every cache miss.