thum.io Alternative: Signed URLs Instead of Live Hits

A thum.io alternative with measured thum.io latency and output: per-hit vs per-render pricing, the animated GIF default, URL options mapped and migration code.

By Simon Chiu · Updated

Disclosure: I build RenderScreenshot. thum.io is one of the cheapest and simplest ways to put a website thumbnail on a page, and for many sites it is all they need. This page measures it, then explains where a different model fits better.

thum.io's model is unusual and very convenient: you put https://image.thum.io/get/<url> straight into an <img> tag, and the image appears. No backend code, no key for the first 1,000 views a month. The two things that send people looking for a thum.io alternative are built into that model: you pay per hit (every view), not per render, and the URL is public, so access control depends on referrer checks or thum.io's signed keys. RenderScreenshot uses signed URLs generated on your server, with an expiry, and bills renders rather than views. This page shows thum.io's real output and timing, maps its URL options, and gives you code to rewrite existing <img> tags. It is a spoke of our best screenshot API comparison.

Last verified: October 15, 2026, against the thum.io URL API documentation and the pricing section of thum.io, and by calling thum.io's free endpoint ourselves.

Should you switch?

Stay with thum.io if Move to RenderScreenshot if
You want zero backend code: paste a URL into <img> and you are done You can generate URLs on your server (any framework can)
Your traffic is small; 1,000 free hits a month covers you High page views would multiply per-hit charges
A 1200 px browser and a 600 px thumbnail suit you You need other viewports, retina, element capture, WebP or AVIF
You like the animated loader as a progressive placeholder You want a final image on the first byte, every time
You need wait conditions, cookie-banner blocking, or social-card presets

What thum.io actually returns: measured

We called thum.io's free endpoint from one machine on October 15, 2026, with no account. To get uncached renders, each URL had a unique query string (https://example.com/?rs-b08-...), which thum.io treats as a new page. Then we requested the same URL again for the cached case.

Request Runs Median Range Output
/get/noanimate/<url>, uncached 5 1.21 s 1.12 to 1.84 s PNG, 600×600, 29,025 bytes
/get/noanimate/<url>, repeated 5 0.38 s 0.33 to 0.43 s same PNG
/get/<url>, uncached (default) 3 1.76 s 1.30 to 1.81 s animated GIF, 600×600, 77 to 108 KB, 10 frames

Method: curl timing (time_total) from a single client, sequential requests, target example.com. Five or three samples are enough to show behaviour, not to rank services. The responses whose headers we inspected carried cache-control: max-age=86400.

Three 600 px thum.io outputs for example.com: the first GIF frame shows a grey loading ring and the thum.io logo, the last GIF frame shows the page, and the noanimate PNG shows the page

Left and centre: first and last frame of the default response for an uncached URL. Right: the same URL with noanimate/. Captured from image.thum.io on October 15, 2026.

Three things to take from this:

  1. The default is an animation, not a screenshot. On an uncached URL, thum.io streams a GIF whose first frames are a branded loading ring (the Good plan lists a "Thum.io branded loader"). In a browser that is a nice progressive placeholder. In a backend job that downloads images, you save a GIF of a spinner unless you add noanimate/. The documentation recommends noanimate/ for batch jobs, and our test shows why.
  2. The default framing is 1200×1200 scaled to 600×600. The browser is 1200 px wide, the capture is cropped at 1200 px high, and the thumbnail is half the width. That is a square thumbnail, not a 16:9 or Open Graph shape.
  3. Caching is fast. A repeat request came back in under half a second. That is also the hit you pay for on a paid plan.

We have not yet published RenderScreenshot timings from the same client for comparison, so this page shows no side-by-side render from our API. You can produce one in a minute with the request at the end of the page.

Per-hit vs per-render pricing

thum.io's prices, as listed on its home page on October 15, 2026:

  • Good: $2 per 10,000 hits, $1 monthly minimum, first 1,000 hits free, branded loader.
  • Better: $1 per 10,000 hits, $20 monthly minimum, first 1,000 hits free, unbranded loader, queue priority, viewport width and full-page options.
  • Enterprise: by email.

These are low numbers, and for a site with few views thum.io is close to free. The model matters at scale because a hit is a view. Put 20 thumbnails on a directory page that gets 50,000 views a month and you serve 1,000,000 hits, whether the underlying screenshots changed or not.

Scenario thum.io Good thum.io Better RenderScreenshot
1,000 hits (1,000 views of one image) free $20 minimum 1 render per image per cache period
10,000 hits about $1.80 $20 minimum 1 render per image per cache period
100,000 hits about $19.80 $20 minimum same
1,000,000 hits (20 images × 50,000 views) about $200 about $100 20 renders per cache period

