- Lua 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| nginx | ||
| nitter | ||
| README.md | ||
Nitter Public Instance Customizations
Nginx configuration for caching nitter HTML pages and proxying /pic/ and /video/ media directly from Twitter CDN — used in production on xcancel.com.
Performance benefits
These optimizations were measured in production on xcancel.com:
- Caching HTML pages reduced CPU load by ~60% across all cores. Most popular posts and profiles are served directly from nginx cache without ever hitting nitter. Since a small number of posts account for the majority of traffic, the cache hit rate is very high.
- Proxying
/pic/and/video/in nginx reduced CPU load by ~30% across all cores. Media proxying is pure I/O (fetch from Twitter CDN, forward to client) — nitter doesn't need to be involved. Handling this in nginx with Lua avoids spinning up nitter's runtime for every image and video request.
Combined, these two changes cut nitter's CPU usage by roughly 90%, making it feasible to run a large public instance on modest hardware.
Requirements
- OpenResty (nginx + LuaJIT) — the Lua scripts require OpenResty's
ngxAPI andffimodule. Standard nginx won't work.
What's included
Caching
Nginx proxy cache for nitter HTML pages with cookie-based bypass. Users who change nitter preferences (theme, proxy settings, etc.) bypass the cache; everyone else gets served from cache.
Two cache profiles:
- cache-with-bypass.conf — 30 min cache, respects user preference cookies (for HTML pages)
- cache-without-bypass.conf — 2 hour cache, no bypass (for static assets, media)
A pathcached shared dict optimization tracks which URI paths are already cached, so you can skip expensive access checks (rate limiting, antibot) on subsequent requests for those paths.
Twitter CDN Proxy (/pic/ and /video/)
Instead of letting nitter proxy all media through itself, nginx handles /pic/ and /video/ requests directly by fetching from Twitter CDN (twimg.com). This reduces load on nitter and lets you run the CDN proxy on a separate server if needed.
How it works:
- Nitter generates HMAC-signed media URLs (signature rotates daily)
- Lua scripts in nginx verify the HMAC signature
- For valid requests: nginx fetches directly from Twitter CDN with 1-week browser caching
- For
.m3u8/.vmapvideo manifests: falls back to nitter (these need content rewriting that only nitter can do)
Setup:
- Set
M.HMAC_KEYinnginx/lua/nitter-hmac.luato match thehmacKeyvalue in yournitter.conf - See
nginx/example-nginx.conffor how to wire up the locations and internal proxy
Running multiple nitter processes
Nitter is single-threaded, so running multiple processes behind nginx is the way to scale. The included nitter/docker-compose.yml uses deploy.replicas to spawn 5 replicas by default:
deploy:
replicas: 5
This creates containers named nitter-nitter-1 through nitter-nitter-5. List each replica in the nginx upstream block so nginx load-balances across them:
upstream nitter {
server nitter-nitter-1:8080;
server nitter-nitter-2:8080;
server nitter-nitter-3:8080;
server nitter-nitter-4:8080;
server nitter-nitter-5:8080;
}
All replicas share the same nitter.conf and Redis instance. Adjust the replica count to your CPU/memory budget. See nitter/docker-compose.yml for the full Docker Compose setup.
Nitter patches
Running the CDN proxy on a separate server requires patches to nitter. See: https://github.com/unixfox/periodic-build-with-github-actions/tree/master/nitter/patches
File overview
nitter/
Docker Compose setup for running nitter with Valkey (Redis). Contains a nitter service (scalable with --scale) and a nitter-redis service. The nitter service is configured as read-only with dropped capabilities for security. Set the $IMAGE env var to your nitter Docker image.
nginx/
nitter/
└── docker-compose.yml # Docker Compose for nitter + valkey (use --scale to run multiple)
nginx/
├── example-nginx.conf # Example nginx config showing how to use everything
├── extra-conf/
│ ├── cache-with-bypass.conf # Cache profile: 30 min, cookie bypass
│ ├── cache-without-bypass.conf # Cache profile: 2 hours, no bypass
│ └── proxy-nitter.conf # Proxy pass to nitter upstream
└── lua/
├── nitter-hmac.lua # HMAC-SHA256 verification (shared by pic + video)
├── nitter-pic_proxy.lua # /pic/ → Twitter CDN proxy
└── nitter-video_proxy.lua # /video/ → Twitter CDN proxy (mp4/ts/m4s only)