<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: rastersly</title>
    <description>The latest articles on DEV Community by rastersly (@rasterly).</description>
    <link>https://dev.to/rasterly</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4083191%2Fc85ef47e-2866-45fd-8187-94e91b2ef913.png</url>
      <title>DEV Community: rastersly</title>
      <link>https://dev.to/rasterly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rasterly"/>
    <language>en</language>
    <item>
      <title>HTML to PDF that doesn't look broken: the CSS nobody documents</title>
      <dc:creator>rastersly</dc:creator>
      <pubDate>Mon, 21 Sep 2026 11:43:38 +0000</pubDate>
      <link>https://dev.to/rasterly/html-to-pdf-that-doesnt-look-broken-the-css-nobody-documents-3id8</link>
      <guid>https://dev.to/rasterly/html-to-pdf-that-doesnt-look-broken-the-css-nobody-documents-3id8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Disclosure up front: I build a render API (&lt;a href="https://rasterly.dev" rel="noopener noreferrer"&gt;rasterly&lt;/a&gt;) that does HTML to PDF among other things. This post is the stuff I wish someone had written before I spent a week on it, not a pitch. The rules below work with any Chromium-based renderer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You have an HTML invoice. It looks perfect in the browser. You render it to PDF and a table row is sliced in half across the page break, the brand color is gone, and there's a mystery 1cm margin you never set. Here's why, and the small set of CSS rules that actually fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The renderer matters more than the HTML
&lt;/h2&gt;

&lt;p&gt;Two families of HTML-to-PDF exist, and they behave completely differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chromium's &lt;code&gt;printToPDF&lt;/code&gt;&lt;/strong&gt; (Playwright/Puppeteer &lt;code&gt;page.pdf()&lt;/code&gt;, and hosted APIs built on it). This is a real browser print engine. It honors &lt;code&gt;@page&lt;/code&gt;, &lt;code&gt;break-inside&lt;/code&gt;, print media queries, and web fonts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight HTML-to-image/PDF libraries&lt;/strong&gt; (wkhtmltopdf, many "html to pdf" npm packages). These use older or partial engines and quietly ignore half of the print CSS spec.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your PDFs look wrong and you're on a lightweight library, that's usually the whole problem. Everything below assumes a Chromium-based renderer, because that's the one that respects these rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four rules that fix 90% of broken PDFs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Backgrounds disappear unless you ask for them.&lt;/strong&gt; Print defaults to no background graphics, to save ink. Colors, background images, and shaded table headers vanish. In Chromium you turn them back on at render time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;printBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a hosted API it's usually a &lt;code&gt;print_background=true&lt;/code&gt; flag. This single setting is the most common "my PDF is missing the design" bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Control the page box with &lt;code&gt;@page&lt;/code&gt;, not element margins.&lt;/strong&gt; The paper size, orientation, and the physical margin around the printable area are set with the &lt;code&gt;@page&lt;/code&gt; rule, in real units:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@page&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;A4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;18mm&lt;/span&gt; &lt;span class="m"&gt;16mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trying to fake page margins with &lt;code&gt;body { padding }&lt;/code&gt; fights the engine and drifts across pages. Set the page box once with &lt;code&gt;@page&lt;/code&gt; and let content flow inside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Stop rows and cards from splitting across pages.&lt;/strong&gt; The ugliest break bug is a table row or a card cut in half by a page boundary. &lt;code&gt;break-inside: avoid&lt;/code&gt; keeps an element whole:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.card&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.line-item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;break-inside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;avoid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;thead&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;table-header-group&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c"&gt;/* repeat table headers on every page */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;table-header-group&lt;/code&gt; trick is the one people miss: it makes the table header reprint at the top of every page instead of appearing only on page one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Fonts: embed or fall back visibly.&lt;/strong&gt; A PDF renders on a server with no system fonts, so anything you don't load explicitly falls back to a default and your careful typography is gone. Load web fonts and, for print, prefer &lt;code&gt;font-display: swap&lt;/code&gt; so text never renders invisibly while a font is pending.&lt;/p&gt;

