Skip to content

Troubleshooting

Each entry gives the message you see, what it means and how to fix it. Messages from the CLI start with error:. Add --json to any command to get the error as JSON, with a code such as not_logged_in, unauthenticated, not_found or plan_limit. A command line the CLI can’t read is invalid_argument, and a failure on your computer with no code of its own, like a folder with nothing to deploy, is failed.

error: You're not signed in. Run `forculus login`.

This computer has no session. Run forculus login. Each config directory has its own session, so if you set --config-dir or FORCULUS_CONFIG_DIR, check it. With --json, the code is not_logged_in. For a session that has ended, it’s unauthenticated.

error: Your session has ended. It was signed out, revoked or expired. Run `forculus login`.

The session expired, was signed out or revoked on the dashboard’s Sessions page, or a newer forculus login on this computer replaced it. Run forculus login again. A CLI session lasts 30 days.

error: You need to accept the terms of service dated 21 October 2026 first. Sign in to the dashboard at https://app.forculuslabs.tech, accept them, then run this again.

You can’t create a workspace, an app or a token until you’ve accepted the current terms, and you can only accept them in a browser. Sign in to the dashboard, accept them, and run the command again.

When the terms change, the dashboard asks you to accept the new ones the next time you sign in. If you accepted the earlier terms, you can choose Not now. These commands keep working until 30 days after we email workspace owners about the change. After that, they’re refused until you accept.

The dashboard says Check your email, but no code comes. If it hasn’t come in a minute or two, check your spam folder, then choose Send a new code. If that doesn’t come either, write to support@forculuslabs.tech. The page can’t tell you when an email fails to send, so a missing code looks the same as a slow one.

Too many sign-in codes have been sent. Try again in 42 minutes.

An address gets 5 email codes an hour from one network and 10 from anywhere, counting only the codes nobody signed in with, and 30 an hour in all. Wait as long as it says, or sign in with a passkey you’ve added, or with Google or Microsoft if your address works with one. A code you signed in with doesn’t count, so signing in again to approve a CLI or delete your account doesn’t use these up. If you didn’t ask for the codes, someone else may have typed your address. They can’t sign in without your inbox. See How many codes you can get.

This account has been closed. If you think that's a mistake, write to support@forculuslabs.tech.

We closed the account at this address, for example under the repeat-infringer policy in the terms, so it can’t sign in. We don’t email it sign-in codes, and a code typed in for it gets this message. If you think we got it wrong, write to support.

error: Couldn't reach https://api.forculuslabs.tech: ...

The CLI couldn’t connect. Check your network. If your network has a proxy that inspects TLS, like Zscaler or Netskope, the rest of the message is usually a certificate error. The CLI trusts what your operating system trusts, so this means the proxy’s CA isn’t in your system’s store. Point FORCULUS_CA_FILE at the proxy’s CA bundle, in PEM. If that isn’t set, the CLI uses SSL_CERT_FILE. It trusts the file’s certificates as well as your system’s. See Use it behind a proxy that inspects TLS.

If the CLI can’t use the file, every command stops at once instead of failing later with a certificate error:

error: /Users/you/corp.pem, set in FORCULUS_CA_FILE, has no certificates in it.

If the reason after Couldn't reach doesn’t help, run the command again with FORCULUS_DEBUG=1 set. The message then ends with what the connection itself said, which is worth sending to support.

What you’re told depends on your role in the workspace. A workspace owner or admin sees every app in it, so the answer says plainly that there’s no such app:

error: There's no app hello in acme. `forculus app list acme` lists its apps.

A deploy without --create ends it with the command that makes the app, as Your first deploy shows:

error: There's no app hello in acme. `forculus app list acme` lists its apps. `forculus deploy acme/hello ./hello --create` makes it.

A member may be asking for an app that exists but isn’t shared with them. A GitHub Actions run gets this answer too, even one an owner set up:

error: App acme/hello not found, or it isn't shared with you. `forculus app list acme` shows the apps you can see. If it doesn't exist, `forculus deploy acme/hello <dir> --create` creates it. If that says the name is already used, ask a workspace owner or admin to share it with you. `forculus workspace list` shows your workspaces.

There’s no such app, or you can’t see it. A member gets the same answer for both, so the message doesn’t reveal which apps exist. Check the name with forculus app list acme. Apps you can’t access aren’t listed there, so ask an owner to share it.

