Troubleshooting Proxy Setup
Common issues and solutions for Rybbit proxy configurations
This guide covers common issues you might encounter when setting up a Rybbit proxy and how to resolve them.
Script Loading Issues
Script Fails to Load (404 Error)
Symptoms:
- Network tab shows 404 for
/analytics/script.js - Console errors about failed script loading
- No tracking data in dashboard
Solutions:
Verify Proxy Configuration
Check your proxy rules are correctly configured:
- Next.js: Ensure rewrites are in
next.config.jsand server is restarted - Nginx: Verify location blocks are active and Nginx is reloaded
- Caddy: Check Caddyfile syntax with
caddy validate - Workers: Verify route is configured in Cloudflare dashboard
Check Path Mapping
Ensure paths are correctly mapped:
Your domain → Rybbit backend
----------------- ---------------
/analytics/script.js → /api/script.jsCommon mistakes:
- Wrong source path (
/trackinginstead of/analytics) - Incorrect destination (missing
/apiprefix) - A renamed script file. Keep the proxied file named
script.js. The script cuts its own URL at/script.jsto find where to send events, so a renamed file (for example/analytics/a.js) sends every request to the wrong place. - Typos in configuration
Test Direct Backend Access
Verify Rybbit backend is accessible:
curl -I https://app.rybbit.io/api/script.jsShould return HTTP/2 200 with content-type: application/javascript.
Script Loads but Returns HTML
Symptoms:
- Script loads with 200 status
- Content-Type is
text/htmlinstead ofapplication/javascript - Console errors about unexpected token
<
Cause: Proxy is serving your application's 404 page instead of proxying.
Solutions:
-
Ensure proxy runs before app routing:
- Next.js: Rewrites run automatically before pages
- Express: Define proxy routes BEFORE catch-all routes
- Laravel: Place proxy routes at the top of
web.php
-
Check route specificity:
// ❌ Wrong order - catch-all comes first
app.get('*', (req, res) => res.sendFile('index.html'));
app.use('/analytics', proxyMiddleware);
// ✅ Correct order - proxy comes first
app.use('/analytics', proxyMiddleware);
app.get('*', (req, res) => res.sendFile('index.html'));Tracking Issues
Script Loads but No Events Tracked
Symptoms:
- Script loads successfully
- No errors in console
- No tracking data appears in Rybbit dashboard
- POST requests to
/analytics/trackfail or return errors
Solutions:
Verify All Endpoints Are Proxied
Ensure every endpoint the script calls is configured:
| Your path | Rybbit endpoint | Without it |
|---|---|---|
/analytics/script.js (GET) | /api/script.js | Nothing loads |
/analytics/track (POST) | /api/track | No events |
/analytics/site/tracking-config/:siteId (GET) | /api/site/tracking-config/:siteId | Every setting on the site's Tracking tab is ignored and the script runs on built-in defaults: web vitals, error tracking, session replay, button clicks, copy and form tracking stay off |
/analytics/identify (POST) | /api/identify | identify() and setTraits() fail, so traits aren't stored and earlier anonymous activity isn't linked to the user |
/analytics/site/:siteId/feature-flags/evaluate (POST) | /api/site/:siteId/feature-flags/evaluate | rybbit.flag() silently returns your fallback |
/analytics/replay.js (GET) | /api/replay.js | Session replay can't start (loaded only when replay is on) |
/analytics/session-replay/record/:siteId (POST) | /api/session-replay/record/:siteId | Replays aren't saved |
Web vitals are bundled into script.js, so there's no separate metrics script to proxy.
Check Network tab: Look for failed requests to any /analytics/* path.
Don't Let a Body Parser Run Before the Proxy
In Express, app.use(express.json()) registered before the proxy reads the request body first, so the proxy forwards an empty body and /track, /identify and session replay POSTs fail. Mount the proxy before any body parser, or use http-proxy-middleware's fixRequestBody:
const { createProxyMiddleware, fixRequestBody } = require('http-proxy-middleware');
// ✅ Proxy first, body parsers after
app.use('/analytics', createProxyMiddleware({
target: 'https://app.rybbit.io/api',
changeOrigin: true,
}));
app.use(express.json());
// Or, if a body parser must run first:
app.use('/analytics', createProxyMiddleware({
target: 'https://app.rybbit.io/api',
changeOrigin: true,
on: { proxyReq: fixRequestBody },
}));Check CORS Headers
CORS only matters when the script is served from a different origin than your pages (for example an analytics. subdomain). Rybbit sets the CORS headers itself, echoing the page's origin back in Access-Control-Allow-Origin, and Nginx passes them through by default. Don't add or override them in your proxy:
// Express
// ❌ Wrong - a cors() middleware in front of the proxy replaces Rybbit's headers
app.use('/analytics', cors({ origin: 'https://yourdomain.com' }));
// ✅ Correct - no cors() on the proxy path; Rybbit's headers pass through
app.use('/analytics', createProxyMiddleware({ target: 'https://app.rybbit.io/api', changeOrigin: true }));Verify Content-Type for POST Requests
Tracking POSTs send JSON data. Ensure Content-Type is preserved:
# Nginx
proxy_set_header Content-Type $content_type;// Express (http-proxy-middleware v3 forwards Content-Type as-is;
// v3 replaced the old onProxyReq option with on.proxyReq)
createProxyMiddleware({
target: 'https://app.rybbit.io/api',
changeOrigin: true,
});Check Request Method Preservation
Ensure POST requests remain POST:
# Nginx - correct
proxy_method $request_method;
# Or let it default (preserves method)
proxy_pass https://app.rybbit.io;Events Tracked but Dashboard Shows Wrong Data
Symptoms:
- Events appear in dashboard
- But geolocation is wrong (all same location)
- Or user agents are incorrect
Cause: Client IP or headers not forwarded properly.
Solutions:
Critical: The X-Forwarded-For header must contain the original client IP for accurate geolocation and user identification, and the First-Party Proxy toggle must be enabled in your site settings. Don't rely on X-Real-IP — on Rybbit Cloud it never reaches the backend (our edge network consumes it), and AWS CloudFront cannot send it at all.
Nginx:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header User-Agent $http_user_agent;Caddy (2.7 or later):
reverse_proxy https://app.rybbit.io {
header_up X-Real-IP {client_ip}
header_up X-Forwarded-For {client_ip}
}If a CDN sits in front of Caddy, add its IP ranges to the trusted_proxies global option, or {client_ip} is the CDN's edge server. See the Caddy guide.
Express (http-proxy-middleware v3):
createProxyMiddleware({
target: 'https://app.rybbit.io/api',
changeOrigin: true,
on: {
proxyReq: (proxyReq, req) => {
// With app.set('trust proxy', ...) configured, req.ip is the visitor IP
proxyReq.setHeader('X-Forwarded-For', req.ip);
},
},
});Cloudflare Workers:
const clientIp = request.headers.get('CF-Connecting-IP') || '0.0.0.0';
proxyHeaders.set('X-Forwarded-For', clientIp);Proxied Requests Are Detected as Bots
Symptoms:
/analytics/trackreturns200 OK- Events do not appear in the normal analytics dashboard (only under bot traffic)
- Tracking works fine when the script talks to Rybbit directly, without the proxy
A 200 from /track doesn't prove an event was stored. Rybbit also answers 200 when the event is diverted as bot traffic, matches a site exclusion, or belongs to a site that's over its monthly limit or has no plan. See Script troubleshooting for the non-proxy causes.
There are two independent causes on the proxy side, and server-side proxies often hit both. Work through them in order.
Cause 1: Your proxy strips the browser's headers
Rybbit's bot detection scores requests on headers that every real browser sends: Accept, Accept-Language, Accept-Encoding (with gzip), and the Sec-Fetch-* headers that all Chromium browsers attach. A proxy that forwards only User-Agent and the visitor IP produces requests indistinguishable from a script — they are accepted with 200 OK but classified as bots.
Fix: forward all incoming request headers, minus Host and hop-by-hop headers (Connection, Content-Length). Hand-picking headers is the most common mistake in PHP/WordPress-style proxies.
// PHP example — forward everything except Host/hop-by-hop
$headers = [];
foreach (getallheaders() as $name => $value) {
if (!in_array(strtolower($name), ['host', 'connection', 'content-length'])) {
$headers[] = "$name: $value";
}
}
$headers[] = 'X-Forwarded-For: ' . $_SERVER['REMOTE_ADDR'];Cause 2: Rybbit sees your proxy's IP instead of the visitor's
First-party proxies typically run on hosting/datacenter infrastructure, so if the visitor IP is not forwarded (or not trusted), traffic carries a datacenter ASN and reinforces bot classification — and geolocation points at your proxy's region.
Fix:
- Set
X-Forwarded-Forto the original visitor IP before forwarding to Rybbit. - Enable the First-Party Proxy toggle in Site Settings → Privacy & Security so Rybbit trusts the forwarded IP unconditionally.
Use X-Forwarded-For, not X-Real-IP. On Rybbit Cloud, X-Real-IP never reaches the backend (our edge network consumes it), and AWS CloudFront cannot send it at all.
Cloudflare Workers:
const clientIp = request.headers.get('CF-Connecting-IP') || '0.0.0.0';
const proxyHeaders = new Headers(request.headers); // preserves all browser headers
proxyHeaders.set('X-Forwarded-For', clientIp);AWS CloudFront:
- Set
X-Forwarded-Forto the viewer IP from a CloudFront Function/Lambda@Edge - Configure the origin request policy to forward all viewer headers
// CloudFront Function (viewer-request)
function handler(event) {
event.request.headers["x-forwarded-for"] = { value: event.viewer.ip };
return event.request;
}Session Replay Issues
Session Replay Not Working
Symptoms:
- Basic tracking works fine
- Session replays don't appear in dashboard
- Console shows errors loading
/analytics/replay.js
Solutions:
- Add replay script endpoint:
// Next.js
{
source: '/analytics/replay.js',
destination: `${process.env.NEXT_PUBLIC_RYBBIT_HOST}/api/replay.js`,
}- Add replay recording endpoint:
{
source: '/analytics/session-replay/record/:siteId',
destination: `${process.env.NEXT_PUBLIC_RYBBIT_HOST}/api/session-replay/record/:siteId`,
}- Increase request size limits:
Session replay can send large payloads (up to 10MB). Ensure your proxy allows this:
# Nginx
location /analytics/session-replay/record/ {
client_max_body_size 10M;
proxy_pass https://app.rybbit.io/api/session-replay/record/;
}// Express: don't parse replay bodies before the proxy at all.
// The proxy streams them through, so no size option is needed.
// See "Don't Let a Body Parser Run Before the Proxy" above.// Next.js: rewrites stream the body through. Only if your app has a
// proxy.ts (middleware) file does Next.js buffer request bodies, capped
// at 10 MB by default. Raise that cap in next.config.js if needed:
module.exports = {
experimental: {
proxyClientMaxBodySize: '10mb',
},
};Configuration and Caching Issues
Stale Configuration (Settings Changes Not Applied)
Symptoms:
- Changed settings in Rybbit dashboard
- But tracking script still uses old settings
- Takes a long time for changes to appear
Cause: Configuration endpoint is cached too aggressively.
Solutions:
- Check cache duration for config endpoint:
# Nginx - cache for 5 minutes max
location /analytics/site/tracking-config/ {
proxy_cache rybbit;
proxy_cache_valid 200 5m; # Not longer!
proxy_pass https://app.rybbit.io/api/site/tracking-config/;
}- Clear cache manually:
# Nginx
sudo nginx -s reload
# Or clear cache directory
sudo rm -rf /var/cache/nginx/rybbit/*// Node.js/Express with node-cache
cache.del(`rybbit_config_${siteId}`);
// Or flush all
cache.flushAll();- Don't cache config endpoint at all (if issues persist):
# Nginx - no caching
location /analytics/site/tracking-config/ {
proxy_cache off;
proxy_pass https://app.rybbit.io/api/site/tracking-config/;
}Script Updates Not Appearing
Symptoms:
- Rybbit released an update
- Your site still loads old script version
- Missing new features or bug fixes
Cause: Script cached too long.
Solutions:
- Recommended cache duration: 1 hour maximum
# Nginx
proxy_cache_valid 200 1h; # Not longer!- Clear script cache:
# Nginx - purge cache
sudo rm -rf /var/cache/nginx/rybbit/
sudo nginx -s reload# Vercel - clear via dashboard
# Deployments → Your deployment → Clear cache// Cloudflare Workers - purge via dashboard
// Caching → Configuration → Purge Everything- Set the browser cache lifetime explicitly (advanced). Rybbit already serves
script.jswithmax-age=3600; set it yourself only if something in your stack changes that header:
// Next.js - rewrites can't set headers; use headers() alongside rewrites()
module.exports = {
async headers() {
return [
{
source: '/analytics/script.js',
headers: [
{ key: 'Cache-Control', value: 'public, max-age=3600, must-revalidate' },
],
},
];
},
};Performance Issues
High Bandwidth Usage
Symptoms:
- Bandwidth costs increasing significantly
- Server struggling with traffic
- Slow response times
Solutions:
Implement Caching
Cache static scripts to reduce backend requests:
# Nginx with caching
proxy_cache_path /var/cache/nginx/rybbit levels=1:2 keys_zone=rybbit:10m;
location = /analytics/script.js {
proxy_cache rybbit;
proxy_cache_valid 200 1h;
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
proxy_pass https://app.rybbit.io/api/script.js;
}Use CDN
Put a CDN in front of your proxy:
- Cloudflare (recommended)
- AWS CloudFront
- Fastly
Monitor and Limit
Add monitoring and rate limiting:
// Express with rate limiting
const rateLimit = require('express-rate-limit');
app.use('/analytics/track', rateLimit({
windowMs: 60 * 1000,
max: 100, // 100 requests per minute per IP
}));Slow Response Times
Symptoms:
- Tracking requests take multiple seconds
- Script loading is slow
- Poor user experience
Solutions:
- Enable compression:
# Nginx
gzip on;
gzip_types application/javascript;- Use connection keepalive:
# Nginx
proxy_http_version 1.1;
proxy_set_header Connection "";
upstream rybbit {
server app.rybbit.io:443;
keepalive 32;
}- Check backend connectivity:
# Test latency to Rybbit
curl -w "@curl-format.txt" -o /dev/null -s https://app.rybbit.io/api/script.js
# Create curl-format.txt:
# time_namelookup: %{time_namelookup}s
# time_connect: %{time_connect}s
# time_starttransfer: %{time_starttransfer}s
# time_total: %{time_total}sIf backend is slow, consider:
- Self-hosting Rybbit closer to your users
- Using a CDN proxy
- Caching more aggressively
Platform-Specific Issues
Next.js: Rewrites Not Working
Problem: Added rewrites to next.config.js but they don't work.
Solutions:
- Restart dev server:
# Stop server (Ctrl+C)
npm run dev- Check async rewrites function:
// ❌ Wrong - not async
rewrites() {
return [...];
}
// ✅ Correct - async
async rewrites() {
return [...];
}- Verify environment variable is loaded:
async rewrites() {
console.log('RYBBIT_HOST:', process.env.NEXT_PUBLIC_RYBBIT_HOST);
// Should log the URL
return [...];
}Nginx: 502 Bad Gateway
Problem: Nginx returns 502 error for analytics endpoints.
Solutions:
- Enable SNI for SSL:
proxy_ssl_server_name on;- Add a
resolverifproxy_passuses a variable (for exampleproxy_pass https://app.rybbit.io/api/site/tracking-config/$1;in a regex location). Nginx resolves variable upstreams at request time and returns 502 without one:
resolver 1.1.1.1 8.8.8.8 valid=300s;- Check DNS resolution:
nslookup app.rybbit.io- Check logs:
sudo tail -f /var/log/nginx/error.log- Test backend directly:
curl -v https://app.rybbit.io/api/script.jsVercel: 404 After Deployment
Problem: Works locally but 404 in production.
Solutions:
- Ensure
vercel.jsonis committed:
git add vercel.json
git commit -m "Add Rybbit proxy config"
git push-
Check build logs in Vercel dashboard for errors
-
Verify environment variables are set in Vercel project settings
Debugging Checklist
When troubleshooting, work through this checklist:
- Script loads successfully (200 status, correct Content-Type)
- Proxied file is still named
script.js - Tracking endpoint is proxied (
/analytics/track) - Config, identify and feature flag endpoints are proxied (
/analytics/site/tracking-config/:siteId,/analytics/identify,/analytics/site/:siteId/feature-flags/evaluate) - No body parser consumes POST bodies before the proxy
- POST requests preserve method and body
- Client IP is forwarded in
X-Forwarded-For(not justX-Real-IP— that header doesn't survive Rybbit Cloud's edge) - First-Party Proxy toggle is enabled in Site Settings → Privacy & Security
- Full browser header set is forwarded (
Accept,Accept-Language,Accept-Encoding,Sec-Fetch-*) — not justUser-Agent - User-Agent header is preserved
- CORS headers are preserved (check response headers)
- Request size limits allow tracking data (1MB minimum)
- Session replay endpoints are proxied (if using replay)
- Cache duration is appropriate (1 hour for scripts, 5 min for config)
- No CSRF protection blocking POST requests
- Environment variables are set correctly
- Configuration changes have been deployed
Getting Help
If you've tried everything and still have issues:
- Check browser console for JavaScript errors
- Check Network tab for failed requests (note status codes)
- Check server logs for proxy errors
- Test with curl to isolate browser vs server issues
- Verify directly against
https://app.rybbit.io/api/script.js
Gather this information before asking for help:
- Framework/platform you're using
- Proxy configuration code
- Network tab screenshot showing the issue
- Any error messages from logs or console
- Whether it worked before (what changed?)
Related Resources
- Generic Proxy Guide - Core concepts
- Tracking Script Documentation - Script configuration
- Framework-specific guides for detailed setup