&lt;h2&gt;
  
  
  A print stylesheet, in one block
&lt;/h2&gt;

&lt;p&gt;Most document PDFs need exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@page&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;A4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;18mm&lt;/span&gt; &lt;span class="m"&gt;16mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;-webkit-print-color-adjust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;print-color-adjust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.line-item&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;break-inside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;avoid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;thead&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;table-header-group&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.page-break&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;break-before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c"&gt;/* don't print the raw URL after every link */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;print-color-adjust: exact&lt;/code&gt; is the CSS-side partner to &lt;code&gt;printBackground&lt;/code&gt; — some engines want the flag, some want the CSS, so set both and stop guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full round trip
&lt;/h2&gt;

&lt;p&gt;Here's the whole thing end to end against a hosted renderer (swap the host/header for whichever you use):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.rasterly.dev/v1/pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Api-Key: sk_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "html": "&amp;lt;style&amp;gt;@page{size:A4;margin:18mm}tr{break-inside:avoid}thead{display:table-header-group}&amp;lt;/style&amp;gt;&amp;lt;h1&amp;gt;Invoice INV-42&amp;lt;/h1&amp;gt;&amp;lt;table&amp;gt;...&amp;lt;/table&amp;gt;",
    "format": "A4",
    "print_background": true
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; invoice.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same call with Playwright directly is &lt;code&gt;page.setContent(html)&lt;/code&gt; then &lt;code&gt;page.pdf({ printBackground: true, format: "A4" })&lt;/code&gt;. Hosted or self-run, the CSS is identical — that's the point of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell if it's your CSS or your renderer
&lt;/h2&gt;

&lt;p&gt;Render the same HTML two ways: once through your current tool, once through headless Chromium (&lt;code&gt;page.pdf&lt;/code&gt;). If Chromium gets the backgrounds, page breaks, and fonts right and your tool doesn't, the tool is the problem, not your markup. If both get it wrong, it's the four rules above.&lt;/p&gt;

&lt;p&gt;I put a live HTML-to-PDF playground on my site if you want to throw a real invoice at it: &lt;a href="https://rasterly.dev/use/pdf-generation-api" rel="noopener noreferrer"&gt;rasterly.dev/use/pdf-generation-api&lt;/a&gt;. But the rules travel — paste that print stylesheet into whatever you already use and most "broken PDF" tickets go away.&lt;/p&gt;

&lt;p&gt;Happy to dig into specific break or font issues in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>pdf</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Screenshot APIs in 2026: what you're actually paying for</title>
      <dc:creator>rastersly</dc:creator>
      <pubDate>Sun, 20 Sep 2026 14:18:45 +0000</pubDate>
      <link>https://dev.to/rasterly/screenshot-apis-in-2026-what-youre-actually-paying-for-6lm</link>
      <guid>https://dev.to/rasterly/screenshot-apis-in-2026-what-youre-actually-paying-for-6lm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Disclosure up front: I build one of these APIs (&lt;a href="https://rasterly.dev" rel="noopener noreferrer"&gt;rasterly&lt;/a&gt;). I've tried to keep the comparison fair and the numbers checkable — call out anything that reads as a plug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every few months someone on my team says "let's just run headless Chrome ourselves." Two weeks later there's a memory-leak cron, a font that renders as tofu in the container, and a Chromium version pinned three releases back because upgrading broke PDFs. So most teams end up renting the render instead. This post is about what you're actually paying for when you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a "screenshot" really is
&lt;/h2&gt;

&lt;p&gt;A screenshot API does one mechanical thing: boot a headless browser, navigate to a URL, wait for the page to settle, and hand back the pixels (or a PDF, or the extracted text). The interesting part is the word &lt;strong&gt;warm&lt;/strong&gt;. A cold browser start is 300–800ms of pure overhead. A &lt;em&gt;warm&lt;/em&gt; browser that's reused across requests turns a render into roughly &lt;strong&gt;one second of CPU&lt;/strong&gt; — and that single fact is why prices are all over the map. Some vendors price on that cost. Some price on "it's a magic image service" and charge accordingly.&lt;/p&gt;

