Calculation Progress
Track the progress of long-running performance calculations (takeoff/landing) in real-time to provide user feedback.
Overview
Performance calculations can take several seconds to complete, especially for complex scenarios involving multiple derate options, optimum configurations, or parallel autobrake calculations. The Progress API allows you to poll for calculation status updates while a calculation is in progress.
When you submit a takeoff or landing performance request, the
calculation runs on the server while you poll the
progress endpoint in parallel. Each poll returns the
latest status, a human-readable description, the current
progress / total values for bar rendering, and an
elapsed_ms timer so you can surface timing
information to your users or detect stalled requests.
Get Progress
Retrieves the current progress status of a running performance calculation. Progress is tracked per API key, so each user's calculation progress is isolated.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
query |
string |
Required | Must be "progress" |
Request Body
{
"query": "progress"
}
cURL Example
curl -X POST "https://api.virtualperformancetool.com/api" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-H "X-API-Version: 1.3.6" \
-d '{
"query": "progress"
}'
Response (Starting)
Immediately after a calculation is submitted, before the first step executes, the server writes an initial state you can use to show an indeterminate spinner or a 0% bar.
{
"datetime": "2026-03-19 14:30:44",
"title": "Starting",
"description": "Preparing calculation...",
"progress": 0,
"total": 100,
"status": "running",
"completed": false,
"elapsed_ms": 38
}
Response (In Progress)
{
"datetime": "2026-03-19 14:30:45",
"title": "Takeoff Request Submission",
"description": "Calculating the regulated TOW based on TOGA...",
"progress": 20,
"total": 30,
"status": "running",
"completed": false,
"elapsed_ms": 1240
}
Response (Completed)
{
"datetime": "2026-03-19 14:30:52",
"title": "Preparing result",
"description": "Completed",
"progress": 30,
"total": 30,
"status": "completed",
"completed": true,
"elapsed_ms": 3870
}
Response (Error)
When a calculation fails, status is set to
"error" and an error_message field is
included. Stop polling and surface the message to the user.
{
"datetime": "2026-03-19 14:30:46",
"title": "Takeoff Request Submission",
"description": "Calculation failed",
"progress": 20,
"total": 30,
"status": "error",
"completed": true,
"error_message": "No valid rating available for windshear calculation",
"elapsed_ms": 1580
}
Response (No Active Calculation)
If no calculation has been started yet for the authenticated
user, the endpoint returns null.
null
Polling Strategy
To track calculation progress effectively, implement a polling loop that queries the progress endpoint at regular intervals.
Poll every 500ms to 1000ms for a smooth progress bar experience. Avoid polling more frequently than 200ms as it creates unnecessary load without improving user experience.
When to Stop Polling
Stop polling as soon as status is no longer
"running" — both "completed" and
"error" are terminal states. The main calculation HTTP
request will also return at that point, so you can safely use either
signal to stop the loop.
-
status === "completed"— success, the main calculation response is ready -
status === "error"— failure, readerror_messageand do not wait for the main request
Implementation Tips
- Start polling immediately after submitting the performance calculation request
-
Use
statusas your primary check —completed: booleanis kept for backward compatibility butstatusis more expressive -
Handle
nullresponses gracefully (no calculation in progress yet) by waiting briefly before the first retry -
Calculate percentage as
(progress / total) * 100. Note thattotalcan grow slightly mid-calculation when optional code paths execute — this is intentional so the bar never jumps backwards -
Use
descriptionfor the user-facing status message andtitlefor the current phase label -
Use
elapsed_msto detect stalled calculations and trigger a timeout after a reasonable threshold (e.g. 60 seconds)
Response Fields
| Field | Type | Description |
|---|---|---|
datetime |
string |
UTC timestamp of the last progress update (YYYY-MM-DD HH:MM:SS) |
title |
string |
Current calculation phase label — suitable for a section header (e.g. "Takeoff Request Submission") |
description |
string |
Detailed status message for the current step — suitable for a sub-label beneath the progress bar (e.g. "Calculating landing distances... (2/4)") |
progress |
number |
Current progress value. Use with total to compute
percentage: progress / total * 100
|
total |
number |
Total progress value. May increase slightly mid-calculation when optional steps execute, ensuring the bar never jumps backwards |
status |
string |
Terminal state indicator. One of "running",
"completed", or "error". Stop
polling when this is not "running"
|
completed |
boolean |
true when calculation has ended (success or
failure). Kept for backward compatibility — prefer
status
|
elapsed_ms |
number |
Milliseconds elapsed since the calculation was submitted. Useful for displaying a timer to the user or detecting stalled requests |
error_message |
string |
Only present when status is "error".
Human-readable reason the calculation failed
|
Status Values
| Value | Meaning | Action |
|---|---|---|
"running" |
Calculation is actively executing | Continue polling, update the progress bar |
"completed" |
Calculation finished successfully | Stop polling, read the result from the main calculation response |
"error" |
Calculation failed |
Stop polling, display error_message to the user
|
Calculation Phases
The title field cycles through these values as the
calculation progresses:
| Phase Title | Calculation Type | Description |
|---|---|---|
Starting |
All | Brief initial state written before the first step executes |
Takeoff Request Submission |
Takeoff | Processing takeoff performance — RTOW, derate selection, V-speeds, windshear |
Landing Dispatch Request Submission |
Landing Dispatch | Calculating max landing weight, MACG, icing conditions, most favorable runway |
Landing Enroute Request Submission |
Landing Inflight | Calculating landing distances per autobrake setting, weight limitations, Vref additive |
Preparing result |
All | Finalizing calculation, assembling response data, generating graphic results |
The description field is more granular than
title. For example, during parallel autobrake
calculations it updates per response:
"Calculating landing distances... (1/4)",
"... (2/4)", and so on. The all-intersections takeoff
(ALL) fires the full-length runway and every
intersection together in one parallel batch, showing
"Computing full-length runway + N intersections in
parallel...". Show it as the live sub-label under the bar for the best user
experience.
Error State
When a calculation throws an unrecoverable error (invalid data, no
suitable rating available, server fault, etc.) the progress file is
immediately updated with status: "error" before the
main calculation request returns. This means your polling loop will
detect the failure as soon as it happens — you don't need to wait
for the main request to time out or return an error response.
The progress error_message mirrors the
Error[].Message from the main calculation response.
Use whichever arrives first — both carry the same message.
Example Workflow
A complete example tracking takeoff performance progress, handling
the error state, enforcing a timeout via elapsed_ms,
and applying a smooth animated progress bar using a
requestAnimationFrame lerp loop.
Polling updates a targetPct every 500 ms, but an
independent animation loop running on every frame eases
displayedPct toward that target. The bar never
overshoots the server-confirmed value — it can only catch up to
it. This gives a fluid feel regardless of network jitter or
variable step sizes.
const POLL_INTERVAL_MS = 500;
const STALL_TIMEOUT_MS = 60_000;
const LERP_SPEED = 0.08; // fraction of remaining gap closed per frame (~60 fps)
// ── Shared animation state ────────────────────────────────────────────────
let targetPct = 0; // set by polling
let displayedPct = 0; // driven by rAF loop
let animating = false;
function startAnimationLoop() {
if (animating) return;
animating = true;
function tick() {
if (!animating) return;
const gap = targetPct - displayedPct;
if (Math.abs(gap) > 0.1) {
displayedPct += gap * LERP_SPEED;
} else {
displayedPct = targetPct; // snap when close enough
}
renderProgressBar(displayedPct);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
function stopAnimationLoop(finalPct) {
animating = false;
displayedPct = finalPct;
targetPct = finalPct;
renderProgressBar(finalPct);
}
// ── Main entry ────────────────────────────────────────────────────────────
async function calculateTakeoffWithProgress(requestData, token) {
// Submit the calculation (runs server-side; response arrives when done)
const calculationPromise = fetch('/api', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'X-API-Version': '1.3.6'
},
body: JSON.stringify({ query: 'takeoff', data: requestData })
});
startAnimationLoop(); // begin smooth bar before first poll arrives
await pollProgress(token); // update targetPct as responses arrive
const response = await calculationPromise;
return response.json();
}
// ── Polling loop ──────────────────────────────────────────────────────────
async function pollProgress(token) {
while (true) {
await sleep(POLL_INTERVAL_MS);
const res = await fetch('/api', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'X-API-Version': '1.3.6'
},
body: JSON.stringify({ query: 'progress' })
});
const progress = await res.json();
if (progress === null) {
// State file not written yet — calculation hasn't started, retry
continue;
}
// ── Update shared target (animation loop reads this) ──────────────────
targetPct = Math.min(100, Math.round(progress.progress / progress.total * 100));
updateStatusTitle(progress.title);
updateStatusMessage(progress.description);
updateElapsedTime(progress.elapsed_ms);
// ── Terminal states ───────────────────────────────────────────────────
if (progress.status === 'completed') {
stopAnimationLoop(100);
break;
}
if (progress.status === 'error') {
stopAnimationLoop(displayedPct);
showError(progress.error_message);
break;
}
// Stall guard — calculation taking unreasonably long
if ((progress.elapsed_ms ?? 0) > STALL_TIMEOUT_MS) {
stopAnimationLoop(displayedPct);
showError('The calculation is taking too long. Please try again.');
break;
}
}
}
// ── UI helpers (implement to match your framework) ────────────────────────
function renderProgressBar(pct) {
document.getElementById('progress-bar').style.width = pct.toFixed(1) + '%';
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
If you only need basic smoothing without predictive easing, add
transition: width 400ms ease-out to your progress bar
element in CSS and call
el.style.width = pct + '%' directly from the polling
loop. The JavaScript lerp approach above gives finer control —
useful when poll intervals are variable or you want to tune the
easing curve.
The progress polling runs concurrently with the main calculation request. The main request returns the full result when complete, while the progress endpoint provides real-time feedback during processing. You can stop polling as soon as either signal arrives.