Disclosure: RenderScreenshot is our product. Cloudinary facts on this page come from its URL2PNG add-on docs and its pricing page; URL2PNG option units come from the URL2PNG docs. All were read on 2026-10-18. We have no Cloudinary account with the add-on, so this page has no side-by-side screenshots and no timings. The migration code builds both providers' URLs offline; no URL was fetched from either service.
People search for a Cloudinary alternative for two different jobs. One is website screenshots, which Cloudinary offers through the URL2PNG add-on. The other is social cards, which Cloudinary makes with text overlays on a base image; that job is compared in the best OG image generators. This page is about the first job: an add-on inside a media platform against an API that only renders pages.
TL;DR
| Stay with Cloudinary if | Move to RenderScreenshot if |
|---|---|
| Your images already live in Cloudinary and you transform them with its URL syntax | Screenshots are the main job, not one image source among many |
You want permanent derived images and format negotiation (f_auto) on one CDN |
You want capture options: cookie-banner blocking, dark mode, waiting, clicks, PDF |
| A few screenshot options (viewport, full page, delay, user agent) are enough | You want official SDKs in six languages and typed error codes |
| One vendor and one bill matter more than capture control | You can regenerate image URLs within 30 days, or store files yourself |
How each one works
Cloudinary. You enable the URL2PNG add-on, then request an image whose delivery type is url2png and whose public ID is the website URL. Options go after the URL as /url2png/viewport=1280x800|fullpage=true. Cloudinary transformations can follow, for example a resize or an overlay. By default the URL must be signed (the s--...-- part) or generated ahead of time with the explicit API. Once a version exists, its derived transformations can use unsigned URLs. You can also turn off the signing requirement for the add-on in the console's security settings.
RenderScreenshot. The capture is the product. A GET /v1/screenshot URL with flat parameters is the image, signed with an HMAC so it can sit in public HTML. Or your server sends a POST with the full option set and gets bytes or JSON back. Every response carries a public CDN link, X-Cache-URL, that lives for the cache TTL.
Option mapping
| Cloudinary URL2PNG add-on | RenderScreenshot | Notes |
|---|---|---|
type: "url2png", public ID = website URL |
url parameter |
GET reference |
viewport=1280x800 |
width=1280, height=800 |
Width 1 to 7680, height 1 to 4320 |
fullpage=true |
full_page=true |
POST adds scroll options for lazy images |
delay=2 (seconds, per URL2PNG docs) |
delay=2000 (milliseconds) |
Capped at 10000 |
user_agent=... |
user_agent=... |
|
Signed delivery URL (sign_url: true) |
Signed GET URL: key_id, expires, signature |
Ours always expires, at most 30 days. See signed URLs in og:image tags |
explicit API to generate ahead of time |
POST /v1/screenshot with your rs_live_ key |
Store the returned link or file |
Transformation w_600,c_scale |
output.resize: { "width": 600 } |
POST only, so it cannot go in a signed URL. A GET can only set the viewport |
f_auto, q_auto |
format (png, jpeg, webp) and quality |
One fixed format per render, no negotiation per browser |
| Overlays, borders, rounded corners | Not available | Do these in CSS on a page you capture, or keep Cloudinary for this step |
| Cached derived image on Cloudinary's CDN | CDN link for the cache TTL (1 hour to 30 days) | Or your own bucket with the storage option |
| Other URL2PNG options | See the URL2PNG alternative mapping | Same option names as URL2PNG itself |
Delivery compared
This is where the two products differ most, and it decides the migration more than any option does.
| Cloudinary add-on | RenderScreenshot | |
|---|---|---|
| Who serves the image | Cloudinary's CDN, as a derived asset | Our CDN via X-Cache-URL, or the API itself for a signed URL |
| How long the URL works | Cloudinary's docs say the screenshots are cached and delivered through its CDN; they do not state a cache duration for the add-on | Signed URL: until expires (at most 30 days). CDN link: until the TTL ends (at most 30 days) |
| Refresh | Not described in the add-on docs | cache_refresh=true, or a purge |
| Further processing | Any Cloudinary transformation in the same URL | Resize in the same POST; anything else happens before capture, in the page |
| Repeat fetches | Delivery bandwidth counts toward Cloudinary credits | A cache hit with cache_available costs 0 credits |
If you need permanent URLs and Cloudinary's transformations, the honest answer may be both: capture with RenderScreenshot, then upload the result into Cloudinary from its CDN link with Cloudinary's upload API, and keep delivering from there.
Migration: one function, two providers
The listings take the URL2PNG-style options you already pass to Cloudinary and return an image URL. RS_PERCENT moves a share of target URLs to RenderScreenshot; the share is picked by a hash of the target, so one page always uses one provider. Both sides use the official SDKs. The RenderScreenshot side signs cache_available=true, so repeat views of an unchanged screenshot cost 0 credits, and uses an expiry that stays the same for a 7-day window.
Node.js
// screenshot-url.mjs: build a website screenshot URL from URL2PNG-style options, // with Cloudinary and RenderScreenshot behind one function. import { createHash } from 'node:crypto'; import { v2 as cloudinary } from 'cloudinary'; import { Client, TakeOptions } from 'renderscreenshot'; const rs = new Client(process.env.RS_API_KEY, { signingKey: process.env.RS_SIGNING_KEY, // rs_secret_... publicKeyId: process.env.RS_PUBLIC_KEY_ID, // rs_pub_... }); const DAY = 86400 * 1000; const RS_PERCENT = Number(process.env.RS_PERCENT ?? 0); // Before: the Cloudinary URL2PNG add-on (cloudinary.config() reads CLOUDINARY_URL). function cloudinaryUrl(target, { viewport = '1280x1024', fullpage = false, delay, userAgent } = {}) { const opts = [`viewport=${viewport}`, `fullpage=${fullpage}`]; if (delay) opts.push(`delay=${delay}`); if (userAgent) opts.push(`user_agent=${userAgent}`); return cloudinary.url(`${target}/url2png/${opts.join('|')}`, { type: 'url2png', sign_url: true, secure: true }); } // After: a signed RenderScreenshot GET URL with the same options. function renderScreenshotUrl(target, { viewport = '1280x1024', fullpage = false, delay, userAgent } = {}) { const [width, height] = viewport.split('x').map(Number); let options = TakeOptions.url(target).width(width).height(height).cacheAvailable().cacheTtl(2592000); if (fullpage) options = options.fullPage(); if (delay) options = options.delay(Math.min(delay * 1000, 10000)); // URL2PNG: seconds; ours: ms, max 10000 if (userAgent) options = options.userAgent(userAgent); // A 7-day window keeps the URL stable for a week; it always has 21 to 28 days left. const start = Math.floor(Date.now() / (7 * DAY)) * 7 * DAY; return rs.generateUrl(options, new Date(start + 28 * DAY)); } export function screenshotUrl(target, opts) { const bucket = createHash('sha256').update(target).digest()[0] % 100; return bucket < RS_PERCENT ? renderScreenshotUrl(target, opts) : cloudinaryUrl(target, opts); }
Python
# screenshot_url.py: URL2PNG-style options -> Cloudinary or RenderScreenshot image URL. import hashlib import os import time import cloudinary # reads CLOUDINARY_URL from cloudinary.utils import cloudinary_url from renderscreenshot import Client, TakeOptions rs = Client( os.environ['RS_API_KEY'], signing_key=os.environ['RS_SIGNING_KEY'], # rs_secret_... public_key_id=os.environ['RS_PUBLIC_KEY_ID'], # rs_pub_... ) DAY = 86400 def cloudinary_screenshot_url(target, viewport='1280x1024', fullpage=False, delay=None, user_agent=None): opts = [f'viewport={viewport}', f"fullpage={'true' if fullpage else 'false'}"] if delay: opts.append(f'delay={delay}') if user_agent: opts.append(f'user_agent={user_agent}') url, _ = cloudinary_url(f"{target}/url2png/{'|'.join(opts)}", type='url2png', sign_url=True, secure=True) return url def renderscreenshot_url(target, viewport='1280x1024', fullpage=False, delay=None, user_agent=None): width, height = (int(n) for n in viewport.split('x')) options = TakeOptions.url(target).width(width).height(height).cache_available().cache_ttl(2592000) if fullpage: options = options.full_page() if delay: options = options.delay(min(delay * 1000, 10000)) # URL2PNG: seconds; ours: ms, max 10000 if user_agent: options = options.user_agent(user_agent) start = int(time.time() // (7 * DAY)) * 7 * DAY # stable for a week, 21 to 28 days left return rs.generate_url(options, start + 28 * DAY) def screenshot_url(target, **opts): bucket = hashlib.sha256(target.encode()).digest()[0] % 100 if bucket < int(os.environ.get('RS_PERCENT', '0')): return renderscreenshot_url(target, **opts) return cloudinary_screenshot_url(target, **opts)
Ruby
# screenshot_url.rb: URL2PNG-style options -> Cloudinary or RenderScreenshot image URL. require 'digest' require 'base64' # cloudinary 2.4.3 calls Base64 without requiring it (Ruby 3.4+) require 'cloudinary' # reads CLOUDINARY_URL require 'renderscreenshot' module ScreenshotUrl DAY = 86_400 RS = RenderScreenshot::Client.new( ENV.fetch('RS_API_KEY'), signing_key: ENV.fetch('RS_SIGNING_KEY'), # rs_secret_... public_key_id: ENV.fetch('RS_PUBLIC_KEY_ID') # rs_pub_... ) module_function def cloudinary(target, viewport: '1280x1024', fullpage: false, delay: nil, user_agent: nil) opts = ["viewport=#{viewport}", "fullpage=#{fullpage}"] opts << "delay=#{delay}" if delay opts << "user_agent=#{user_agent}" if user_agent Cloudinary::Utils.cloudinary_url("#{target}/url2png/#{opts.join('|')}", type: 'url2png', sign_url: true, secure: true) end def render_screenshot(target, viewport: '1280x1024', fullpage: false, delay: nil, user_agent: nil) width, height = viewport.split('x').map(&:to_i) options = RenderScreenshot::TakeOptions.url(target).width(width).height(height).cache_available.cache_ttl(2_592_000) options = options.full_page if fullpage options = options.delay([delay * 1000, 10_000].min) if delay # URL2PNG: seconds; ours: ms, max 10000 options = options.user_agent(user_agent) if user_agent start = (Time.now.to_i / (7 * DAY)) * 7 * DAY # stable for a week, 21 to 28 days left RS.generate_url(options, expires_at: Time.at(start + (28 * DAY))) end def for(target, **opts) bucket = Digest::SHA256.digest(target).getbyte(0) % 100 bucket < ENV.fetch('RS_PERCENT', '0').to_i ? render_screenshot(target, **opts) : cloudinary(target, **opts) end end
Tested on 2026-10-18 with Node.js 22.18 (cloudinary 2.7.0), Python 3.15 (cloudinary 1.44.1) and Ruby 4.0 (cloudinary 2.4.3), with the RenderScreenshot SDKs built from source. All three printed the same signed RenderScreenshot URL for the same input, and a copy of our API's signature check accepted it. The Cloudinary URLs were built with fake credentials and not fetched, so we did not confirm that Cloudinary accepts them.
Roll out in steps
- Find every place that builds a
url2pngURL, and route them through the function above withRS_PERCENT=0. Nothing changes yet. - Check the options your code uses against the mapping table. Transformations after the capture (resize, overlays) need a POST and a stored file, or need to stay on Cloudinary.
- Set
RS_PERCENT=10. Open a sample of pages and compare the two images. - Make sure every page that shows a screenshot builds its URL at render time. Our URLs expire, so a URL stored in a database stops working within 30 days.
- Raise to 100, then remove the Cloudinary branch.
Response and auth differences
- Credentials. Cloudinary signs delivery URLs with your API secret through its SDK. We sign with a separate
rs_secret_key, and the URL carries only thers_pub_ID, so the key that signs URLs cannot call the rest of the API. - What a signed URL locks. Every parameter, including the target and the expiry. A changed parameter fails with
invalid_signature. - Server-side calls.
POST /v1/screenshotwith a Bearer key returns the image bytes, or JSON withimage.urlwhen you sendAccept: application/json. - Errors. In an
<img>tag a failed screenshot shows as a broken image on both services. Our error bodies carry a stablecodeand a link to a page about that error.
Price at three volumes
Cloudinary's pricing page (checked 2026-10-18, USD) lists Free with 25 credits a month, Plus at $99 a month ($89 billed yearly) with 225 credits, Advanced at $249 a month ($224 billed yearly) with 600 credits, and Enterprise on request. One credit is 1,000 transformations, 1 GB of storage or 1 GB of image bandwidth. The page says paid plans can use paid add-ons; we could not find the URL2PNG add-on's own quotas and prices on a public page, because the add-on marketplace is inside the console.
| Screenshots per month | Cloudinary | RenderScreenshot |
|---|---|---|
| 1,000 | A Cloudinary plan plus the add-on's quota price (not public); bandwidth for delivery uses plan credits | Starter, $10/month (2,500 credits) |
| 10,000 | Same: plan plus add-on quota | Growth, $40/month (12,000 credits) |
| 100,000 | Same: plan plus add-on quota; Enterprise on request | More than Scale's 60,000 credits ($100/month); contact us |
On our side, a render is one credit whatever the size or format, a cache hit is 0 credits, and loading the CDN link uses no credits. Our free plan has 150 credits every month. If you already pay for Cloudinary for other images, the add-on may cost you less than a second vendor; check its price in your console before you decide.
What we did not measure
We did not benchmark the URL2PNG add-on against our API, so this page reports no latency, success rate or file size. To compare them yourself, take 10 to 20 of your own target URLs, fetch each one at least three times from both services with the same viewport, and record the median time for the first (uncached) and later (cached) fetches and the bytes of each image.
When to stay with Cloudinary
- Screenshots are a small part of a large media workflow. Your images, transformations and delivery are already in Cloudinary, and the add-on is one more source.
- You need permanent URLs without running storage. Matching that with us needs a paid plan and your own bucket.
- You chain transformations in the URL, such as resizing for several breakpoints with
f_auto. Our GET URL cannot resize after capture. - The add-on's options cover your pages. If viewport, full page and a delay give good screenshots of your targets, there is little to gain.
Try the switch with a free key
Create a free account and a signing key pair, run the listing with RS_PERCENT=100 for one target URL, and open both images side by side.