Admission and limits
Admission and limits
Optional and off by default — concurrency caps, size ceilings, device and build quotas, how a slot is claimed as a row, and the acceptance run that proves access still works.
Optional, and off by default. The caps on this page only bind while
enable_credits is 1, except the device cap and the build cap, which bind
whenever the setting holds a number.
Who this is for. Somebody running BenchPress for a team, who needs one
person's benches not to fill the host.
Before you start. This describes running BenchPress for other people.
None of it is required to deploy a bench on your own machine. Read
Credits and billing first — it owns the
switch that most of this hangs off.
The caps
| Cap | Setting | On this host | Binds when |
|---|---|---|---|
| Concurrent instances, never purchased | max_concurrent_free |
2 | credits on |
| Concurrent instances, has purchased | max_concurrent_paid |
5 | credits on |
| Concurrent instances, credits off | max_concurrent_uncredited |
0 = unlimited | credits off |
| Largest size on a free account | max_size_free |
empty = no ceiling | credits on |
| Sites per instance | Instance Size.max_sites |
3 / 5 / 10 | always |
| VPN devices per account | max_devices |
5 | always |
| Custom image builds per day | max_builds_per_day |
3 | always |
max_concurrent_uncredited reads backwards from how most people expect.
It applies only while credits are off, and 0 there means unlimited, not
zero. Switching credits on does not engage it. The cap becomes
max_concurrent_free, or max_concurrent_paid once the account holds any
Purchase ledger row.
The switch from free to paid reads the row rather than the balance, because a
refund clears the balance and the account has still plainly paid.
Steps
-
Open
/app/credit-settingsin Desk.Screenshot: The Credit Settings document in Frappe Desk, Free but Capped section. Max Concurrent (Free) is 2, Max Concurrent (Paid) is 5, Max Concurrent (Credits Off) is 0, Max Devices is 5, Max Builds per Day is 3, and Max Size (Free) is empty. Every field's help text ends with the same sentence: 0 equals unlimited.
The Free but Capped section holds five of the seven caps:
max_concurrent_free2,max_concurrent_paid5,
max_concurrent_uncredited0,max_devices5 andmax_builds_per_day3.
max_size_freeis empty. -
Set the per-size limits at
/app/instance-size. Three sizes ship:Size Memory CPU Max sites Credits per hour Price multiplier Small (default) 1g 1 3 1 1.0 Medium 2g 2 5 2 1.0 Large 4g 4 10 4 1.0 Every size ships with
include_code_serveron and withdisk_limit,
pids_limit,iops_limitandbps_limitat0, which means unset. -
Save. Caps are read per request, so a change binds on the next call. There
is nothing to restart.
Verify
Ask the app what one user's ceiling is, rather than reading the settings:
bench --site <site> execute benchpress.credits.guard.concurrency_limit \
--kwargs "{'user':'[email protected]'}"
0 means unlimited. Any other number is what that account may hold at once.
How a slot is actually taken
This is the part that surprises people reading the code, and it explains the
refusal messages.
Counting running instances and comparing cannot refuse anything. Two requests
that arrive together read the same count and both pass. A new bench row is
written as Draft for the two minutes a deploy takes, so an in-flight deploy
is invisible to anything counting deploys.
So a slot is a row, not a count. Bench Admission is named after the
bench, which puts the claim on a primary key, and every read and write happens
under a row lock on the caller's Credit Account. The loser of a race waits
there and then reads what the winner wrote.
Three consequences an operator will meet:
- A hold is not a charge. It moves no balance and writes no ledger row.
The charge happens when the bench reachesRunning, in the same locked
transaction that ends the hold. A user who reads their statement during a
deploy sees nothing yet, and their spendable balance is still lower. - A redeploy, a restart and a retry are free of the cap. The bench already
holds its slot, so the claim returns without refusing. The cap forbids new
instances, not touching existing ones. - A slot can leak if a worker is killed between the claim and the charge.
benchpress.credits.admission_repair.reconcile_admissionsruns every five
minutes precisely because a leaked slot is a lockout for somebody at their
cap.
The refusals, and what each one means
Every cap refuses by name. These are the messages a user will quote at you.
| Message | Cap | What to do |
|---|---|---|
You have N instances running, the most your plan allows |
concurrency | They stop one, or buy credits to move to the paid cap |
The <size> size is not available on a free account |
max_size_free |
They deploy smaller, or buy credits |
This instance already has the N sites its size allows |
Instance Size.max_sites |
They redeploy the lab at a larger size |
You already have N devices, the most allowed |
max_devices |
They remove a machine they no longer use |
You have used today's N custom image builds |
max_builds_per_day |
They wait, or deploy a lab whose recipe is already built |
Not enough credits: this needs X and Y are available |
balance | They pick a shorter plan, or buy credits |
This account is suspended, so nothing new can be started |
is_suspended |
An operator clears the flag |
The build cap counts against the lab's owner, not the caller, because that
is the account the build's ledger row is written to. It rides only on the
explicit build action. A build that the deploy path performs is a cache miss
the user did not ask for, and the credit charge is the control there.
The acceptance run
Repeat this after any change to the deploy pipeline, the lab image, or the
access surface. It answers one question: can a user reach the lab they were
just told is ready?
Run it against a lab deployed from a template, from a machine with the tunnel
up. Record the result of every step. A step that was not exercised is reported
as not exercised, never as a pass.
Throughout, <wg_ip> is the bench's WireGuard address, <user> its SSH
username and <site> its site name. All three are on the Connection
details card.
-
The deploy is genuinely green. All eleven steps read
successin the
stepper, and the raw log holds no[warn]line. A build that ran inside
the deploy has its own tab, and a warning there is a real finding even when
the deploy is green. -
The site loads and the admin password works.
curl -s -o /dev/null -w '%{http_code}\n' http://<wg_ip>:8000/ curl -s -X POST http://<wg_ip>:8000/api/method/login -d "usr=Administrator&pwd=<admin_password>"Expect
200and{"message":"Logged In"}. -
The IDE opens, with its password in hand. Open VS Code opens
http://<wg_ip>:8080/and a password dialog appears on the lab page. The
password is never in the URL — a query parameter would land in browser
history, in the container's access log, and in every proxy between.curl -s -o /dev/null -w '%{http_code}\n' http://<wg_ip>:8080/ # 302 -
The integrated terminal is a real bench shell. Inside code-server:
whoami # the lab's ssh username, not frappe pwd # /home/<user>/frappe-bench bench --site <site> list-apps bench versionAll four must work with no
sudoand no PATH fix. -
SSH does the same from your own machine.
ssh <user>@<wg_ip>A one-shot command is not a login shell. Debian's
.bashrcreturns
early for non-interactive shells, before the bench block, so
ssh <user>@<wg_ip> "bench --site <site> list-apps"starts in the home
directory and reports Command not being executed in bench directory. Name
the directory when scripting:ssh <user>@<wg_ip> "cd frappe-bench && bench --site <site> list-apps" -
Off the tunnel, the UI is honest. Disconnect and return to the browser
tab without reloading. Open site and Open VS Code are disabled and
say why. Each site row readsUnreachablewith its own reason, distinct
from a stopped container's. Reconnecting re-enables them on the next focus
of the tab. Nothing spins forever. -
With the bench stopped, nothing offers a dead address. Open VS Code
is gone, the site row readsInactivewith its Open button disabled, and
the primary action is Deploy. -
A failed deploy names its own cause. Point a Lab App row at a branch
that does not exist and deploy. The banner names the image build
failure, not the deploy's log tail, and its button opens the Build log
tab.
Two known intermittents, neither a defect of the step they break.
wg-quick up wg0 can report wg0 already exists at step 5 when the
container's entrypoint won the race — retry the deploy. codeload.github.com
rate-limits in bursts, and a 429 during an image build is retryable.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| A user is refused at a cap they are not at | A slot leaked from a killed worker | Wait for reconcile_admissions, or run it by hand |
| Raising a cap changed nothing | The account is refused on balance, not concurrency | Read the exact message. They name different numbers |
max_concurrent_uncredited seems to do nothing |
Credits are on, and it only applies while they are off | Change max_concurrent_free instead |
| A user cannot add a device on a fresh install | max_devices binds even with credits off |
Raise it, or have them remove one |
| Every build is refused after three | max_builds_per_day counts against the lab owner |
Wait for tomorrow, or deploy a lab already built |
| The size picker hides sizes | max_size_free is set and the account has never purchased |
Clear the ceiling, or let them buy credits |
Reference
| Setting | Value here | Scope |
|---|---|---|
max_concurrent_free |
2 | credits on, never purchased |
max_concurrent_paid |
5 | credits on, has purchased |
max_concurrent_uncredited |
0 (unlimited) | credits off |
max_size_free |
empty | credits on |
max_devices |
5 | always |
max_builds_per_day |
3 | always |
Instance Size.max_sites |
3 / 5 / 10 | always |
| Job | Schedule | Purpose |
|---|---|---|
benchpress.credits.admission_repair.reconcile_admissions |
every 5 minutes | free slots leaked by a killed worker |
benchpress.credits.sweep.enforce_limits |
every 5 minutes | check balances, never the clock |
Lock order across the app is fixed: Bench Instance, then Credit Account,
then Bench Admission. Nothing in admission locks an instance, so admission
cannot close that cycle.
Related
- Credits and billing — the switch these caps hang off.
- Users and roles — who is exempt from what.
- Troubleshooting — the same refusals, indexed for the user who hit one.