Reliability

Errors, limits & best practices

What happens when something goes wrong, what we saw in our own tests, and simple code that keeps your app steady. Anything we could not test yourself is marked clearly.

How the API responds

SituationWhat you getStatus
Missing q on /shorts400 {"error": "Missing query param \"q\""}Observed
Missing q on /200 with an HTML landing page, not JSONObserved
Unknown route, e.g. /nope404 {"error": "Not found"}Observed
Unknown or invalid video ID200 with placeholder data (title like Video abc). Not a 404.Observed
Upstream returns nothing200 with [] (trending, comments, related, transcript, channel, playlist)Observed
More results requested than available200 with fewer items. Example: limit=100 returned 13.Observed
YouTube rate limit or CAPTCHAEmpty or fallback data, per the project docsReported
Internal server exception500 {"error": "message"}From worker source
Missing token on /continue400 {"error": "Missing \"token\" param"}Observed
Non-numeric limit, e.g. limit=abc200. Not rejected. We got the default 15 results.Observed
limit=0 or a negative number200. Clamped to 1 result.Observed
Channel @handle on the metadata route200 with empty fields and the handle as the IDObserved

Rule 1 · Treat “empty” as normal

A successful response can still contain nothing to show. Check the length before you render, and show a message that explains what happened.

Rule 2 · Check the response, not just the network

Some failures return HTML or a fallback object with status 200. Confirm the content type and the expected fields before you trust the data.

Rule 3 · Validate input before the call

Video IDs must be 11 characters from A-Z a-z 0-9 _ -. Search text must not be empty. Clamp limit to 1 to 50.

Rule 4 · Be polite to upstream

There is no official quota, but bursts of requests can trigger CAPTCHAs or 429 responses. Cache, space out bulk work and retry with backoff.

Safe client code

A small wrapper that validates input, checks the response type, retries transient failures, and always returns a predictable shape. Uses only fetch and AbortController, so it runs in modern browsers and in Node.js.

ytapis-client.js
const API = 'https://ytapis.djalokyt27.workers.dev';

const sleep = (ms) => new Promise(r => setTimeout(r, ms));

export async function ytapiGet(path, { retries = 3, timeoutMs = 15000 } = {}) {
  for (let attempt = 0; ; attempt++) {
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), timeoutMs);
    try {
      const res = await fetch(API + path, { signal: ctrl.signal });
      const type = res.headers.get('content-type') || '';
      if (res.status === 429 || res.status >= 500) throw new Error('retryable ' + res.status);
      if (!res.ok) return { ok: false, error: `HTTP ${res.status}`, data: null };
      if (!type.includes('application/json')) return { ok: false, error: 'Not JSON', data: null };
      return { ok: true, data: await res.json(), error: null };
    } catch (err) {
      if (attempt >= retries) return { ok: false, error: err.message, data: null };
      const delay = Math.min(8000, 500 * 2 ** attempt) * (0.5 + Math.random() / 2);
      await sleep(delay);
    } finally {
      clearTimeout(timer);
    }
  }
}

// Usage: always check ok and the length before rendering
const r = await ytapiGet('/?q=cats&limit=5');
const videos = r.ok && Array.isArray(r.data) ? r.data : [];
if (videos.length === 0) console.log('No results right now');

Note: the wrapper does not retry a 400 or 404, because those will not succeed on a second try.

Caching

Cache by URL with a time limit. Five to ten minutes is a good default for search and channel data. Stats and live status change faster, so cache them for about a minute.

cache.js
const cache = new Map();

async function cached(path, ttlMs = 5 * 60 * 1000) {
  const hit = cache.get(path);
  if (hit && Date.now() - hit.at < ttlMs) return hit.value;
  const value = await ytapiGet(path);
  cache.set(path, { at: Date.now(), value });
  return value;
}

Bulk jobs

Run requests one after another with a short pause, not all at once. Half a second between calls is a polite default. Save results as you go so a failure does not lose the whole batch.

bulk.js
const ids = ['dQw4w9WgXcQ', 'jfKfPfyJRdk'];
for (const id of ids) {
  const r = await ytapiGet(`/video/${id}/stats`);
  if (r.ok) console.log(id, r.data.views);
  await sleep(500);
}

Health check before a batch job

Start every job with /health. If it does not answer with {"status":"ok"}, stop and try again later instead of retrying every request.

check.sh
curl -fsS 'https://ytapis.djalokyt27.workers.dev/health' || echo "API is not reachable"

Found a bug or a changed response?

Open an issue on GitHub with the URL you called and the response you got. Upstream changes are the most common cause of empty or broken data.