What a database branch actually is, and when to delete it
A branch is a full clone, not a live replica. What that means for freshness, for the parent relationship, and for the four states you will see.
An Infrai database branch is a complete Postgres database created as a clone of another one. POST /v1/db/branch/create takes a project_id, a branch_name and an optional from_branch, and what you get back is a separate database with its own identifier — not a view, not a schema, not a replica that follows its source.
Everything surprising about branches follows from that one sentence, so it’s worth unpacking before you design around them.
The parent relationship is a fact about the past
curl -sS -X POST "https://api.infrai.cc/v1/db/branch/create" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"project_id": "dbp_7Uu2kQxWvR4mBn8d", "branch_name": "experiment-pricing", "from_branch": "main"}'
{
"ok": true,
"data": {
"db_id": "db_2fVc8nRqLmT4xBzY",
"branch_name": "experiment-pricing",
"parent": "main",
"created_at": "2026-09-21T03:12:40Z",
"state": "ready"
}
}
parent: "main" records where the data came from. It does not create an ongoing link. Writes to main after 03:12:40 never appear in this branch, and writes to this branch never appear in main — there’s no merge, no sync, no conflict resolution, because these are two independent databases that happened to start identical.
That’s why “my branch is missing yesterday’s orders” isn’t a bug. It’s a branch created the day before.
The four states you’ll see
curl -sS "https://api.infrai.cc/v1/db/branch/get/dbp_7Uu2kQxWvR4mBn8d?branch_name=experiment-pricing" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
state | What it means | What to do |
|---|---|---|
creating | the clone is in progress | wait; don’t hand out the connection yet |
ready | usable | connect |
deleting | teardown in progress | don’t recreate the same name yet |
deleted | gone | the name is free again |
Code that assumes ready immediately after create is the most common integration mistake here. Poll until the state says so — a connection attempt against a creating branch is a confusing failure rather than an informative one.
import os
import time
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
def create_branch(project_id: str, name: str, from_branch: str = "main",
timeout_seconds: int = 600) -> dict:
"""Create, then wait for 'ready'. Returning a branch in 'creating' to the
caller is how a CI job gets a connection error that looks like a network
problem and is really a race with provisioning."""
resp = SESSION.post(
f"{API}/v1/db/branch/create",
json={"project_id": project_id, "branch_name": name, "from_branch": from_branch},
timeout=90,
)
body = resp.json()
if not body.get("ok"):
raise RuntimeError(body["error"]["code"])
branch = body["data"]
deadline = time.monotonic() + timeout_seconds
while branch.get("state") != "ready" and time.monotonic() < deadline:
time.sleep(3)
check = SESSION.get(f"{API}/v1/db/branch/get/{project_id}",
params={"branch_name": name}, timeout=25)
check.raise_for_status()
branch = check.json()["data"]
if branch.get("state") != "ready":
raise TimeoutError(f"{name} is {branch.get('state')}")
return branch
if __name__ == "__main__":
print(create_branch(os.environ["INFRAI_DB_PROJECT_ID"], "experiment-pricing"))
What a branch is not
It isn’t a backup. A backup is a snapshot you can restore from — POST /v1/db/snapshot/create — and it survives the deletion of what it captured. A branch is a live database that costs you rent and can be dropped.
It isn’t a read replica. There’s no replication, so it won’t reduce load on your primary and it won’t serve fresher data than the moment it was cloned.
And it isn’t free to keep.
When to delete it
The rule that works: a branch should live no longer than the reason it was created.
| Reason for the branch | Delete when |
|---|---|
| Pull request environment | the PR closes, or the nightly sweep catches it |
| Trying a migration | the migration is proven, either way |
| Debugging a production issue | the issue is understood |
| Load testing | the test finishes |
| ”Staging” | never — but budget it as infrastructure, not as a branch |
That last row is the honest exception. A long-lived branch is a permanent database wearing a temporary name, and the risk is that it’s managed like a temporary thing — no backups, no monitoring, no owner — while being depended on like a permanent one. If you need staging, make it a project.
Cost follows lifetime
Creating a branch is a one-time fee, live in the billing block of GET /v1/discovery/db.branch.create. Keeping one accrues standing rent by occupied gigabyte over time, the way stored objects do, so the cost of a branch is essentially the cost of how long you left it there.
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
That read (free, verified 2026-09-21) shows what actually accrued, and GET /v1/account/balance adds runway_days at your current burn. Platform rates drift downward as vendor contracts improve, so both of those beat any figure written into a document — including this one.
One operational fact to know rather than discover: metered resources belong to an account with a healthy balance. If a balance sits below the minimum past the grace window, metered resources are reclaimed so the cost stops accruing — which is another reason a preview branch is the wrong place to keep something you can’t regenerate.
The limitation, and what’s next door
There’s no merge. You cannot take the schema change you made in a branch and apply it to the parent through this API; that’s what your migration tool is for, and the branch was there to prove the migration works rather than to carry it. If you expected Git-like semantics, that expectation is the thing to adjust — the name is an analogy, not a promise.
What is genuinely convenient is that the rest of the environment the branch belongs to is on the same key: a bucket for its fixtures via POST /v1/storage/bucket/create, a queue its worker drains via POST /v1/queue/create, the sweep that removes all three on POST /v1/cron/create, and one GET /v1/account/usage covering the lot. Neon and Supabase both offer richer branching workflows if the database is the whole purchase; the argument here is that it usually isn’t.