The RenderScreenshot column assumes you serve the cached image. With the default 24-hour cache, 20 images cost about 600 renders a month; with a 30-day cache.ttl, about 20. Free plan: 150 credits a month. Paid plans start at $10 for 2,500 credits (pricing). Cache hits are not billed as renders (caching docs).

For a small site, thum.io is cheaper and simpler. For a high-traffic page of thumbnails, per-render billing with a long cache wins by a wide margin.

URL options mapped

thum.io encodes options as path segments before the target URL. Here is each one in RenderScreenshot terms:

thum.io path option RenderScreenshot Notes
/get/<url> GET /v1/screenshot?url=... with a signed URL Signed on your server, see below
width/600/ (thumbnail width, default 600) output.resize.width
crop/1200/ (capture height, default 1200) viewport.height
viewportWidth/1200/ (max 2400, Better plan) viewport.width (up to 7680)
fullpage/ (Better plan) capture.mode: full_page or the full_page preset
wait/3/ (seconds, Better plan) wait.delay: 3000 or wait.for_selector
maxAge/24/ (hours) cache.ttl: 86400 (seconds)
png/ / allowJPG/ output.format (png, jpeg, webp, avif)
noanimate/ not needed We never return a loader animation
iphoneX/, iphone14promax/, galaxys5/ ... viewport.device (iphone_14_pro, iphone_14_pro_max, pixel_7, ...)
prefetch/ + callbackUrl POST /v1/batch or webhook.url
/image/... (resize an image) not offered
/pdfSource/... (PDF page to image) not offered
auth by referer, or signed requests signed URLs with expires (max 30 days)

Two ways to replace a live thum.io URL

1. Signed URLs in templates. Generate the URL on your server when you render the page. It contains the options, an expiry and an HMAC signature, never your secret key. If someone changes a parameter, the signature fails. This is the closest to thum.io's "URL in an <img>" workflow.

2. Render once, store the CDN link. Call the API when content is published, store the CDN URL from the response, and put that URL in the page. Views never touch the API. This is the cheapest pattern for high-traffic pages.

Node: rewrite thum.io tags to signed URLs

// rewrite-img-tags.mjs: replace thum.io <img> URLs in HTML with signed RenderScreenshot URLs.
// Node 22+, npm i renderscreenshot. Run it server-side (it needs your key); never ship the key.
// Usage: RS_API_KEY=rs_live_... node rewrite-img-tags.mjs < page.html > page.signed.html
import { Client, TakeOptions } from 'renderscreenshot';
import { text } from 'node:stream/consumers';

const client = new Client(process.env.RS_API_KEY);
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; // the maximum lifetime of a signed URL

