Bản lưu ngày / Snapshot of 2026-10-08 · Xem bản mới nhất / See the latest version
WanduraDocsBeta
BetaThese docs are in beta and change often. Something wrong, unclear or missing? Tell us.
Guides

Smooth playback under load

Markdown
On this page

TL;DR — When traffic peaks, a viewer should see a short spinner, never an error. Ask for tickets early (the episode plus the next one or two, in one request), send a stable viewer_ref so repeats reuse tickets, retry 429 and 503 with Retry-After, full jitter and a retry budget, and keep the current episode playing whatever happens to the next one.

What Wandura does when it is busy

When Wandura
You reach your playback limit Answers 429 with Retry-After, usually 1–2 seconds: the allowance refills every second.
Less than about 20 % of the allowance is left Refuses prefetch tickets first ("reason": "prefetch_shed"); play tickets keep flowing.
A play request arrives just over the limit Holds it up to about 0.5 seconds before answering 429.
The same viewer asks again for the same episode, on the same device Returns the ticket it already issued (≥ 5 minutes left), not counted. Needs viewer_ref.
A ticket cannot be issued in time 503 service_unavailable with Retry-After; in a batch, only that item fails.
Wandura's database is slow Serves the catalog and licence checks from the last good answer, up to 5 minutes old.

Two consequences for your code: always read Retry-After instead of assuming a number, and give ticket requests at least 5 seconds of client timeout.

Request tickets early

  1. Batch the next episodes. When a viewer starts episode N, send one request: N with "intent": "play", plus N+1 and N+2 as prefetches if the viewer may watch them. One round trip, up to three tickets.
  2. Reuse tickets correctly. A ticket is bound to one device. Reuse it only for the same viewer, the same device, the same episode and the same intent, and only until 5 minutes before expires_at. Wandura does this for you when you send a stable viewer_ref; a backend cache under the same key is fine too. Never share a ticket across devices.
  3. Send a stable viewer_ref: an HMAC of your user ID plus a hash of the install ID. Never per session, never personal data.
{
  "episodes": [
    { "film_ref": "the-heiress-returns", "episode_no": 4, "intent": "play" },
    { "film_ref": "the-heiress-returns", "episode_no": 5, "intent": "prefetch" },
    { "film_ref": "the-heiress-returns", "episode_no": 6, "intent": "prefetch" }
  ],
  "union_info": "<device ID from the Player SDK>",
  "drm_kek": "<device public key from the Player SDK>",
  "viewer_ref": "k3Jd9sQm2vXa7LpT0bWc1e:Zr8uYq2nM4a"
}

Each item of the answer has its own status: one can be 200 while another is 404 not_ready (past the last episode), 429 (a paused prefetch) or 503 with retry_after. Retry only the failed items you need.

Retry 429 and 503 the right way

The rules (which statuses, Retry-After, full jitter, caps, timeouts, budget, concurrency) are the canonical retry policy. In code:

/** How hard to try. INTERACTIVE = a viewer is waiting; BACKGROUND = a sync job. */
export type RetryPolicy = {
  maxRetries: number; // retries after the first attempt
  baseMs: number; // first backoff step
  capMs: number; // largest backoff step
  timeoutMs: number; // per attempt
  deadlineMs: number; // the whole call, retries included
};
export const INTERACTIVE: RetryPolicy = { maxRetries: 3, baseMs: 500, capMs: 8_000, timeoutMs: 8_000, deadlineMs: 15_000 };
export const BACKGROUND: RetryPolicy = { maxRetries: 5, baseMs: 1_000, capMs: 30_000, timeoutMs: 15_000, deadlineMs: 120_000 };

/** Retry only what can succeed later: network errors, 429, 500, 502 and 503 (except 503 `not_ready`). */
export function isRetryable(status: number | null, errorCode?: string): boolean {
  if (status === null) return true;
  if (status === 503) return errorCode !== 'not_ready';
  return status === 429 || status === 500 || status === 502;
}

/** Wait before retry number `attempt` (0-based): Retry-After (+ up to 1 s jitter) wins, else full jitter under the cap. */
export function backoffMs(attempt: number, retryAfterS: number | null, p: RetryPolicy, random = Math.random): number {
  if (retryAfterS !== null && retryAfterS > 0) return retryAfterS * 1_000 + random() * 1_000;
  return random() * Math.min(p.capMs, p.baseMs * 2 ** attempt);
}

/** Retries may be at most 10 % of the requests of the last minute (never fewer than 10 a minute). */
export class RetryBudget {
  private sent = 0;
  private retries = 0;
  private since = Date.now();
  private roll() {
    if (Date.now() - this.since > 60_000) { this.sent = 0; this.retries = 0; this.since = Date.now(); }
  }
  noteRequest() { this.roll(); this.sent++; }
  tryRetry(): boolean {
    this.roll();
    if (this.retries >= Math.max(10, this.sent * 0.1)) return false;
    this.retries++;
    return true;
  }
}

The backend endpoint that uses it, with the batch and the per-item handling: Playback flow and paywall.

Keep the viewer watching

  • Spinner, not error. While your backend retries, keep the loading state. Show Retry only when the budget is spent (about 15 seconds).
  • The current episode keeps playing. A failed request for the next episode never stops or reloads the one on screen.
  • A refused prefetch is not a failure. Do nothing visible; ask with "intent": "play" when the viewer gets there.
  • Never show a raw status code.

The app-side code, in Swift and Kotlin, does exactly this: iOS, Android.

Read responses tolerantly

Ignore unknown fields, treat unknown values as the default case, and handle unknown error codes by status class. See Versioning and API evolution.

Checklist

  • Starting an episode sends one request: play for it, prefetch for the unlocked next ones.
  • Tickets are reused only per viewer, device, episode and intent, never across devices, until 5 minutes before expires_at.
  • viewer_ref is stable per viewer and device and holds no personal data.
  • 429 and 503 are retried after Retry-After plus jitter; otherwise exponential backoff with full jitter.
  • At most 3 retries per interactive request, and a 10 % retry budget per minute.
  • Client timeout of at least 5 seconds on ticket requests.
  • A refused prefetch has no visible effect and never stops the current episode.

Updated 2 Oct 2026 · Change history (3)

↑↓ selectEnter openEsc close

Send feedback

Your message goes straight to the Wandura team. It includes the address of this page.

0 / 4000