Troubleshooting
The errors you will actually see, what they mean, and the fix.
First: is it your query, or the database service?
Almost every database error answers this mechanically, and getting it wrong sends you debugging the wrong layer. Postgres returns a five-character SQLSTATE with every error, plus a position when the parser read your statement.
The one-line rule: if the error has a position, the server parsed what you sent — it's your SQL. Service problems fail before the parser ever sees the statement, so they never carry a position.
| Signal | Whose | What it means |
|---|---|---|
Any error with a position: | Yours | The server read your statement and pointed at a character in it |
42601 syntax_error | Yours | Malformed SQL — often a client-side query builder producing bad text |
42703 / 42P01 / 42883 | Yours | Undefined column / table / function — schema mismatch |
23505 / 23503 / 23502 | Yours | Unique, foreign-key or not-null violation — your data |
57014 query_canceled | Yours | Hit the plan's statement_timeout; batch the work or use the direct URL |
40001 / 40P01 | Yours | Serialization failure or deadlock — retry the transaction |
08P01 protocol_violation | Ours | Startup parameter rejected at handshake — see below |
42P05 duplicate_prepared_statement | Ours | Prepared statements through the transaction pooler — set prepare: false |
53300 too_many_connections | Ours | Plan connection budget exhausted |
57P01 / 57P03 | Ours | The cell was paused/resumed mid-statement — retry |
| TLS, DNS or connect errors (no SQLSTATE at all) | Either | Never reached the database — check your connection string first, then us |
"No SQLSTATE" narrows it to the connection layer, but that layer is shared: a missing or wrong connection string fails exactly like an unreachable service. Two tells that point back at your side:
address: undefined, port: undefined(or a connect attempt tolocalhost) means the driver never had a host — the environment variable was empty where the code ran, not that we were down.- The error happens during a build rather than at request time. Frameworks that prerender will
execute route handlers at build time; if those read the database, the build needs a live
connection. In Next.js, mark database-backed routes
export const dynamic = 'force-dynamic'(and droprevalidate) so they run per request instead. A paused cell resuming is not the cause here — resumes are held and served, they do not time out.
The dashboard's SQL runner applies this classification for you: statements that fail on their own merits come back with the real Postgres message and position, while service-side failures are reported separately.
A worked example — this is your SQL, not the service, because of the position:
PostgresError: syntax error at or near "SELECT"
position: '629'
routine: 'scanner_yyerror'routine: scanner_yyerror is Postgres's own parser. You would get the byte-identical error on any Postgres anywhere.
Connection problems
connection refused / timeouts
- Check the host and port: direct is
5432, pooled is6432. A URL pasted into the wrong slot is the usual suspect. - The project must be in
readystate - a project mid-provision or mid-restore does not accept connections yet.capydb status --remoteor the dashboard shows the state. - Corporate networks sometimes block outbound 5432/6432; test from another network before blaming the database.
TLS errors (SSL connection required, server does not support SSL)
- Keep the
sslmodeparameter in the URL. CapyDB requires TLS; clients that strip query params (some env-var templating, some GUI clients) produce exactly this error. - For asyncpg,
sslmodeis not a recognized parameter - passssl=truein connect args instead.
root certificate file "~/.postgresql/root.crt" does not exist
You are using a libpq client (psql, pgAdmin, Rails, Django, psycopg/SQLAlchemy) with sslmode=verify-full, which issued URLs carry. libpq does not read the operating system trust store by default - it wants its own root.crt. The certificate is fine; the client just does not know where to look. Point it at the system roots:
psql "$DATABASE_URL&sslrootcert=system" # libpq 16+On libpq 15 and older sslrootcert=system is not recognised - give it an explicit bundle instead (sslrootcert=/etc/ssl/certs/ca-certificates.crt on Debian/Ubuntu, /etc/ssl/cert.pem on macOS).
sslrootcert is a client-side option: libpq consumes it locally and never sends it to the server, so it is safe to keep in a URL shared between tools.
certificate verify failed: unable to get local issuer certificate
The client has no CA bundle at all rather than a bad certificate - common in slim container images:
# Alpine
RUN apk add --no-cache ca-certificates
# Debian/Ubuntu slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificatesPython clients using certifi (and asyncpg, which takes ssl= rather than sslmode=) need the bundle wired into the SSL context rather than the URL.
Downgrading to sslmode=require also makes the error go away, but it stops verifying the certificate entirely and re-opens the machine-in-the-middle gap - fix the trust store instead.
too many connections / remaining connection slots are reserved
The plan's connection budget (15 / 30 / 60 for Vibe / Ship / Business) is shared by everything you run.
- Move app traffic to the pooled URL - that is what it is for.
- Shrink client-side pool sizes (
max,pool_size,RAILS_MAX_THREADSmath). - Check Observability for idle direct sessions holding slots - GUI clients left open are a classic.
unsupported startup parameter: ... (code 08P01)
Your driver sends a GUC in the connection startup packet - postgres.js does this for everything in connection: {...}, libpq for options= - and the pooled (:6432) endpoint does not accept that parameter, so every connection fails at handshake. Timeout GUCs (statement_timeout, idle_in_transaction_session_timeout, lock_timeout, idle_session_timeout) are accepted but silently not applied under transaction pooling; anything else (notably search_path) is rejected. Either way the durable fix is the same: ALTER ROLE your_role SET <param> = ... once on the direct URL, remove the option from the driver config. Details in Connection pooling.
prepared statement "..." already exists
You are running a prepared-statement-caching driver through the transaction pooler. Fixes per stack are in the framework guides: prepare: false (postgres.js), prepared_statements: false (Rails), simple protocol mode (pgx), statement_cache_size=0 (asyncpg) - or use the direct URL.
API and job errors
project must be ready / project is not ready for this operation
The operation needs a ready project, and yours is provisioning, restoring, importing, or failed. Wait for the in-flight job (capydb jobs get --job-id ... --wait) or check what failed in the dashboard. Jobs run one lifecycle mutation at a time per project by design.
Import preflight failures
| Check | Fix |
|---|---|
source_size_within_plan fail | The source is bigger than the plan's storage limit. Trim the source (drop logs/archives tables), or upgrade the plan. The warn threshold is 80% - heed it; databases grow. |
source_version_compatible fail | The source runs a newer Postgres major than this database's selected target major. Downgrades are not supported. Import from a logical copy on or below the target major, or create a new CapyDB database on a compatible supported major (16, 17, or 18). |
extensions_available_on_target fail | The detail names the offending extensions. Drop unused ones on the source (DROP EXTENSION ...), replace replaceable ones (uuid-ossp → pgcrypto), or contact support about the allowlist. |
could not inspect source database | The source is not reachable from the internet, credentials are wrong, or TLS failed. The same check will fail the real import, so fix it now. See the provider playbooks for reachability gotchas (RDS security groups, Fly private networking, Render allow-lists). |
The worker re-runs these checks before the destructive part of an import - passing preflight yesterday does not exempt a source that grew overnight.
preview database must be ready to extend its ttl / preview gone
Previews expire on their TTL - that is the feature. If a preview vanished, it expired or was deleted; create a new one (the GitHub Action and branch integrations do this idempotently). Extend TTLs before they lapse: capydb preview extend <id> --ttl-hours 48.
ttl_hours must be between 1 and 168
The hard preview TTL ceiling is 7 days. For something permanent, that is a project, not a preview.
Webhooks
Signature verification keeps failing
- Verify against the raw request body, byte-for-byte. Parsing then re-serializing JSON is the number-one cause of mismatches (Express: use
express.raw()on the webhook route). - The signed string is
"<t>.<body>"- timestamp, a literal dot, then the body. HMAC-SHA256, hex output, compared againstv1=. - Confirm you are using the current secret: rotating the endpoint secret invalidates the old one immediately.
- See the reference implementation.
Deliveries marked failed
Eight attempts exhausted (backoff 30s → 30m). Common causes: receiver returning non-2xx (redirects count as failures), timeouts from doing work inline - return 204 first, process after. The delivery history on the endpoint shows the status code and error per attempt.
CLI
no api key available; pass --api-key, set CAPYDB_API_KEY, or run 'capydb login'
Exactly what it says - the CLI fails fast instead of hanging on missing auth. Run capydb login, or provide a key for non-interactive use.
login session expired or not found; run 'capydb login' again / login session poll was rejected (unauthorized)
The browser device-login flow timed out or was completed in a different context. Re-run capydb login; use --no-open to print the URL if the browser hand-off is the problem (SSH sessions, containers).
capydb link cannot find the project
--project accepts an id, slug, or name; names must match a project in the active organization - check capydb whoami and switch the dashboard org if you logged in against the wrong one.
Still stuck
Include the project id, the job id (for lifecycle failures), and the exact error string when contacting support - the audit log and job records make those three things instantly diagnosable.