&lt;p&gt;That's the lens for the whole market: &lt;strong&gt;is this priced like a CPU-second, or like a SaaS seat?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The runnable version
&lt;/h2&gt;

&lt;p&gt;Every hosted option is basically the same shape — a URL, some options, a key. Here's the same job three ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# curl&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://api.rasterly.dev/v1/screenshot?url=https://stripe.com&amp;amp;full_page=true&amp;amp;format=png"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Api-Key: sk_live_..."&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; shot.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node — save a full-page PNG&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.rasterly.dev/v1/screenshot?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://stripe.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;full_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Api-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RASTERLY_KEY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shot.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.rasterly.dev/v1/screenshot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://stripe.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Api-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RASTERLY_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shot.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap the base URL and header name and this is Urlbox, ScreenshotOne, ApiFlash or Browserless too. Migration between them is mostly a find-and-replace, which is worth remembering when a vendor's price creeps up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five things that actually matter
&lt;/h2&gt;

&lt;p&gt;Forget the feature matrices for a second. In production these are what bite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Warm vs cold.&lt;/strong&gt; Ask (or benchmark) whether renders reuse a browser. It's the difference between ~1s and ~3s per call, and it shows up directly in your bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-page + retina done right.&lt;/strong&gt; &lt;code&gt;full_page=true&lt;/code&gt; and &lt;code&gt;scale=2/3&lt;/code&gt; sound trivial; they're where lazy-loaded images and sticky headers break. Test on a real, heavy page, not example.com.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF page control.&lt;/strong&gt; If you generate documents, you need &lt;code&gt;@page&lt;/code&gt;, margins, and &lt;code&gt;break-inside: avoid&lt;/code&gt;. Chromium honours these; lighter HTML-to-PDF tools mostly don't, and that's usually what makes a PDF look wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSRF protection.&lt;/strong&gt; A "render any URL" endpoint is an SSRF cannon if it isn't locked down. Whatever you use, confirm it refuses &lt;code&gt;localhost&lt;/code&gt;, private ranges, and &lt;code&gt;169.254.169.254&lt;/code&gt;. If you self-host, this is on you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing units.&lt;/strong&gt; Per-render, per-1,000, monthly tiers, credits-that-cost-different-amounts-per-feature… normalize everything to &lt;strong&gt;dollars per 1,000 renders at your real volume&lt;/strong&gt; before comparing. It's often a 10x spread.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Rough cost intuition
&lt;/h2&gt;

&lt;p&gt;I won't quote competitors' live prices — they change, and you should check them yourself — but the shape is consistent: category-priced screenshot APIs tend to land in the &lt;strong&gt;low single-digit dollars per 1,000&lt;/strong&gt; renders at mid volume, sometimes higher. A CPU-cost-priced one lands in the &lt;strong&gt;cents&lt;/strong&gt;. On rasterly that's &lt;strong&gt;$0.58 per 1,000&lt;/strong&gt; at the Growth tier and a free 100/month with no card; the point isn't the specific number, it's that a one-second render &lt;em&gt;can&lt;/em&gt; be cents, so if you're paying dollars, know why.&lt;/p&gt;

&lt;p&gt;Self-hosting Puppeteer/Playwright looks free until you price the cold starts, the memory headroom for full-page captures, the base image with the right fonts and codecs, and the human who babysits it. For a low, spiky volume it can win. For steady volume it usually doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually pick
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Just need a picture of a URL?&lt;/strong&gt; Any of them work; sort by dollars-per-1,000 at your volume and by whether full-page/retina look right on your worst page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generating documents/invoices?&lt;/strong&gt; Prioritise PDF page-control and font handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feeding an LLM or agent?&lt;/strong&gt; You'll want the page as Markdown/JSON too, not just an image — check whether one key covers screenshot + read/extract, or whether you're bolting on a second vendor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny or bursty volume?&lt;/strong&gt; Self-hosting or a generous free tier may beat any paid plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  See it before you sign up
&lt;/h2&gt;

