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 theContent-Typefirst. - The code can be more specific than
internal_error. Server-side failures share theinternal_errortype and HTTP 500, while thecodecan 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
- Retry with exponential backoff and jitter: three or four attempts over about 15 seconds is enough.
- Honour
Retry-Afterif a 503 carries it. - Log the
request_idof each failed attempt. Support can find it in the request logs. - 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.
- 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_idand 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
- Error handling and retries for one policy across every error type.
- Request logs to look up a
request_id.