Someone who isn’t in the workspace is told that. If the app isn’t shared with them, they get the same answer whether or not it exists:

error: Not found: you can't see acme/hello because you aren't in a workspace called acme. `forculus workspace list` shows your workspaces, and `forculus app list --shared` the apps shared with you.

If it is shared with them, they’re told so, since they can open it:

error: acme/hello is shared with you and you can open it. You aren't in the acme workspace, though, so you can't see or change anything else about it. `forculus app list --shared` lists what's shared with you@example.com.

App names are unique in a workspace. If --create says That name is already used in this workspace, maybe by an app that isn't shared with you, an app you can’t see has it. Ask a workspace owner or admin to share it, or pick another name.

error: Workspace acme not found, or you're not in it. `forculus workspace list` shows yours, and `forculus workspace create <slug>` creates a new one.

Workspace slugs must match exactly. forculus workspace list shows the ones you belong to. A deleted workspace’s slug stays taken for 30 days, so you can’t reuse it straight away.

error: This workspace has 3 apps. The Free plan allows 3. Delete one or upgrade. Team allows 25 apps. Billing: https://app.forculuslabs.tech/w/acme/billing
error: The Free plan only runs JS and static apps. Container apps, like Python, Go, Rust and Node servers, need a paid plan. Team runs container apps. Upgrade the workspace at https://app.forculuslabs.tech/w/acme/billing

The workspace’s plan doesn’t allow one more of something. The message names the limit and the plan that allows more. An owner can upgrade on the billing page it links to. A member is told only that there’s no room, not how many apps the workspace has, and to ask an owner or admin. See Limits and Plans and billing.

forculus deploy checks the workspace before it builds anything, and stops:

error: The workspace acme is suspended. Nothing can be deployed to it until that's lifted. Contact support.

Any other command that would change something fails with:

error: We've suspended this workspace. Nothing can be deployed or changed until we lift the suspension. Contact support. The dashboard, billing, export and deletion still work.

If it ends with “The dashboard, billing and export still work.” instead, you can’t delete the workspace or anything in it either. Its apps don’t answer. Contact support. A workspace suspended over a payment dispute gets a different message, which says when it will work again.

error: Older audit log entries are busy right now. Try again in a few seconds.

Entries more than 30 days old live in separate storage. We read it one request at a time for each workspace, and only a few at once overall. Another read was running: yours in another tab or command, someone else’s, or an export. forculus audit retries a few times before it shows this. Wait a moment and run it again. An older forculus can hit this when a page goes far back. Update it and it reads those pages in steps instead.

error: We can't read older audit log entries right now. Our team has been alerted.

We couldn’t reach the storage that holds entries more than 30 days old. Trying again won’t help until we fix it. Newer entries still work: run forculus audit with --before set to a date in the last 30 days.

error: Logs are busy right now. Try again in a few seconds.

We read logs from Cloudflare a few at a time: a read every two seconds for each person, a few at once, and only so many for everyone. You, or everyone together, asked faster than that. forculus logs waits and asks again a few times before it shows this, and --follow keeps trying. Wait a moment and run it again. The API answers 503 with the code busy and a Retry-After header that says how many seconds to wait.

error: This has both a Dockerfile and wrangler.jsonc, so it could be either kind. Pick one with --kind=container or --kind=worker.

The directory looks like two kinds of app. Pass --kind, or set kind in forculus.json so every deploy uses the same one. See How deploy decides what to build.

The same goes for a page beside a Worker’s code:

error: This has index.html and src/worker.ts, so it could be a site or a Worker. As a site, src/worker.ts would be served as a file. Pick one with --kind=worker or --kind=static, or set "kind" in forculus.json.
error: The API returned 500 Internal Server Error. If it keeps happening, tell support it was request 8c2f0a1b2c3d4e5f-LHR.

Something failed on our side, not in your request. Try again in a moment. If it fails again, write to support@forculuslabs.tech with the request it names, which is how we find it in our logs.

error: We can't tell what this is. We looked for:
- a Dockerfile
- a wrangler config (wrangler.jsonc, wrangler.json, wrangler.toml)
- package.json with a Cloudflare adapter, a static builder or a server
- requirements.txt, pyproject.toml or Pipfile
- deno.json
- go.mod
- Cargo.toml
- index.html
- an entry point (index.js, index.mjs, src/index.js, src/index.mjs, dist/index.js, dist/index.mjs, worker.js, worker.mjs, src/worker.js, src/worker.mjs, src/index.ts, index.ts, src/worker.ts, worker.ts)
Set the kind with --kind, or in forculus.json.

