← Back to blog
12 min readMarch 15, 2026

Screenshot API Error Handling: Best Practices

Screenshot APIs interact with the open web, which means things can and will go wrong. Target sites go down, pages take too long to load, rate limits get hit, and network connections fail. Building robust error handling from day one saves you from debugging production incidents at 3am.

Understanding HTTP Status Codes

ScreenshotAPI returns standard HTTP status codes. Here is what each category means for your integration:

CodeMeaningAction
200SuccessSave the image/PDF
400Bad RequestFix request parameters
401UnauthorizedCheck API key
403ForbiddenURL blocked (SSRF protection)
404Not FoundTarget page does not exist
429Rate LimitedWait and retry with backoff
500Server ErrorRetry after delay
504Gateway TimeoutPage too slow; increase wait or use async

Retry Strategy with Exponential Backoff

Not all errors are permanent. Network blips and temporary server issues resolve on their own. Implement exponential backoff to handle transient failures:

async function captureWithRetry(url, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(
        `https://screenshotapi-api-production.up.railway.app/v1/screenshot?url=${encodeURIComponent(url)}`,
        { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
      );

      if (response.ok) {
        return await response.arrayBuffer();
      }

      // Don't retry client errors (except 429)
      if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        throw new Error(`Client error ${response.status}: ${await response.text()}`);
      }

      // For 429, respect Retry-After header
      if (response.status === 429) {
        const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
        console.log(`Rate limited. Waiting ${retryAfter}s...`);
        await sleep(retryAfter * 1000);
        continue;
      }

      // Server error: retry with backoff
      const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
      console.log(`Attempt ${attempt} failed (${response.status}). Retrying in ${delay}ms...`);
      await sleep(delay);

    } catch (error) {
      if (attempt === maxRetries) throw error;
      const delay = Math.pow(2, attempt) * 1000;
      await sleep(delay);
    }
  }
  throw new Error('Max retries exceeded');
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Timeout Handling

Some websites take a very long time to load -- heavy JavaScript, slow APIs, large images. Set a reasonable timeout on your end to avoid hanging requests:

// Set a 30-second timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);

try {
  const response = await fetch(apiUrl, {
    headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
    signal: controller.signal,
  });
  clearTimeout(timeout);
  // Process response...
} catch (error) {
  clearTimeout(timeout);
  if (error.name === 'AbortError') {
    console.error('Screenshot request timed out after 30s');
    // Use fallback image or queue for async processing
  }
}

For pages that consistently timeout, consider using the wait parameter to give the page more time to render, or switch to async mode with webhooks.

Fallback Images

When a screenshot fails and retries are exhausted, show a fallback instead of a broken image. This is especially important for customer-facing features like link previews:

async function getScreenshotOrFallback(url) {
  try {
    const screenshot = await captureWithRetry(url);
    return { type: 'screenshot', data: screenshot };
  } catch (error) {
    console.error(`Screenshot failed for ${url}: ${error.message}`);
    return {
      type: 'fallback',
      data: generateFallbackImage(url), // SVG or canvas with URL text
    };
  }
}

function generateFallbackImage(url) {
  // Generate a simple placeholder with the domain name
  const domain = new URL(url).hostname;
  return `<svg width="1280" height="800" xmlns="http://www.w3.org/2000/svg">
    <rect fill="#1A1A2E" width="100%" height="100%"/>
    <text x="50%" y="50%" text-anchor="middle" fill="#94a3b8"
      font-family="system-ui" font-size="24">${domain}</text>
  </svg>`;
}

Rate Limit Management

ScreenshotAPI includes rate limit headers in every response. Use them to pace your requests and avoid hitting limits:

// Check rate limit headers
const remaining = response.headers.get('X-RateLimit-Remaining');
const resetTime = response.headers.get('X-RateLimit-Reset');

if (parseInt(remaining) < 5) {
  console.warn(`Only ${remaining} requests remaining. Reset at ${resetTime}`);
  // Slow down or queue remaining work
}

// For batch processing: respect limits proactively
async function batchCapture(urls) {
  const CONCURRENT = 5; // Don't exceed rate limit
  const DELAY_BETWEEN = 200; // 200ms between requests

  for (let i = 0; i < urls.length; i += CONCURRENT) {
    const batch = urls.slice(i, i + CONCURRENT);
    await Promise.all(batch.map(url => captureWithRetry(url)));
    if (i + CONCURRENT < urls.length) {
      await sleep(DELAY_BETWEEN * CONCURRENT);
    }
  }
}

Async Mode for Heavy Pages

For pages that consistently timeout or when you need to capture many screenshots, use the async endpoint with webhooks. This decouples the request from the response and handles all retry logic server-side:

// Submit async screenshot request
const response = await fetch(
  'https://screenshotapi-api-production.up.railway.app/v1/screenshot',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://heavy-js-site.com',
      format: 'png',
      async: true,
      webhook_url: 'https://your-app.com/api/screenshot-callback',
    }),
  }
);

const { job_id } = await response.json();
// Screenshot will be delivered to your webhook URL when ready

Logging and Monitoring

Track error rates and response times to catch issues before they affect users. Log enough context to debug failures without logging sensitive data:

async function captureAndLog(url, context) {
  const startTime = Date.now();
  try {
    const result = await captureWithRetry(url);
    const duration = Date.now() - startTime;
    console.log(JSON.stringify({
      event: 'screenshot_success',
      url,
      duration_ms: duration,
      context,
    }));
    return result;
  } catch (error) {
    const duration = Date.now() - startTime;
    console.error(JSON.stringify({
      event: 'screenshot_failure',
      url,
      error: error.message,
      duration_ms: duration,
      context,
    }));
    throw error;
  }
}

Error Handling Checklist

Build robust screenshot workflows

ScreenshotAPI includes rate limit headers, detailed error messages, and async processing out of the box. Start free with 100 screenshots/month.