&lt;p&gt;The most useful thing I added to my own site is a page that just renders &lt;em&gt;your&lt;/em&gt; URL live and shows the cost next to whatever vendor you're on, no signup:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://rasterly.dev/proof?url=stripe.com" rel="noopener noreferrer"&gt;rasterly.dev/proof?url=your-site.com&lt;/a&gt;&lt;/strong&gt; — swap in your own domain.&lt;/p&gt;

&lt;p&gt;If you're evaluating any screenshot API, do that with all of them: throw your ugliest real page at each, full-page, and compare the output and the per-1,000 side by side. The winner is usually obvious once you stop reading marketing pages and start rendering your own worst case.&lt;/p&gt;

&lt;p&gt;Happy to answer questions on the rendering pipeline, PDF gotchas, or the pricing math in the comments.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I built a screenshot API that's 89% cheaper — here's the one trick</title>
      <dc:creator>rastersly</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:13:02 +0000</pubDate>
      <link>https://dev.to/rasterly/i-built-a-screenshot-api-thats-89-cheaper-heres-the-one-trick-5em1</link>
      <guid>https://dev.to/rasterly/i-built-a-screenshot-api-thats-89-cheaper-heres-the-one-trick-5em1</guid>
      <description>&lt;p&gt;Screenshot APIs are one of the most quietly overpriced corners of dev tooling. The big ones charge $250–500/mo — for what is, underneath, a headless browser opening a page and handing back the bytes. I wanted to know how cheap it &lt;em&gt;could&lt;/em&gt; be, so I built one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfdswoicwm12tqthsszm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfdswoicwm12tqthsszm.png" alt="rasterly pricing next to the incumbents — same job, a fraction of the bill" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The one trick: reuse the browser
&lt;/h2&gt;

&lt;p&gt;The naive way to build a screenshot API is one browser launch per request. Launching Chromium costs ~2–5 seconds and a chunk of RAM every single time — that's where the cost (and the pricing) comes from.&lt;/p&gt;

&lt;p&gt;rasterly keeps &lt;strong&gt;one&lt;/strong&gt; Chromium instance alive and opens a fresh &lt;em&gt;context&lt;/em&gt; per request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cold start (first request): ~5s&lt;/li&gt;
&lt;li&gt;Warm render: &lt;strong&gt;~1s of CPU&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A warm render costing about a second of CPU means the real per-render cost is cents, not dollars. So I price on that: &lt;strong&gt;from $0.90 per 1,000&lt;/strong&gt;, versus ~$5/1,000 at the pricier incumbents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't skip the SSRF guard
&lt;/h2&gt;

&lt;p&gt;A screenshot API takes a URL from a stranger and opens it from &lt;em&gt;your&lt;/em&gt; server. Without a guard, someone points it at &lt;code&gt;http://169.254.169.254/…&lt;/code&gt; and reads your cloud metadata.&lt;/p&gt;

&lt;p&gt;Every target is DNS-resolved first and refused if it lands on localhost, a private range, link-local, or cloud metadata. Enforced, not a setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  PDF is basically free
&lt;/h2&gt;

&lt;p&gt;Once you have a page open in Chromium, &lt;code&gt;page.pdf()&lt;/code&gt; is right there — so PDF is a first-class format at the same endpoint and price as an image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.rasterly.dev/v1/screenshot?url=https://stripe.com&amp;amp;format=pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Api-Key: ..."&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; page.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it — no signup
&lt;/h2&gt;

&lt;p&gt;There's a live box on the landing page: paste a URL, and it hits the real API and streams the screenshot right back. Here it is rendering Hacker News in ~1.7s:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcruptv0yurma9i5dpai4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcruptv0yurma9i5dpai4.png" alt="The live Try it box on rasterly.dev rendering Hacker News in about a second and a half" width="800" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go break it yourself → &lt;a href="https://rasterly.dev?ref=devto" rel="noopener noreferrer"&gt;rasterly.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's brand new and I'm a solo dev, so I'd love feedback — especially on the security side and on formats you'd actually need.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Prices and features mentioned were observed in August 2026 and are shown for comparison only; rasterly is an independent product, not affiliated with any other tool named.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