Check you pointed the deploy at the right directory. If you did, say what it is with --kind or forculus.json.

A static site’s deploy says:

error: .env looks like a credential, and everything here is served to everyone who can open the app. Remove it from the directory you're deploying.

A Worker’s names the whole path:

error: /Users/you/hello/.env looks like a credential, and apps shouldn't hold one. Store it with `forculus secret set` and remove the file.

A container’s lists every file the build would copy into the image. It also lists any that are in the directory but that the Dockerfile forculus writes leaves out:

error: These look like credentials and the build would copy them into the image: config/.env. So do .env, which the image leaves out. Apps shouldn't hold credentials. Store them with `forculus secret set`, then delete the files or list them in .dockerignore.

The deploy refuses to upload .env files, private keys and anything else that looks like a credential. Move each value to where it belongs, then remove the file from the directory:

error: STRIPE_SECRET_KEY wasn't set because its name says it's a credential. Credentials belong in `forculus secret set`. If this really is configuration, pass --allow-sensitive-name.

Store credentials with forculus secret set, so your app doesn’t hold them. A webhook’s signing secret is different: it goes on the path’s verifier in the dashboard, and we check each delivery with it. See Public paths and webhooks. If your app really has to hold the value, pass --allow-sensitive-name to forculus env set. A value that is clearly a credential, like a live API key or a whsec_ signing secret, is refused even then.

error: We set FORCULUS_APP_ID ourselves. Pick another name.

Names starting with FORCULUS_, CF_ or CLOUDFLARE_, and a few others such as PORT, DB and FILES, are reserved. So is KV once the app has key-value storage, and QUEUE once it has a queue. Pick another name. See the full list.

error: This app has a variable named KV, which is the name of its key-value storage. Nothing was changed. Remove it with `forculus env remove acme/hello KV`, then try again.

The app took the name before it was refused, and its Worker can’t go up with it. Remove it as the error says, then make your change again.

error: Cloudflare refused the app's code: Uncaught Error: No such module "forculus.mjs".
imported from "index.js"

Cloudflare loads a Worker when it’s uploaded. It refuses one that doesn’t parse, throws on startup, imports a module that isn’t there, is too big, or handles no requests. The rest of the message comes from Cloudflare and is about your code. A Worker with no build goes up as the files in its directory, so every module it imports has to be one of those files.

Fix it and deploy again. The version is listed as failed, and the previous version keeps serving. The audit log has the same reason, under app.deploy_failed.

error: We stopped partway through putting this version live. Deploy again.

We stopped while putting the version live, and nothing is left to finish the job. The version is listed as failed, but it may have got far enough to serve some or all requests. When it has files, as a site does or code that ships them, we put the previous version back up ourselves. Until that’s running again, usually seconds, the app answers 503 with “The app is starting”, and the audit log gets an app.put_back once it’s done. For any other version, you can’t be sure the previous one is what’s running. Either way, run the same deploy again, or roll back to the version you want live. A site’s files are already uploaded, so they aren’t sent again.

error: You could no longer edit this app by the time version 4 went up, so it isn't live. We put version 3 back live.

When forculus deploy was waiting for the version and the person who ran it can’t read the app’s versions any more, it says:

error: You could no longer edit acme/site by the time version 4 went up, so it isn't live. Version 3 stays live.

The version is listed as failed, with “Whoever deployed this version could no longer edit the app by the time it went up, so it isn’t live.” The person who deployed it was removed from the workspace, or lost their editor role on the app, while it was going live. We check again before the code goes up, and again when we mark the version live. If their code was already up by then, we put back the version that was live before, or take the app down if there wasn’t one, and record that in the audit log as app.deploy_failed with put_back, or later as app.put_back. If their code never went up, there’s nothing to put back: the version that was live stays live, and the audit log has only app.deploy_failed with the reason not_allowed. The end of the message says what happened to the app. Someone who can still edit the app can deploy it again.

From the moment their access ends until we know which it is, the app answers 503 with “The app is starting”, and their code gets none of the app’s secrets, even when their code never goes up. This usually takes seconds, and longer while Cloudflare’s API is failing. For a static site it can last until the rest of its files are uploaded, a minute or more for thousands of files. Someone already using the app may reach it for a minute or two. The audit log records these requests as putting_back. The app answers as usual once the previous version is running again.

