internal_error

Something failed on our side while we handled the request. Your request may be completely valid. The error is usually short-lived, so retry with backoff. If it continues, send us the request_id. This page is part of the error code reference.

HTTP status 500 Internal Server Error
error.type internal_error
error.retryable true
Credits used 0

Example response

{
  "error": {
    "type": "internal_error",
    "code": "internal_error",
    "message": "An unexpected error occurred. Please try again.",
    "retryable": true
  },
  "request_id": "req_abc123"
}

Branch on the status, not only on the code

Build your handler around the HTTP status (any 5xx) and error.retryable, and treat error.code as detail for the logs. Two reasons:

  • Not every 5xx has a JSON body. A 502, 503 or 504 can come from a load balancer or proxy in front of the API, with an HTML or empty body. res.json() throws on those. Check the Content-Type first.
  • The code can be more specific than internal_error. Server-side failures share the internal_error type and HTTP 500, while the code can name the failure more exactly.

Errors about the target page are not 500s. A page that is slow, unreachable or broken returns a 422 target_error, such as timeout or navigation_failed. Those need different settings, not just a retry.

How to handle it

  1. Retry with exponential backoff and jitter: three or four attempts over about 15 seconds is enough.
  2. Honour Retry-After if a 503 carries it.
  3. Log the request_id of each failed attempt. Support can find it in the request logs.
  4. After the last attempt, fail the job so it goes back to your queue, and alert on the rate of 5xx responses, not on each one.
  5. If one specific request fails with 500 every time while others work, it is a bug on our side. Email [email protected] with the request_id and the request body (remove secrets such as cookies first).

A retry session in Python

requests can retry 5xx responses for you through urllib3. By default it does not retry POST, so allow it explicitly. This is safe here because a failed render uses no credit.

# retry_session.py (Python 3.10+, requests 2.32+, urllib3 2.x)
import os
import sys

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

API = os.environ.get("RS_BASE_URL", "https://api.renderscreenshot.com")

retry = Retry(
    total=4,
    status_forcelist=(500, 502, 503, 504),
    allowed_methods=frozenset({"GET", "POST"}),  # POST is not retried by default
    backoff_factor=1,                            # waits 0 s, 2 s, 4 s, 8 s between attempts
    backoff_jitter=0.5,
    respect_retry_after_header=True,
    raise_on_status=False,                       # hand the last response back to us
)

session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))
session.mount("http://", HTTPAdapter(max_retries=retry))
session.headers["Authorization"] = f"Bearer {os.environ['RS_API_KEY']}"


def capture(url: str) -> bytes:
    res = session.post(f"{API}/v1/screenshot", json={"url": url}, timeout=(10, 90))
    if res.ok:
        return res.content
    if res.status_code >= 500:
        is_json = "json" in res.headers.get("Content-Type", "")
        request_id = res.json().get("request_id") if is_json else None
        raise RuntimeError(f"HTTP {res.status_code} after retries, request_id={request_id}")
    res.raise_for_status()
    raise RuntimeError(f"unexpected status {res.status_code}")


if __name__ == "__main__":
    try:
        print(len(capture(sys.argv[1])), "bytes")
    except RuntimeError as e:
        print(e, file=sys.stderr)
        sys.exit(1)

The Ruby SDK marks 5xx errors as retryable, so you can use e.retryable? directly:

require 'renderscreenshot'

client = RenderScreenshot.client(ENV.fetch('RS_API_KEY'), timeout: 90)
options = RenderScreenshot::TakeOptions.url(ARGV.fetch(0, 'https://example.com'))

attempt = 0
begin
  attempt += 1
  image = client.take(options)
rescue RenderScreenshot::Error => e
  raise unless e.retryable? && attempt < 4

  warn "attempt #{attempt}: HTTP #{e.http_status} #{e.code} (#{e.request_id})"
  sleep(rand * (2**attempt)) # full jitter: up to 2 s, 4 s, 8 s
  retry
end
File.binwrite('shot.png', image)

Tested on 2026-10-15 with Python 3.14, requests 2.34.2 and urllib3 2.8.0, and with Ruby 4.0.7 plus the renderscreenshot gem 1.0.0, against a local mock. A URL that failed twice with 500 succeeded on the third attempt (about 2.4 s in Python). A permanent 502 with an HTML body gave up after 4 retries in about 14 s and exited with code 1.

Related in this topic

Was this page helpful?