Home · About · Services · Industries · Projects · Products · Process · Blog · FAQ · Contact
Online exam portal design: building one that holds up on exam day How to design an online exam portal that survives exam day: start-time spikes, server-side timers, autosave that copes with bad Wi-Fi and realistic load tests.
At 9:59 the portal is quiet. At 10:00 every candidate presses Start within the same few seconds, each browser asks for a question paper, and each one begins saving answers. Most exam portals that fail do so in that first minute, or twenty minutes later when a candidate loses connection and the timer, the saved answers and the support desk all disagree.
The short answer: an online exam portal holds up when the server owns the clock, every answer is saved as a small idempotent write, the question paper is prepared before the start time, and the system has been load tested with the real start-time spike. Proctoring matters far less than these basics.
We have built an admission test portal on Django, PostgreSQL and React behind NGINX on AWS, and an assessment portal for both enrolled students and external candidates. These are the design decisions that matter.
Understand the load shape before choosing servers Exam traffic is close to zero for weeks, then arrives all at once, at a known time and from a known number of candidates. That makes it easier to plan for than ordinary web traffic.
Work out three figures for the largest sitting, say 2,000 candidates. If all of them start within ten seconds, the paper endpoint sees around 200 requests per second at the peak. If each browser autosaves every 30 seconds, steady load is roughly 67 writes per second for the whole exam. And at the end, every remaining attempt is submitted in the same minute. None of these numbers is large, but each will break a design that does too much work per request.
Start spike: login, attempt creation and paper fetch for everyone at once. Steady state: small autosave writes and occasional page loads. End spike: final submissions, plus anyone who reloads to check they were recorded. Prepare the paper before the clock starts The most expensive thing a portal can do at 10:00 is assemble question papers: drawing from pools, shuffling options and rendering for 2,000 people in the same ten seconds.
Instead, open a lobby 20 to 30 minutes before the start. Candidates log in and check their connection while load is spread out. When an attempt is created in the lobby, generate that candidate's question order and option order and store it with the attempt. At the start time, the paper endpoint only has to return a prepared payload, which is a cheap read that can be cached. Storing the order also settles disputes, because you can show exactly what each candidate saw.
Keep answer keys out of anything sent to the browser. Questions go to the client; correct options stay on the server and are only used during marking.
Let the server own the timer A countdown that lives only in JavaScript can be edited, or wrong after a laptop sleeps. Store the start time and the deadline on the attempt record when the candidate begins. The browser displays a countdown calculated from the server's deadline and the server's current time, and it refreshes both on every autosave response.
The server rejects writes after the deadline plus a short grace period, such as 30 seconds. Extra time for candidates with approved accommodations becomes a change to their deadline, not a separate code path. If an invigilator extends time after a power cut, that is an audited update to the deadline with a reason attached.
Make autosave small, idempotent and ordered Saving the whole answer sheet on every change invites race conditions. Save one answer per request, keyed by attempt and question, with a sequence number from the client. Retrying the same request is then harmless, and a delayed older request cannot overwrite a newer answer. In PostgreSQL, a single statement handles the deadline check, the upsert and the ordering.
CREATE TABLE answer (
attempt_id bigint NOT NULL REFERENCES attempt(id),
question_id bigint NOT NULL,
response jsonb NOT NULL,
client_seq integer NOT NULL,
saved_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (attempt_id, question_id)
);
-- $1 attempt, $2 question, $3 response, $4 client sequence
INSERT INTO answer (attempt_id, question_id, response, client_seq)
SELECT a.id, $2, $3, $4
FROM attempt a
WHERE a.id = $1
AND a.submitted_at IS NULL
AND now() <= a.deadline + interval '30 seconds'
ON CONFLICT (attempt_id, question_id) DO UPDATE
SET response = EXCLUDED.response,
client_seq = EXCLUDED.client_seq,
saved_at = now()
WHERE answer.client_seq < EXCLUDED.client_seq;If no row is affected, the API tells the client whether the attempt is closed or the write was stale. In the browser, keep a queue of unsent answers in IndexedDB or localStorage. When the network drops, the candidate keeps working, the interface shows a clear "not yet saved" status, and the queue is replayed on reconnect. Candidates panic when they cannot tell whether an answer is saved, so that indicator matters.
Integrity: what a browser can and cannot do No web page can stop a candidate using a second device. Say so in the requirements, then choose controls that raise the effort of cheating and produce evidence for review.
One active session per attempt: a new login ends the old one, and both events are logged. Log visibilitychange and blur events, so leaving the tab is recorded with timestamps. Request fullscreen with the Fullscreen API and log exits, rather than blocking the exam. Draw questions from pools and shuffle options so neighbours see different papers. Record IP address and user agent per request for later review. Treat all of these as flags for a human to review, never as automatic failure. Webcam or screen-recording proctoring raises storage, consent and retention questions, and adds bandwidth on the weakest connections. If you need it, decide who can view recordings, how long they are kept and how candidates are told before choosing a vendor.
Load test the spike you will actually get A gentle ten-minute ramp proves little. With k6, the script below fires one near-simultaneous paper fetch per candidate, then runs autosave traffic at the real interval. Seed test candidates first and put their tokens in a file.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
const tokens = new SharedArray('tokens', () => JSON.parse(open('./tokens.json')));
const BASE = __ENV.BASE_URL;
const auth = () => ({ headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${tokens[(__VU - 1) % tokens.length]}`,
} });
export const options = {
scenarios: {
start_spike: { executor: 'per-vu-iterations', vus: 2000, iterations: 1,
maxDuration: '2m', exec: 'startPaper' },
autosave: { executor: 'constant-vus', vus: 2000, duration: '20m',
startTime: '1m', exec: 'saveAnswer' },
},
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<800'],
},
};
export function startPaper() {
const res = http.get(`${BASE}/api/attempt/paper`, auth());
check(res, { 'paper 200': (r) => r.status === 200 });
}
export function saveAnswer() {
const body = JSON.stringify({ question_id: 1 + (__ITER % 60),
response: { choice: 'B' }, client_seq: __ITER + 1 });
const res = http.post(`${BASE}/api/attempt/answers`, body, auth());
check(res, { 'saved': (r) => r.status === 200 });
sleep(30);
}Run it from outside your network against a staging copy sized like production, and watch database connections as closely as response times. A common failure is application workers each holding their own connections until PostgreSQL hits max_connections; a pooler such as PgBouncer in transaction mode keeps that under control. With Django behind PgBouncer in transaction mode, set DISABLE_SERVER_SIDE_CURSORS to True in the database settings, because server-side cursors do not survive transaction pooling.
One NGINX detail catches many college deployments. Candidates in an exam hall often share a single public IP address, so a per-IP rate limit can lock out a whole room. Limit authenticated traffic per session instead, and keep a looser per-IP limit only on the login endpoint.
# http block
limit_req_zone $binary_remote_addr zone=login_ip:10m rate=20r/s;
limit_req_zone $cookie_sessionid zone=candidate:10m rate=5r/s;
# server block
location = /api/auth/login { limit_req zone=login_ip burst=200 nodelay; proxy_pass http://app; }
location /api/ { limit_req zone=candidate burst=20 nodelay; proxy_pass http://app; }NGINX does not count requests with an empty key, so the application must reject unauthenticated /api/ calls. Use your framework's session cookie name.
Exam-day operations and checklist Most exam-day problems are operational: an unplanned deploy, a full disk, a certificate that expired that morning, or a support desk that cannot see an attempt's state.
Freeze deployments from the day before, and scale servers up ahead of the sitting rather than relying on autoscaling to react in seconds. Take a database backup before the exam and confirm continuous archiving or replication is healthy. Give invigilators an admin view showing each attempt's status, last save time and remaining time. Agree a written procedure for disconnections, time extensions and reissued attempts, with every action logged. Keep marking and result generation out of the exam window; run it as a background job afterwards. Rehearse with real devices in a real exam hall at least a week before. Check accessibility: keyboard navigation, readable contrast and screen reader labels, measured against WCAG 2.2 AA. Deadline stored on the server, with the grace period enforced in the write path. Question and option order generated in the lobby and stored per attempt. Autosave is per answer, idempotent and ordered, with an offline queue in the browser. Load test covers the start spike, steady autosave and the end spike. Rate limits key on the session, not only the IP address. Integrity events are logged and reviewed by people. A rehearsal has been run and the disconnection procedure is written down. If several are missing weeks before an important sitting, start with the server-side timer, autosave and the load test. They are much cheaper to fix before exam day than to explain after it.
Infigini IT Services · 11 Kanwali Road, Dehradun 248001, India · info@infigini.com · +91 95684 87789