For a container app, we have to wait for their image’s rollout to finish, then roll the old image out again. This can take a few minutes. Meanwhile their image gets no requests and can’t reach the internet. If Cloudflare’s API was failing when we started, their image can reach the internet, still without the secrets, until we get through.

error: Version 4 is still going live. `forculus versions acme/site` shows when it's live, or why it isn't. Newer versions of forculus wait for it.

A deploy that takes longer than about a minute, like a site with thousands of files, carries on after we’ve answered. An older forculus can’t wait for it, so we tell it the version is still on its way. Run forculus versions acme/site until the version is deployed or failed, and update forculus so the next deploy waits by itself.

The deploy step in a GitHub Actions workflow stops with:

This GitHub run can't deploy acme/site. An owner of the app names the repository, and a branch or environment, under Deploy, GitHub Actions. A run started by a pull request puts up a preview only once they turn on previews of pull requests there.

The run’s repository, branch or environment isn’t the one saved for the app, or nothing is saved yet. Compare the run with the app’s Deploy, GitHub Actions tab. A run from a pull request never deploys. With previews of pull requests on, it puts up a preview instead. See What can go wrong for the other messages.

A page load with no session goes to the dashboard’s sign-in page and back. This is normal the first time someone opens an app, and again after their session ends. After signing in, they land on the page they asked for.

A page titled Sign in again means the browser came back from signing in, but the app couldn’t finish it. The sign-in took more than 10 minutes, it was finished in a different browser, or the link was used already. The page’s Open the app link starts a new sign-in. The audit log records it as invalid_handoff.

If you open a used sign-in link again, from your history for example, and you’re still signed in to the app, you go straight to the app. The audit log records that as invalid_handoff too.

unauthorized

Either the edge or your app sent this. The bodies look the same, but every refusal the edge sends itself, any 4xx or 5xx, has an X-Forculus-From header, and an app’s answer never does, because the edge strips it from anything an app sends. The header is platform when the edge answered before your app got the request. It’s platform-after-dispatch when the edge answered after your app got it, so your app may have run it.

The edge sends it when it doesn’t know who is calling: no session on this app’s host, a session that has ended, or no bearer token at forculuslabs-apps.tech/<workspace>/<app>/.

It also sends it at that address for a path it won’t pass on: one with an encoded dot, slash, backslash or % in it (%2e, %2f, %5c, %25), which could change which app the path points at. Other escapes, like %20, reach the app as written. A path with %00 never reaches the edge: Cloudflare refuses it first, with a bare 400 Bad Request page and no X-Forculus-From header. forculus curl and the agent plugin refuse these paths before sending them.

A request from a page’s script, rather than a page load, gets a 401 instead of a redirect. Reload the page to sign in again. A script needs a bypass token.

The SDKs refuse a caller with the same unauthorized. If the audit log has an allow for the request, the edge let it through and the 401 is your app’s. Its log says why, such as couldn't fetch the identity keys or invalid identity token.

forculus curl tells you which. If your CLI session has ended, it fails with the session has ended error. If the edge refused the request for another reason, it says so:

This 401 came from Forculus, not the app. Your session is fine, so check the app name and the path. We refuse paths with an encoded dot, slash, backslash or %.

A 401 with no X-Forculus-From header is your app’s. forculus curl says so:

This 401 came from your app. Forculus marks its own refusals with X-Forculus-From, and this one has none.

A browser gets 403 for a POST, PUT, PATCH, DELETE or WebSocket sent from another site, even when it’s signed in. Send the request from the app’s own pages. A page on another site, another app included, that opens the app in a frame gets 403 too: only the app’s own pages can frame it.

A bypass token gets 403 for any app except the one it was made for.

A suspended workspace’s apps answer 403 too, with a page that says so. See App unavailable.

Terminal window
forculus curl acme/hello
not found
404 Not Found

No app answers at that address, nothing is deployed to it yet, its workspace was deleted, or the app isn’t shared with you. The edge answers all of these the same way, so nobody can tell which apps exist, not even members of the workspace. The audit log records a missing share as no_grant.

Check the name with forculus app list acme. If the app should be shared with you, ask an owner of the app, or an owner or admin of the workspace. They can check with forculus access acme/hello and share it. A new share works within 60 seconds.