// Matches //image.thum.io/get/<options>/<target>, e.g. /get/width/600/crop/800/https://example.com/
const THUM = /(?:https?:)?\/\/image\.thum\.io\/get\/((?:[a-zA-Z0-9]+\/(?:\d+\/)?)*?)(https?:\/\/[^"'\s<>]+)/g;

export function toSigned(optionPath, target, expiresAt = new Date(Date.now() + THIRTY_DAYS)) {
  const parts = optionPath.split('/').filter(Boolean);
  const get = (name) => { const i = parts.indexOf(name); return i >= 0 ? Number(parts[i + 1]) : undefined; };
  // thum.io renders a 1200 px wide browser and scales the result to `width` (default 600).
  let opts = TakeOptions.url(target).width(get('viewportWidth') ?? 1200).height(get('crop') ?? 1200).format('webp');
  if (parts.includes('fullpage')) opts = opts.fullPage();
  return client.generateUrl(opts, expiresAt);
}

const html = await text(process.stdin);
process.stdout.write(html.replace(THUM, (_m, optionPath, target) => toSigned(optionPath, target)));

Use it as a build step for static pages, or call toSigned from your template helper for dynamic ones. Regenerate pages before the 30-day expiry; a page cached longer than that will show broken images.

"""render_on_publish.py: render a thumbnail once when content is published and store
its CDN URL, instead of pointing every page view at a live screenshot URL.

Python 3.10+, standard library only.
Usage: RS_API_KEY=rs_live_... python3 render_on_publish.py https://example.com
"""
import json
import os
import sys
import urllib.error
import urllib.request

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


def thumbnail_url(page_url: str) -> str:
    body = {
        "url": page_url,
        "viewport": {"width": 1200, "height": 1200},              # thum.io's browser size
        "output": {"format": "webp", "resize": {"width": 600}},   # thum.io's default width
        "cache": {"ttl": 2592000},                                 # keep the CDN copy 30 days
    }
    req = urllib.request.Request(
        f"{RS}/v1/screenshot", data=json.dumps(body).encode(), method="POST",
        headers={
            "Authorization": f"Bearer {os.environ['RS_API_KEY']}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        },
    )
    with urllib.request.urlopen(req, timeout=60) as res:
        return json.load(res)["image"]["url"]


if __name__ == "__main__":
    try:
        print(thumbnail_url(sys.argv[1] if len(sys.argv) > 1 else "https://example.com"))
    except urllib.error.HTTPError as err:
        print(f"HTTP {err.code}: {err.read()[:200]!r}", file=sys.stderr)
        sys.exit(1)

The stored link stays valid until the cache entry expires (30 days here). Re-render on a schedule or when the target page changes.

Ruby: a view helper with expiry tracking

# thumbnail_helper.rb: a view helper that replaces image.thum.io/get/<url> in templates.
# It renders once, then serves the stored CDN URL until it is close to expiry.
# gem install renderscreenshot
require 'renderscreenshot'
require 'time'

module ThumbnailHelper
  CLIENT = RenderScreenshot.client(ENV.fetch('RS_API_KEY'), base_url: ENV['RS_BASE_URL'])
  STORE = {} # use Rails.cache or a database column in a real app

  def self.thumbnail_url(page_url)
    cached = STORE[page_url]
    return cached[:url] if cached && cached[:expires_at] > Time.now + 3600

    response = CLIENT.take_json(
      RenderScreenshot::TakeOptions.url(page_url).width(1200).height(1200).format('webp').cache_ttl(2_592_000)
    )
    expires = response.dig('cache', 'expires_at')
    STORE[page_url] = {
      url: response.dig('image', 'url'),
      expires_at: expires ? Time.parse(expires) : Time.now + 86_400
    }
    STORE[page_url][:url]
  end
end

puts ThumbnailHelper.thumbnail_url(ARGV.fetch(0, 'https://example.com')) if $PROGRAM_NAME == __FILE__

In a Rails view, pass ThumbnailHelper.thumbnail_url(site.url) to image_tag where the template used to build the image.thum.io/get/ URL.

Tested on October 15, 2026: the Node rewriter with Node 22.18.0 and [email protected] on sample HTML (URL generation runs locally, so no request was sent); the Python and Ruby versions (Python 3.14.8; Ruby 4.0.7 with the renderscreenshot gem 1.0.0) against a local mock of the API. None were run against production.

When thum.io is the better choice

thum.io is the better choice for a small site, a prototype or an internal tool where pasting one URL into an <img> tag is worth more than any feature list, and where traffic stays near the free 1,000 hits. Its image resizing and PDF-to-image endpoints are also things we do not offer. If you are comparing other "URL in an img tag" services, see the url2png alternative page too.

Move when page views start to cost real money, when you need a non-square framing, or when you cannot have a loader animation in your downloads. To see the difference on your own page, create a free key and request GET https://api.renderscreenshot.com/v1/screenshot?url=<your page>&width=1200&height=1200&format=webp with your key as a Bearer token; compare it with https://image.thum.io/get/noanimate/<your page>.

Frequently asked questions

Why does thum.io return a GIF instead of a PNG?
On an uncached URL, thum.io streams an animated GIF that starts with a loading animation and ends with the screenshot. Add noanimate/ to the URL path to get a single PNG. In our test, cold requests without noanimate returned 77 to 108 KB GIFs with 10 frames.
How fast is thum.io?
From one client on October 15, 2026, five uncached captures of example.com with noanimate took 1.12 to 1.84 seconds (median 1.21 s), and repeat requests for the same URLs took 0.33 to 0.43 seconds (median 0.38 s). This is a small sample from one location, not a benchmark.
How does thum.io pricing compare to a per-render API?
thum.io charges per hit: every time an image is requested, after the first 1,000 free each month. It lists $2 per 10,000 hits on its Good plan ($1 monthly minimum) and $1 per 10,000 on Better ($20 minimum). RenderScreenshot charges per render; cache hits are not billed as renders.
Can I use RenderScreenshot directly in an img tag like thum.io?
Yes, with a signed URL generated on your server. The URL carries an expiry of up to 30 days and a signature instead of your secret key. Or render once and use the public X-Cache-URL CDN link.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.