Skip to main content
Think of a pharmacy counter in a busy market in Chennai. The shop is open, the lights are on, the pharmacist is standing there. But the billing system has lost its connection, so nothing can be sold. Ask “is the shop open?” and the answer is yes. Ask “can I buy my medicine here right now?” and the answer is no. Both answers are true. They answer different questions. Servers have the same two questions, and mixing them up is one of the most common mistakes I see in deployments.

The problem: alive is not the same as useful

A hosting platform checks your server regularly. If the check fails, it restarts the process or stops sending it traffic. Most teams point that check at one route that returns “OK” if the process answers at all. For Parmana that is not enough. Every approved action needs a database: the request is recorded, a signed Execution Intent is stored before release, the authorization’s nonce is consumed so it can be used only once, and the caller audit trail is written. A Parmana process whose database is unreachable is up, but every real request will fail. If your health check says “OK”, the platform keeps sending it traffic, and an agent asking to refund ₹9,500 gets an error instead of a decision. The opposite mistake is just as bad. If the liveness check also tests the database, a short database blip makes the platform restart every instance at once. Restarting does not fix a database. It only adds a cold start to the outage.

The idea: two routes, two questions

Parmana has two routes, deliberately different, both open without a caller key so a platform can call them:
  • GET /health asks: is this process running? It always answers 200 with status UP and checks nothing else. If the process can answer HTTP, it answers this. Use it for restart decisions.
  • GET /ready asks: can this process serve real traffic now? When storage is Postgres backed, it runs SELECT 1 against the database. When Execution Intents are enforced, which in production they always are, it also checks that the execution_intents table exists. If either fails, it answers 503 with status NOT_READY and a reason. Use it for traffic decisions.
The table check is there because of a real deployment risk. A new version of the server needs that table before it can release anything. A deploy that skipped the migration would pass a plain database ping, and then refuse every execution with 503 EXECUTION_INTENT_UNAVAILABLE. The readiness check catches it first and the reason tells you which migration to apply.

One more field: is authentication off?

The readiness response also carries an authDisabled field, with a warning when it is true. Setting PARMANA_AUTH_DISABLED=true is meant only for local development, and it is exactly the kind of line someone copies from an example file without reading. A warning in a startup log is easy to miss. A field on a route your monitoring already checks every 30 seconds is easy to alert on. The production runbook says to alert when /ready is not 200 or when authDisabled is true.

The bug the readiness check found in itself

The test for /ready runs three cases: test mode, memory storage, and a database URL pointed at an address that cannot be reached. On 2026-09-16 that third case kept reporting READY. Two real bugs were behind it. The Postgres pool had no connection timeout, so a connection to a dead address could hang instead of failing. And a change made that same day opened the shared database pool when a module was first loaded, so the pool was already connected to the working database before the test switched the URL. The test looked correct and tested nothing. Both were fixed: a five second connection timeout, and building that repository lazily, only on first use. Now the third case reports NOT_READY with the real connection error. I keep this story because it is a good reminder that a readiness check needs its own test against a database that is really down.

What it does not do

  • It checks the database, not everything. /ready does not test that the signing key works, that a connector is reachable, or that your policies are approved. Signing is checked separately before each release, and an unreachable connector shows up as 502 EXECUTION_OUTCOME_UNKNOWN.
  • Memory storage is always ready. With PARMANA_STORAGE=memory, or in test mode, there is nothing to probe, so /ready says READY without any check.
  • It is a snapshot. A database that drops a second after the check passes still fails the next request. Readiness lowers the chance of sending traffic to a broken instance; it cannot remove it.
  • Where it drives traffic depends on the host. On Fly.io or any Docker platform, the runbook points the platform’s own health check at /ready. My production runs as a Vercel function, and there the runbook uses /ready as a check after each deploy and as a target for your own monitoring, not as a switch the platform routes on.

The takeaway

“Is it up?” and “can it do its job?” are different questions. Ask both, separately, and wire each answer to the action it should drive: restart on one, route on the other, alert on both. Read more: Production deployment runbook, Chapter 19: Health and Readiness.