Being a workspace owner or admin doesn’t let you into a private app. If you are one and the app exists, forculus curl says so, and tells you how to share it with yourself.

A bypass token that still works gets 403 in these cases, the same as for another app. A token that no longer works gets 401 wherever it’s sent, because it isn’t a credential any more. That covers tokens that were revoked, expired, or made for an app that was deleted.

In a browser, the person sees a page titled Nothing to open here, with the address they’re signed in as. Usually the app was shared with another of their addresses. They should sign out in the dashboard and open the link again with that address.

A page titled “Paused for the month” means a Free workspace has run out for this month. It served 200,000 requests, twice what Free includes, or used 50,000,000 ms of CPU time, 50,000,000 database rows read or 100,000 written. Every request the edge lets through counts, including public paths and webhook deliveries. Refused requests don’t. The app answers again when the next month starts, or as soon as an owner moves the workspace to a paid plan. See Limits.

The workspace is on Free, and we haven’t been able to measure its database rows for two hours, usually because Cloudflare’s analytics are down. Only apps with a database stop, and they answer again once we can measure. You don’t need to do anything. The audit log records it as rows_unmeasured.

The same page, recorded as cpu_unmeasured, means we haven’t been able to count the workspace’s CPU time for two hours. Worker apps that run code stop until we can, and static sites keep answering. This one is on our side too.

While either lasts, the dashboard says so at the top of every workspace page, forculus whoami and forculus workspace show say the apps are paused until we can count their usage again, and GET /v1/workspaces/<workspace> has unmeasured_stop set to rows_unmeasured or cpu_unmeasured.

The workspace’s databases together hold more than the plan allows: 1 GB on Free, 2 GB on Team or 10 GB on Business. Apps with a database answer only the workspace’s owners and admins, signed in as themselves, so they can delete rows through the app. Everyone else gets this page, including bypass tokens, public paths and webhooks, and the audit log records database_full. If the databases keep growing or reach 10% over, owners and admins get it too, recorded as database_hard.

To get back under, an owner or admin can run a DELETE, DROP TABLE or DROP INDEX with forculus db exec, which works while the workspace is over. They can also export data and remove it, or upgrade. The apps answer again within a minute of a measurement that finds the databases back under. See the database limit.

The workspace stores more than its plan allows: its sites’ files, its apps’ own files and its databases together, 1 GB on Free, 10 GB on Team or 100 GB on Business. Once any app keeps files of its own, every app with a place for files answers this page to everyone, owners and admins included, and the audit log records storage_full. Nothing new deploys until the workspace is back under.

Since the apps won’t answer you either, make room from the command line: forculus files rm deletes an app’s files, and forculus db exec its rows. Or move to a bigger plan, which takes effect at once. See Storage.

On Team or Business, your apps answer 402 with a page titled “Paused at the usage limit” when:

  • usage charges reach the usage limit, until the billing month ends
  • an invoice during the month wasn’t paid
  • you reach what the plan includes of CPU time or database rows before we’ve finished setting up billing for them. We finish it ourselves, usually within the hour

Every page of the dashboard says which. By default the usage limit is $0, so your apps pause when the plan’s allowances run out. An owner can raise or remove the usage limit on the Billing tab, and the apps answer again straight away. Paying the invoice does the same. See Limits.

The app runs in a container, or has key-value storage or a queue, and its workspace is on Free, which has none of those. It answers again once an owner moves the workspace to Team or Business.

The workspace has been suspended, or we’ve taken this one app down, after a copyright notice for example. Its owners can see which in the dashboard, or with forculus workspace show <workspace> and forculus app show <workspace>/<app>. Our email to them says why. forculus audit <workspace> --app <app> has the reason too.

You can still deploy an app that’s been taken down, and change its settings, so its owner can remove what the notice named. Nothing is served until we lift the takedown, and forculus deploy tells you so instead of printing the app’s URL.

You were signed in twice here, which can mean another app set a cookie it shouldn't have. Clear this site's cookies in your browser, then open the app again.

A page load shows this as a page titled Signed in twice. A request from a page’s script gets it as plain text, with a 401.

The request had two session cookies for this host, and the edge refuses it instead of picking one. A normal browser can’t get into this state, so something set a cookie it shouldn’t have. The edge can’t clear it for you, so clear this site’s cookies in your browser and open the app again.

