Fixes for the errors you're most likely to hit during a deploy
A field guide to the errors you're most likely to see when running bunx percher publish. Every error name below matches what the CLI and dashboard show, so you can search for it verbatim.
The app name in percher.toml is in use by another account. App names are global to the platform — they become part of the public URL (name.percher.run), so they have to be unique. Pick a different value for app.name and try again.
The runtime field must be one of node, bun, python, static, or docker. Anything else fails parsing. See the percher.toml reference for the full list of allowed values.
web.port must be between 1024 and 65535. Privileged ports below 1024 aren't supported because Percher app containers don't run as root.
Your project tarball is over 500 MB. Almost always this is node_modules, a stale .next / dist directory, large committed binaries, or .git history that wasn't excluded. Check what would actually be uploaded:
bunx percher publish --dry-run
Percher honors .gitignore plus a built-in default exclude list: node_modules, .git, dist, .next, .svelte-kit, build, coverage, .env*, .bun, *.log, and .DS_Store. If a file you don't need is listed in --dry-run, add it to .gitignore.
Your build command exited non-zero. Pull the build log to see exactly which step blew up:
bunx percher logs --build
You can also view the same log in the dashboard under the failed deploy. Common causes: a missing dev dependency, a TypeScript error that only shows up in production builds, or a build script that assumes an env var is set without declaring it (see REQUIRED_ENV_MISSING).
The app started but its health endpoint either returned a non-200 status or didn't respond in time.
First, check whether the live site is actually broken. Visit https://your-app.percher.run/health in a browser. If it returns 200 and the app is responding normally, the pipeline's health-check step probably hit a transient network blip — the previous version is still serving (correct rollback behavior) and the new build was never swapped in. Update the CLI and try again:
bunx percher@latest publish
If the live site is also unhealthy, it's a real app problem. Check three things:
[web].port.[web].health matches the route in your code.An environment variable your app needs at runtime isn't set in the encrypted env store. Set it with one of these (file/stdin/interactive modes are recommended for real secrets so the value never enters your shell history):
# inline (value visible in shell history — avoid for real secrets): bunx percher env set OPENAI_API_KEY=sk-... # from a file (recommended for secrets): bunx percher env set OPENAI_API_KEY --from-file ./secret.txt # from stdin: echo "$MY_SECRET" | bunx percher env set OPENAI_API_KEY --from-stdin # interactive prompt (hidden input, sudo-style): bunx percher env set OPENAI_API_KEY
Then re-run bunx percher publish. Env changes only take effect on the next deploy.
DAILY_QUOTA_EXCEEDED (HTTP 429)You've hit your per-day deploy cap. Defaults are: Free 50 live + 25 preview, Starter 100 / 50, Maker 200 / 100, Pro 1000 / 500. Counters reset at 00:00 UTC; the error response includes the exact resetAt timestamp.
Retrying immediately won't work — the cap is enforced until reset. Live and preview counters are independent, though, so if you've exhausted your live quota you can still preview-deploy:
bunx percher publish --preview
If you regularly hit this, upgrade at /settings.
DEPLOY_RATE_LIMITED (HTTP 429)Per-app burst limit on a sliding 60-second window. This is different from the daily quota — it is retryable after a short wait. The error response includes a retryAfterSec field telling you how long to wait.
RETRY_LIMIT_REACHEDPercher auto-retries transient infra failures up to a per-deploy limit. When that limit is hit, the platform stops retrying and surfaces this error so you can decide what to do — usually that means trying again manually after a few minutes, or contacting support@percher.app if it persists.
already_in_progressA previous deploy for the same app is still queued or building. Wait for it to finish (or cancel it from the dashboard) before kicking off another one. The error response includes the active deploy id so you can find it in the deploys list.
percher.toml supports an explicit env contract that catches missing-env problems at upload time, before a 90-second build wastes your quota:
[env] required = ["OPENAI_API_KEY", "DATABASE_URL"] # must be set before deploy queues optional = ["SENTRY_DSN"] # may be referenced; not required ignore = ["NODE_ENV"] # explicitly ignored by the scanner
REQUIRED_ENV_MISSINGA key listed under [env].required (or detected as required by the source scanner) isn't set in the encrypted env store. Set the missing keys and re-run publish:
bunx percher env set OPENAI_API_KEY --from-file ./key.txt
ENV_KEY_UNDECLAREDYour source code references an env key that isn't in any of required, optional, or ignore. Add it to one of those lists in percher.toml so the scanner knows how to treat it. Use required for keys the app can't boot without, optional for nice-to-have integrations, and ignore for keys the platform sets for you (like NODE_ENV).
The app itself is almost certainly fine — this is usually third-party fonts hanging the first paint. Apps generated by Lovable, v0, shadcn, and similar starters typically load fonts directly from https://fonts.googleapis.com/css2?family=.... When the visitor's network has any trouble reaching Google's font CDN — a brief incident, an aggressive ad-blocker, a corporate firewall, a Pi-hole — the browser hangs 10–19 seconds on that one request before giving up and falling back to system fonts.
How to detect. Open DevTools → Network → reload. If you see a single fonts.googleapis.com request stuck in (pending) for many seconds before failing with ERR_FAILED, you're hitting this. Every other request (HTML, your bundle, your API) will look fast — only the Google Fonts row hangs.
Why Percher can't fix it server-side. The fonts request goes directly from the visitor's browser to Google. It doesn't pass through Percher's reverse proxy or your VPS.
Fix: self-host fonts. Best practice for production apps in any case — also avoids GDPR issues with Google-Fonts-embed and removes a cross-origin TLS handshake from the critical path.
For Next.js:
// app/layout.tsx — replace any <link href="fonts.googleapis.com/...">
import { DM_Sans, JetBrains_Mono, Inter_Tight } from "next/font/google";
const dmSans = DM_Sans({ subsets: ["latin"], display: "swap" });
// next/font downloads + self-hosts at BUILD time. No runtime call to Google.
// Apply via <html className={dmSans.className}>.For Vite, SvelteKit, Astro, or Solid:
bun add @fontsource/dm-sans @fontsource/jetbrains-mono
/* in app.css or main.tsx */ @import "@fontsource/dm-sans/400.css"; @import "@fontsource/dm-sans/500.css"; @import "@fontsource/jetbrains-mono/400.css";
For plain CSS / static sites: download the font files from fonts.google.com, drop them in public/fonts/, and reference them via @font-face with src: url('/fonts/...woff2').
After redeploying, the first visit may still hang once if a stale service worker is cached — tell affected users to unregister via DevTools → Application → Service Workers, or just hard-reload twice.
The build pipeline has an image cache that skips a full rebuild when the build inputs haven't changed (keyed on a content hash that includes percher.toml, package.json, the lockfile, Dockerfile, build env, and every source file in the tarball). If a deploy reports Build cache: hit and you expected new behavior, force a fresh build:
bunx percher publish --no-cache
The freshly-built image gets registered into the cache so future deploys still benefit. Every successful publish prints either Build cache: hit or Build cache: miss (fresh build) so you can tell at a glance which path ran.
If --no-cache still shows the same stale behavior, the source bytes themselves aren't reaching the upload. Check that you're running publish from the project root (not a subdirectory) and that .gitignore isn't excluding the changed files.
Email support@percher.app with the deploy id (visible in the dashboard or printed by the CLI on failure) and the build log if you have one. We reply same-day on paid plans.