Troubleshooting
Find your symptom, then its fix. Each message RoutingCat prints is explained in the messages reference, and npx routingcat doctor, read check by check, names most of them.
init stops
init says |
Do this |
|---|---|
| No project it recognizes | Run init in your site’s own folder, where its configuration is. Plain HTML pages are recognized by public/index.html, or by index.html at the folder’s root: move pages that sit in another folder, such as www/, into public/. |
| Several sites below the folder | Run init in the folder of the site to set up. |
| The pages sit at the project’s root | Move them into public/, which init recognizes by public/index.html, so that your host never publishes the build’s credential, then run init again from the project’s folder. |
| Nuxt 2, built by a server | Set target: 'static' in nuxt.config and build with nuxt generate, then run init again. |
| It can’t tell where the site deploys or is served | Run init again with --host vercel or --host cloudflare-workers, which is a Worker in front of your server where it renders your site, or, for a static site, --host cloudflare-pages. Where neither Vercel nor Cloudflare serves it, --host none sets up the script integration, which the docs for any other website cover. |
| Laravel, Rails, Django, WordPress or PHP set up for the script integration | Where Vercel serves your site, or a Worker in front of your server, run init again with --host vercel or --host cloudflare-workers: it moves the site there, its pages, variants and results kept. |
--host refused |
Pages serves static sites only: a site your server renders takes --host vercel or --host cloudflare-workers. A site set up on Workers or Pages moves to Vercel or to the script integration, never to the other. |
| The folder isn’t linked to Vercel, or the Vercel CLI is signed out | Run npx vercel link or npx vercel login, then init again. |
| Wrangler is signed out, or reaches several accounts | Run npx wrangler login, or set account_id in the Wrangler configuration, then init again. |
| It needs your production address | Run init again with --origin and the address, as --origin https://www.lumen.com. |
| Workers Cache is on | Remove cache from the Wrangler configuration: see Cloudflare. |
| Installing the package failed | Install routingcat yourself, then run init again. |
| The sign-in isn’t approved yet, with exit code 3 | Approve the link, then run init again: it continues where it stopped. |
| Your host’s project belongs to a site you’re not a member of | Ask that site’s owner to add you. |
| A file is too unusual to edit | init prints the exact edit, such as the head partial’s include: make it, then run npx routingcat doctor, which checks it. |
Running init again is safe: it does what’s missing, brings RoutingCat’s own entries up to date, and leaves your edits as they are.
push or the build refuses your markup
push, plan and the build check your templates alike, and name the file, the line and the fix, here for a variant’s key written as a Hugo expression:
$ routingcat push✗ layouts/index.html:6: data-rc-variant is written as a template expression, {{ .Params.hero }}, so routingcat cannot read this slot from your templates. Write the key itself: lowercase letters, digits, - and _. (D66, exit 1)| The message says | Do this |
|---|---|
| Stamps or the head partial out of date | Run npx routingcat push, then commit what it changes. The build fails until you do. |
| Markup the scan can’t read | Write each key as itself, never as a template expression; close every tag inside the variant that opens it, in the same branch of a conditional; keep slots in HTML-syntax templates. |
| Markup that breaks its grammar | One default, without hidden; every other variant hidden, with no display in its style; every image lazy; every copy of a slot alike. |
| A slot the browser would move | Use span elements inside a paragraph, put the container in a table cell or a list item, and never nest a slot in another’s variant or in <head>. |
| A variant holding what acts while hidden | Move scripts, structured data and stylesheets out of the variant, give each id once, keep one h1 at most, and keep forms where they work once shown. |
The markup and routingcat.config.json disagree |
Declare under pages each page your markup names, with its routes, and name slots and variants as the markup does. |
| Built pages that differ from the templates | Build from the templates push stamped, keeping the slot markup as written: a minifier that drops data-rc-* attributes or hidden breaks the slots. |
| A routed page without an objective | Add the line it prints to routingcat.config.json. |
| A config field outside its rules | Correct the field it names. |
Build a page gives every rule with its reason.
The build registers nothing
A build that registered prints RoutingCat’s receipt in its log, starting routingcat LIVE, NOT LIVE or registered. Without it, the deployment shows your defaults and records nothing, conversions included. The build log says why, on a line starting routingcat:.
A build with no token and no deploy key says what to set where it runs. In Vercel’s build:
routingcat: no Vercel or GitHub Actions OIDC token and no ROUTINGCAT_DEPLOY_KEY, so this build cannot identify itself to RoutingCat: it registers nothing, and visitors get your defaults. On Vercel, turn on Settings → Security → Secure backend access with OIDC federation, then redeploy.In Cloudflare’s builds:
routingcat: no Vercel or GitHub Actions OIDC token and no ROUTINGCAT_DEPLOY_KEY, so this build cannot identify itself to RoutingCat: it registers nothing, and visitors get your defaults. Run npx routingcat keys deploy --branch <your production branch>, set what it prints as the build variable ROUTINGCAT_DEPLOY_KEY in Cloudflare's build settings, then redeploy.In any other CI, the line names what that CI takes. The build log’s other reasons:
| The build log says | Do this |
|---|---|
| RoutingCat can’t be reached | Redeploy once it’s back. |
| The build’s token or deploy key was refused | Check ROUTINGCAT_DEPLOY_KEY. A production key proves only builds of its branch, as your CI reports it, and in a CI RoutingCat doesn’t read, as ROUTINGCAT_BRANCH names it: see Cloudflare. On your machine, for a site on Cloudflare, run the build with ROUTINGCAT_ENV=production instead: see Deploy from your machine. On Vercel, check that the project is still bound to your site. |
| The Vercel project is bound to another site | Make site in routingcat.config.json the project’s site. |
| A build on your own machine | Nothing, for a build that tries your pages under wrangler dev or vercel dev. To deploy production to Cloudflare from your machine, run routingcat build with ROUTINGCAT_ENV=production: see Deploy from your machine. |
When the log holds no line of RoutingCat’s at all, your build command doesn’t run npx routingcat build: Set up RoutingCat gives it for each host.
Once a production build registers, its deployment registers at its first request. If none has 10 minutes later, open your site; if it still doesn’t register, check your host’s entry, as init wrote it: middleware.ts exporting middleware(…) from routingcat/vercel, a Worker whose main exports worker(…) from routingcat/cloudflare, or functions/_middleware.ts exporting onRequest = pages(…).
Pages show no decisions
A decision is RoutingCat’s choice of a visitor’s variants on a routed page, made by your host’s middleware or Worker. doctor checks each page’s first one.
-
RoutingCat didn’t run on the page (the middleware or Worker did not run). On Vercel,
middleware.tskeeps RoutingCat’s matcher entries. On Cloudflare,run_worker_firstkeeps RoutingCat’s patterns, or the Worker’s route covers your pages.doctornames the page, here on Workers:✗ Worker on / https://www.lumen.com/ answered without RoutingCat's visitor cookie, rc_vid, so the Worker did not run on /: visitors there get your defaults, and their visits are not recorded. The Worker's main must export worker(…) from routingcat/cloudflare, and run_worker_first must keep RoutingCat's patterns. (D82) -
Vercel runs the middleware on another runtime (not Node.js). Keep
runtime: 'nodejs'inmiddleware.ts’sconfig, then redeploy. -
The page came over plain HTTP (plain HTTP). RoutingCat leaves such views undecided: turn on “Always Use HTTPS” in Cloudflare.
-
The page isn’t live yet. Only a production deploy makes a page live, and for about 2 minutes after its first one on Vercel, the page shows its defaults and records arrivals only.
-
Routing is off. The build log’s receipt says so;
npx routingcat set routing=onturns it back on. When your plan turned it off, past Hobby’s visitors a second month running or at Pro’s cap, upgrade or raise the cap instead. -
Routing waits for a card. On Hobby, it starts once the site’s owner has a card on file: see Routing waits for a card.
-
A slot has one variant. Nothing is routed there until you add a second.
-
A cache in front serves the page (a cache in front). Stop caching routed pages’ HTML there: Run it in production.
-
The middleware or Worker lost access (no records). Redeploy, which mints a fresh credential. On Cloudflare’s Workers Free plan, the Worker also stops past 100,000 requests a day: RoutingCat needs Workers Paid.
-
Decisions come, but no render beacon. Keep the tracker’s script of RoutingCat’s head partial in each page’s
<head>, and let your Content Security Policy load it, by the page’s nonce on its tag or withhttps://cdn.routingcat.cominscript-src, and send its beacons through/_rc, with'self'inconnect-src. In your own browser, an ad blocker also stops it.
Visitors routed to a variant see the default
Every variant is in the page, and RoutingCat’s head snippet shows the routed one before the page paints. When the default shows instead:
-
The head snippet is missing from the page (no head snippet). Include the head partial first in the
<head>of the layout that renders it; on plain HTML, paste it in the page; on Nuxt 2, keep RoutingCat’s entries innuxt.config.js’s head, with the snippet’shidin__dangerouslyDisableSanitizersByTagID.doctornames the page and the partial, here on Vercel:✗ head snippet https://www.lumen.com/ carries no RoutingCat head snippet in its <head>, so visitors routed there see your defaults. Include layouts/partials/routingcat-head.html first in the <head> of the layout that renders /. (D89) -
Server-Timingdoesn’t reach the page (norcentry). The selection travels in it: remove the header rule or the proxy in front that drops it. -
The page’s Content Security Policy blocks the snippet or its style rules (the policy). Give the head partial’s script tags the page’s nonce in your template:
pushkeeps it, and the style rules the snippet writes carry it too. Or allow the snippet by its hash inscript-src,'sha256-VA7FIBNQntYn209pbrfrTBJ1g1fF+qHfG+08SyvP+D8=', which an upgrade ofroutingcatcan change; on Hugo, whose build rewrites the snippet, by the hashdoctornames, which changes with your build. With the hash, allow the style rules with'unsafe-inline'instyle-src, and no hash or nonce beside it there, which would cancel it: the rules differ from one visitor to the next. -
Rocket Loader runs the snippet late (Rocket Loader). Keep
data-cfasync="false"on its tag, as RoutingCat writes it. -
Behind a Worker in front of your server, the Worker didn’t register the page’s markup (unregistered markup). Until the Worker’s build and your server’s deploy serve the same templates, a slot whose markup changed shows its default and records no decision: deploy the Worker with your server’s templates.
-
The page’s markup differs from your templates (served markup), as when a minifier drops
data-rc-*attributes orhidden. Deploy whatpushstamped, the markup as written. -
The browser is too old to show a selection: Safari before 16.4, Chrome before 105 and Firefox before 121 get the defaults, unrouted.
When slots show as boxes instead, the policy refuses the markup’s style="display:contents". 'unsafe-inline' in style-src allows it; a style-src-attr of your own takes 'unsafe-hashes' and the hash doctor names, 'sha256-BDgIqUOjni0WelhOfIRW4cmSnxZNAa0L9tCq85UnRac=' for the value as RoutingCat writes it.
Routing waits for a card
On Hobby, routing starts once the site’s owner has a card on file, and nothing is charged. Until then, every visitor sees your defaults, and:
- a deploy that routes a page logs
routingcat NOT LIVE routing waits for a card on file: add one at …, nothing charged; doctorshowscard on filewaiting, with the link;npx routingcat set routing=onis refused, with the way out.
The owner adds a card in the dashboard’s Settings → Billing, with nothing charged, or starts Pro’s 14-day trial. Routing starts within about 2 minutes, with no new deploy. A member asks the owner. See Add a card to start routing.
A variant never shows
- Your browser keeps its variants for the whole visit, 7 days by default, and may be in the control, the 10% who always see the defaults. To see a variant, add
?rc=hero:learnsunderwrangler devorvercel dev, afternpx routingcat build, or use a preview link in production: Change, preview and deploy. - It’s paused or retired.
npx routingcat show home/hero:learnssays so, andnpx routingcat resume home/hero:learnsbrings it back. - The plan holds it. On Hobby, past the 10 live variants across the owner’s sites, a variant is registered and not served: see A variant is held.
- It’s new, or routing favors others. A new variant starts on a limited share, and routing serves most what is likely best, though the random slice shows every variant now and then.
showgives its share. - It was added during a visit. A slot added mid-visit shows its default to that visitor until their next visit.
- Its build failed after registering (a build that failed after registering). Visitors keep the previous version: fix the build and redeploy, or revert the page.
- Browsers report it shown less often than the slot’s other variants (for a smaller share of the page loads it was picked for). Check that it renders as fast as they do.
A variant is held
On Hobby, the sites you own serve at most 10 live variants together, the slots’ defaults not counted. A deploy or an act past them still succeeds, and each variant past the 10th is held: registered, and not served. The build log names it after its first line, status reads held on its row, and the dashboard marks it “Held by the plan”.
To put it live, either:
- upgrade to Pro in Settings → Billing, for every site you own, which allows up to 100 live variants a page;
- or retire or pause a live variant on any of your sites, which frees room.
Held variants then go live in the order they were declared, within about 2 minutes, with no new deploy. See Go past Hobby’s 10 live variants.
Conversions don’t join
Track conversions explains the tracking line of status. The messages behind it:
- Conversions with no key: send
visitor_id, from therc_vidcookie, oraccount_id. - Accounts never linked: send an identity link at sign-up and at every sign-in.
- Identify without a visitor id: often a callback posted from another site, which carries no cookie. Identify at the next page load instead.
- The preview server key used in production: use the production key.
- Events under an undeclared name: declare the name in
routingcat.config.jsonif it matters. - No event yet, a silence or a drop: check that your code still sends the event, and after a drop, record a tracking gap.
- Conversions with no routed visit need nothing: they count once their visitors see a routed page.
The control’s share is off
status raises the control check when the control’s share of new visitors departs from its setting, which can bias the measured gain. Check that page for a cache in front, or a matcher of RoutingCat’s middleware or Worker that skips it for some visitors.
A preview link fails
| What happens | Do this |
|---|---|
preview says the production deployment doesn’t carry the version |
Run push, commit, and deploy it to production first, the Worker too where one stands in front of your server. |
preview says no deployment serving it has registered yet |
Right after a deploy, open your site, then run preview again. If the build failed after registering, deploy again. |
preview says the page has no path to preview |
Pass one: --path /pricing. |
preview refuses variants of several pages, or several of one slot |
Preview one page at a time, one variant per slot. |
| The page opens without the preview badge | The link is valid for one hour: make a new one. The badge comes from the tracker’s script in the head partial, so keep it there. |
| The badge names another variant | The badge reads what the page shows. Right after a deploy, a new instance can serve the page before it knows the variant: reload a few seconds later. |
To end a preview before its hour is up, follow the badge’s link, or open the page with ?rc_preview=off at the end of its address.
Run commands in a sandbox
Coding agents often run commands in a sandbox, as Work with your coding agent explains:
- No network, exit code 4: allow network access for the command, or ask the person to run it.
- The sandbox can’t keep a sign-in, exit code 3: run
npx routingcat loginin your own terminal, then the command again. - A sign-in link is waiting, exit code 3: give the link to the person, and once they approve, run the same command again.
- A production build on the machine waits for the person, exit code 3: show them the plan it printed, with any warning beside it, and once they agree, run the build again with
ROUTINGCAT_CONFIRMset to its digest, besideROUTINGCAT_ENV=production. doctor --waitruns up to 5 minutes: give it a 6-minute timeout, or run it in the background.
push, push --check and push --dry-run read and write your templates only, with no sign-in.
Read doctor check by check
npx routingcat doctor reads your project, your sign-in, your site’s records and your production pages: /, and each page’s url in routingcat.config.json. It changes nothing. Its requests are recorded as arrivals, flagged as RoutingCat’s own and kept out of results. After a deploy, --wait waits up to 5 minutes, printing each check as it passes, and stops early when only a person can move the rest. Set up RoutingCat shows a run where every check passes.
| Check | Passes when | Otherwise |
|---|---|---|
| Your files, shown only when one fails | init‘s edits, your templates’ stamps and head partial, the site’s public key in routingcat.config.json, and an up-to-date skill |
an edit to make, stale stamps or partial, the wrong public key, the skill |
| build registered | A production build registered, with the objective and outcomes it declares | Waits for a production deploy |
| deployment registered | The deployment serving that build registered at its first request | No deployment registered after 10 minutes |
| runtime, on Vercel | Vercel runs the middleware on Node.js | Another runtime |
| Worker version, on Workers | The deployment registered under its Worker’s version | No version metadata binding |
| middleware on /, or Worker on / | Your production page set rc_vid |
RoutingCat did not run, or a cache in front answered. On Cloudflare, it waits until routingcat.config.json lists your production address: run npx routingcat init --origin with it. |
| /_rc | Your site’s /_rc path reaches RoutingCat |
Browser events skip /_rc |
| head snippet | Each page carries RoutingCat’s head snippet | The snippet missing |
| selection | Each routed page’s answer carries its selection in Server-Timing |
The selection missing, RoutingCat did not run, a cache in front, or a build made for development on a person’s machine |
| policy | Your pages’ Content Security Policy lets RoutingCat run, apply and record | What it blocks |
| Cloudflare’s features | No zone feature changes what your pages run | Rocket Loader, Zaraz before the snippet, or plain HTTP |
| records | RoutingCat accepts records from your production address | Records refused |
| markup | Each page’s slots, as served, match your templates, and behind a Worker in front of your server, the markup its deployment registered | Markup that differs, markup the Worker didn’t register |
| first arrival | The pulse shows an arrival, often the check’s own request | Waits about a minute |
| The objective, by its name | A first conversion arrived | No event yet: waits for the first real conversion, a visitor’s or your own, and nothing else waits on it |
| card on file, on Hobby | The site’s owner has a card on file, shown by its brand and last 4 digits | Waits for the owner to add one, at the link it gives: routing waits for a card |
| page live, first decision, first render beacon | Each routed page is live, decided, and shown in a browser | Waits for a visit; no render beacon, or a cache in front |
| server key, location headers, shown only when one fails | The preview key used in production, Cloudflare in front of Vercel without its location headers |
Lines marked · are information and need nothing: conversions with no routed visit yet, an undeclared name, and, run in your project, whether AGENTS.md has the note for coding agents, which is your choice.
doctor exits with 0 when every check passed, 1 when one failed, 3 when one is still waiting, and 4 when the network blocks it.