On a public path with no verifier, one client can send an app only so many requests a minute. Slow down and retry. A page load gets a page titled Too many requests, and anything else gets the plain words.

Webhook providers and Workers share their sending addresses with other senders, and what those others send to the same path counts toward the same limit. If the sender signs its deliveries, give the path a verifier. Verified paths don’t have this limit.

We also limit how many names with no app behind them one client can look up in a minute. That count is separate from real apps, so someone sending to made-up names from the same address can’t hold up your deliveries. An app created in the last few minutes may count as a name with no app until our list of apps includes it.

Each real app has its own count too. A client only reaches it while the app’s workspace can’t serve requests, or we can’t look the app up, because then every request is a lookup. See Public paths and webhooks.

On a path with a verifier, a delivery that fails any check gets 401 unauthorized, so the provider’s log won’t say which check failed. The path’s row under Sharing, and forculus app public-paths acme/hello, show how the latest delivery went. People who can change the verifier also see which check it failed. Workspace owners and admins can see every delivery in the audit log, with the check after webhook_refused:

Check What to do
signature The secret on the verifier isn’t the one the provider signs with. Rotate it to the provider’s current one.
missing_header, malformed The sender isn’t the provider the verifier names, or for Standard Webhooks, sends the other set of header names. Replace the verifier.
stale The delivery was signed longer ago than the verifier allows. Raise How old a delivery may be, up to 10 minutes.
method Only POST reaches a verified path.
replayed A copy of a delivery already let through. Nothing to do.
unconfigured The verifier, its secret or the app’s public paths changed a moment ago. The provider’s retry is checked against what’s there now.
rate_limited A 429: the workspace had too many verified deliveries, or this path more than its share of them. Most providers retry. Redeliver GitHub’s from the webhook’s Recent Deliveries.
too_large A 413: the body is over 1 MiB, more than a verified path takes.

A delivery sent to the app’s own address, https://hello--acme.forculuslabs-apps.tech/hooks/stripe, doesn’t reach the path, because that address asks everyone to sign in. Give the provider the address the dashboard shows, https://forculuslabs-apps.tech/acme/hello/hooks/stripe.

A delivery was let through, but the provider shows 401

Section titled “A delivery was let through, but the provider shows 401”

The path’s row says let through, and the log shows an allow with the provider as the caller:

20:58:02 ALLOW webhook:stripe hello POST /acme/hello/hooks/stripe -

In that case the 401 came from your app. Most often, a check for people, like app.use(requireUser()) or forculus.Middleware, runs before the webhook route and refuses the delivery’s token as invalid identity token. Mount the webhook route before it. Each SDK’s page shows where. If the route calls requireWebhook and logs not a webhook this route takes, it names a different provider from the path’s verifier.

We don’t keep a delivery your app didn’t answer with a 2xx, so the provider’s retry gets through once the app is fixed.

The edge let the request through and your app answered with a 500 of its own, so it has no X-Forculus-From header. forculus logs acme/hello shows what it wrote. See Logs.

For a container app, don’t add --level error: everything a container writes reads as info, tracebacks too. Levels in a container app explains why. --search Traceback or --search Error finds the exception instead.

A Worker app threw an exception it didn’t catch, so no answer came back. A page load gets a page titled The app didn’t answer, and anything else gets bad gateway. Both have X-Forculus-From: platform-after-dispatch, since the app may have done some of the work first. forculus curl says so on stderr:

502 Bad Gateway
This 502 came from Forculus after the app got the request, so the app may have run it. We couldn't get an answer from the app. If the request changes something, check `forculus logs` to see whether it ran before you send it again.

Its log has the exception’s message:

Terminal window
forculus logs acme/hello --level error
10:04:31 ERROR v3 GET /pay Cannot read properties of undefined (reading 'id')

The line says which version wrote it. If your latest deploy broke it, forculus rollback acme/hello 2 puts the previous one back while you fix it. See Logs.

The edge losing a Worker app’s request on the way gets the same answer, with nothing in the app’s log.

For a container app, the container went away before it answered. It crashed, ran out of memory, or was stopped mid-request. The answer is The app didn't answer., with no X-Forculus-From header. If a container stops during a GET or HEAD, for example because a new version replaced it, we send the request once to the next container. A request that never reached the app gets the 503 below. The log has what the container printed before it stopped:

