How it's built, and what it can take.
The detail IT departments and returning officers ask us for. Every number here was measured on the build we ship, running on a laptop you could buy off a shelf.
The headline
1,000 people at once, on one laptop, with nothing failing. We ran 1,000 simulated attendees for seven minutes straight, each holding a live connection the way a phone in someone's hand does. All 1,000 connections came up and stayed up, the server pushed 718,038 live updates back out to them, around 1,700 every second without a break, and not one attendee action failed.
We paced each attendee at one action every 15 to 45 seconds, since that's what a hall full of people does. Nobody taps a phone non-stop for seven minutes. It also puts the strain where a real event puts it, on pushing every change back out to a thousand phones, instead of hammering one endpoint to run up a big number.
What one laptop handled in seven minutes
With 1,000 attendees connected the whole time.
Joins, ballots, revotes, questions, upvotes, survey answers, check-ins, downloads. We paced each attendee at one action every 15 to 45 seconds, because that's what a thousand people in a hall actually do. Nobody taps a phone non-stop for seven minutes. None of the 15,774 failed.
How we tested it
We simulated whole events rather than one endpoint. Attendees joined by code, kept a connection open, browsed the schedule, cast ranked ballots and changed their minds, chatted, asked and upvoted questions, answered surveys, checked in and downloaded documents. Organisers worked the dashboard, ran polls from creation through to reveal, sent notifications, moderated questions, ran breakouts and pulled exports. All of it at once, the way it happens on the day.
| Server | One process, FastAPI with SQLite in WAL mode, on a Windows 11 laptop with an Intel i7-13700H |
|---|---|
| Load generator | A second PC on the same network, running Locust 2.45 across 10 workers |
| Generator headroom | 34.6% CPU on average, 75.7% at its busiest |
| Server memory | About 450 MB at peak |
The second machine matters. Run the load generator on the same box as the server and the two fight over one CPU, so what you end up publishing is the limit of your test rig. Ours had its own hardware and never went past 76% CPU, so the timings are the server's, not ours.
One thousand phones, all connected at once
One dot per attendee, each holding a live connection the same way a phone in the room does.
1,000 of 1,000 connected. All 1,000 WebSocket connections were established and held for the whole session, with a median connect time of 89 ms.
At 500 attendees it barely notices
Run the same test with 500 in the room and every request comes back clean, most of them inside 20 to 300 milliseconds. At that speed nobody thinks about the software at all. They tap, and the next screen is already there.
What that costs the machine
Memory in use at peak, on the laptop that ran the test.
- ~450 MBConclave at peak
- ~15.2 GBStill free on the test laptop
Conclave at peak. The whole platform at full load: API, database, real-time fan-out and the attendee app, in one process.
Where the ceiling is
- 1,000 at once per server, verified. That's the number we'll put in writing, and what the Events licence is built around.
- Conclave scales up, not out. Connection state lives in the server process, which is the price of not leaning on outside services. A bigger machine lifts the ceiling. Adding more server processes doesn't.
- Give the host Ethernet or proper venue Wi-Fi. A phone hotspot runs out of connections long before the server does. In practice that's what limits a venue, not the software.
One host, no moving parts
Conclave is one process on one machine. A Python backend (FastAPI on Uvicorn) serves the API, the live feed and the attendee app from a single port, backed by one SQLite file in WAL mode. No message broker, no cache server, no second machine to keep in step.
The attendee app has no build step: plain ES modules and system fonts, served from your host. Nothing on the page comes from a CDN, because at your venue there may be no route to one. The election code is separate again, plain Python with no framework underneath it, which is what makes it easy to test on its own and to follow during an audit.
Correctness, and how you can check it
The build ships with 124 automated tests and all 124 pass. We re-run them after any performance work, and the linter stays clean. Whatever we change, ballots stay append-only, tie-breaks stay reproducible from a published rule, and audit exports stay byte-for-byte stable.
- IRV is checked against the Tennessee state capital example from the ranked-choice literature, asserting the whole round-by-round elimination path rather than just the winner.
- STV is checked against a hand-worked fractional Gregory example, with the Droop quota, surplus transfers and exhaustion, then cross-checked against IRV for the single-seat case where the two must agree.
- Exports are byte-for-byte reproducible. The whole auditability claim rests on that, so it gets its own test.
- Tie-breaks are seeded and deterministic. The same audit seed gives the same result every time, so an auditor replaying your tally gets exactly what you got.
- The rest boot the real server and drive it over HTTP and WebSockets: poll lifecycle, quorum, moderation, surveys, Q&A, breakouts, minutes, badges, ballot papers and backups.
Ballots and the audit trail
- Append-only. Ballots are never edited in place. A revote is a new record.
- Every round exports as JSON or CSV, with each elimination, transfer and count, so a scrutineer can replay the lot.
- The vote log holds the results summary and every timestamped ballot, with voter names attached unless the poll was set anonymous, in which case they are never recorded against the ballot.
- Backups run themselves. Timestamped copies of the database go to a folder you pick while the event is live.
Security
- Sensitive columns are encrypted at rest. Organiser codes and attendee display names sit in the database as ciphertext, so a copied file is not a readable attendee list.
- Session tokens are signed and scoped to one role and one event. No accounts, no passwords to leak.
- Nothing calls out. No telemetry, no analytics, no licence check phoning home, no third-party fonts or scripts. It works air-gapped because nothing in it expects the internet.
- It runs unprivileged. In Docker that means a non-root user, no-new-privileges, and one port open.
- Moderation is built in. Mute or remove someone, revoke a leaked code, issue tokens that expire.
When the Wi-Fi drops
A ballot is written to the phone's own storage before the network call, so a connection that dies mid-vote cannot take the vote with it. Resubmissions carry an id that makes them idempotent, so replaying the queue on reconnect never double-counts anyone.
Worth knowing: caching the app shell for a cold start needs HTTPS. On a plain http://192.168.x.x address the browser turns the service worker off, though ballot durability still works. If you want the full offline behaviour we'll help you put a certificate in front of the host.