Terminal window
forculus logs acme/hello

Cloudflare stopped the request because it used more CPU time or memory than a request is allowed. It’s the app’s error, so there’s no X-Forculus-From header, and the app may have done some of the work first. Cloudflare can stop a request at about its plan’s CPU time, so a request that only sometimes gets this is still over it. Keep each request under its plan’s CPU time. Move heavier work to a container app, or split it across requests. See Limits.

A Worker app gets this answer too when a request makes more subrequests than it may and the code doesn’t catch the Too many subrequests error. Its log has the error.

The edge couldn’t check access, couldn’t check a webhook delivery’s signature in time, or couldn’t record the request in the audit log. In each case it refuses the request instead of letting it through. Try again shortly. A page load gets a page titled This can’t open right now, and anything else gets the plain words.

A container app answers The app is starting. Try again in a few seconds. with a Retry-After when its container isn’t up within 20 seconds. A browser loading a page waits 3 seconds instead, then gets a short page saying the same thing, which reloads itself every 5 seconds. The container keeps starting, so the next try usually works. Right after an app’s first deploy, Cloudflare can take several minutes to get its container ready, and every request gets this answer until then.

If it keeps happening, check that the app listens on 0.0.0.0 and on the port in PORT. A server listening on 127.0.0.1, or on a port of its own, can’t be reached, and looks the same as one that’s still starting. forculus logs shows what it printed as it started. A start that keeps failing gets the answer below instead.

A container app’s container keeps failing to start. It has exited 3 starts in a row before it listened, or its starts have kept failing for 10 minutes. Failures with a quiet spell of more than 5 minutes between them aren’t added up, and a new deploy counts afresh. With more than one container, each one counts on its own. The app answers The app couldn't start. with a Retry-After of 30 seconds, and a browser loading a page gets a page saying so, which reloads itself every 30 seconds. The answer never says why, so nobody opening the app sees your image or its errors.

To see why, read the platform’s lines along with the app’s:

Terminal window
forculus logs acme/hello --platform

Our lines say how each start failed, and the app’s own lines show what the container printed before it stopped. Common causes are a missing environment variable, a crash on startup, or a server that listens on 127.0.0.1 or a port of its own instead of 0.0.0.0 and PORT.

Every request still tries to start the container. Once a deploy or a change to the app’s environment variables fixes it, the app answers again with nothing else to do.

The SDK couldn’t verify the token. Log the error’s message, which says why. forculus logs acme/hello shows what the app logged.

Message What to do
FORCULUS_APP_ID, FORCULUS_IDENTITY_ISSUER and FORCULUS_JWKS_URL must be set The app isn’t running on Forculus or under forculus dev, or it reads its environment in a way that loses them. On Workers, pass the handler’s env to the SDK.
FORCULUS_IDENTITY_ISSUER and FORCULUS_JWKS_URL must use https, except under forculus dev (FORCULUS_DEV=1, on this machine) See Every request gets 401: must use https.
no identity token The request didn’t come through the edge. Locally, open http://127.0.0.1:8787, not your app’s own port.
invalid identity token The token failed a check. Make sure the app reads the FORCULUS_APP_ID we set, not a value of its own.
not a webhook this route takes A verified path’s delivery reached a route that names another provider. Name the provider the path’s verifier checks.
no provider named The route calls requireWebhook with no provider, so it refuses everything. Name the one that sends to it.
couldn't fetch the identity keys The app couldn’t reach FORCULUS_JWKS_URL. In a Rust container, see TLS in a container.

A request that carries only X-Forculus-Email is refused, because the SDKs read only the signed token. See SDK overview.

In Go, the package-level functions read the variables once. If they were missing on the first request, restart the process.

On a webhook route, invalid identity token can also mean the token is a person’s, not a webhook’s. The request came in through a signed-in session or a bypass token instead of a verified public path, or through forculus dev as anyone but --as webhook.

hello null

The caller is a bypass token, which has no email. Use user.sub when there is no email. See What you get back.

Refused: apps can't send requests to Forculus itself.

Your app sent a request to the dashboard, the API, the edge or another Forculus app, and apps can’t reach those. The only one of our addresses an app can fetch is the key set at FORCULUS_JWKS_URL.

Refused: example.org isn't on this app's list of hosts it can send requests to.

The app has an outbound list turned on, and the host isn’t on it. A workspace owner or admin adds it with forculus egress allow. See Sending requests out.

Fetch the Go SDK as forculuslabs.tech/sdk, not by its GitHub path. Without the module proxy, as with GOPROXY=direct, that takes Go 1.25 or newer. See Go.

Creating a signing key for this session ...
error: Couldn't listen on 127.0.0.1:8787: Address already in use (os error 48).

Something else holds port 8787. Stop it, or pass --port.

Nothing is listening on 127.0.0.1:8788 yet (Connection refused (os error 61)).

Your app hasn’t started, crashed, or listens on another port. Make it listen on PORT, or pass --app-port to match the port it uses.

With --docker you may see this instead while the container starts:

127.0.0.1:8788 took the connection but didn't answer. Is the app still starting?

Wait a few seconds and reload. If it doesn’t go away, check that the app listens on 0.0.0.0 and on the port in PORT, not on 127.0.0.1.

forculus dev answers only requests for localhost, 127.0.0.1, [::1] and names ending in .localhost. Open http://127.0.0.1:8787, not another name that points at your machine.

A route that calls requireWebhook takes only webhooks. Switch to the webhook persona, with the provider the route names and a pattern that covers its path: forculus dev --as webhook --provider github --pattern /hooks/github, or the buttons at http://127.0.0.1:8787/.forculus. A path the pattern doesn’t cover gets 401 before your app sees it. See Personas.

FORCULUS_IDENTITY_ISSUER and FORCULUS_JWKS_URL must use https, except under forculus dev (FORCULUS_DEV=1, on this machine)

Your app’s SDK is version 0.3.0 or newer, and it didn’t get FORCULUS_DEV=1. Update the CLI, since forculus dev sets it from 0.1.11. If you start the app yourself, or a tool between them drops the environment, pass FORCULUS_DEV=1 on. See FORCULUS_DEV.

On a deployed app it means the app sets its own http issuer or key set. Use the ones we set.

forculus dev --docker and a project that’s only a Dockerfile need Docker. Start Docker or OrbStack, or name another docker with FORCULUS_DOCKER. To run the app without Docker, give the command: forculus dev -- <command>.

See Local development.

Nobody is signed in to the forculus CLI on this machine. Ask the person to run `forculus login` in a terminal. Don't run it yourself, because signing in is theirs to do. This server acts as whoever the CLI is signed in as, and has no credentials of its own.

Run forculus login in a terminal. The agent uses the CLI’s session, and picks it up on its next call.

The agent won’t deploy from your home directory

Section titled “The agent won’t deploy from your home directory”
This session runs in your home directory, a directory above it, or the filesystem root, which hold too much to deploy from. Start it in the project's own directory.

Start the agent in the project’s directory, or pass --roots. See Where a deploy may read from.

No directory is open for deploys. This client started the server in the plugin's own directory, ..., and didn't say which project the session is in. Ask the person to set FORCULUS_MCP_ROOTS to the project's directory in the environment their client starts from, for example `export FORCULUS_MCP_ROOTS="$PWD"` in the project before starting it, or to add `--roots <project directory>` to the server's arguments.

Start the client from the project’s directory. Codex passes that on, so that’s all it needs. For a client that doesn’t, run export FORCULUS_MCP_ROOTS="$PWD" there before you start it. See Where a deploy may read from.

Update the CLI. Since 0.1.1, deploy_app answers within 45 seconds and a longer deploy carries on while the agent waits for it with deploy_status. See A deploy that takes a while.

An AI client can’t connect to an MCP server app

Section titled “An AI client can’t connect to an MCP server app”
  • If the approval page says the app doesn't take AI clients, AI clients aren’t turned on for it. Someone who can share it runs forculus app mcp acme/tools on.
  • If the approval page says the app isn’t shared with you, you’re signed in as someone without access. Sign in as someone it’s shared with.
  • Use the app’s address on forculuslabs-apps.tech, not a custom domain.
  • Cursor has to be a version that signs in through an address on your computer, not only its cursor:// one.

See MCP servers behind sign-in.

If none of this helps, check what the edge saw. forculus audit acme --app hello lists every request it allowed or refused, and why. The workspace’s owners and admins can run it, and an app’s owners for their app. forculus watch acme/hello shows requests as they happen. See Audit log.

Then check what your app saw. forculus logs acme/hello shows what it printed and the exceptions it threw. See Logs.