<?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: MyWorld0007</title>
    <description>The latest articles on DEV Community by MyWorld0007 (@myworld0007).</description>
    <link>https://dev.to/myworld0007</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%2F4127297%2F82f0caa6-36a1-4f74-9c3f-2f732242f96b.png</url>
      <title>DEV Community: MyWorld0007</title>
      <link>https://dev.to/myworld0007</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/myworld0007"/>
    <language>en</language>
    <item>
      <title>How I Built a Screenshot API on Rs.0/Month Infrastructure (Cloudflare Workers + Oracle Free Tier)</title>
      <dc:creator>MyWorld0007</dc:creator>
      <pubDate>Wed, 16 Sep 2026 05:28:24 +0000</pubDate>
      <link>https://dev.to/myworld0007/how-i-built-a-screenshot-api-on-rs0month-infrastructure-cloudflare-workers-oracle-free-tier-57mj</link>
      <guid>https://dev.to/myworld0007/how-i-built-a-screenshot-api-on-rs0month-infrastructure-cloudflare-workers-oracle-free-tier-57mj</guid>
      <description>&lt;p&gt;"The complete architecture of a screenshot API that runs entirely on free-tier services — Cloudflare Workers, D1, R2, and an Oracle ARM instance running headless Chromium."&lt;br&gt;
tags: cloudflare, api, indiehackers, tutorial&lt;br&gt;
cover_image: &lt;a href="https://shotlyapi.in/logo.svg" rel="noopener noreferrer"&gt;https://shotlyapi.in/logo.svg&lt;/a&gt;&lt;br&gt;
I ran a Puppeteer cluster on AWS for a year. Three EC2 instances, a load balancer, auto-scaling rules that nobody on the team fully understood. The monthly bill crossed Rs.15,000 (~$190) — for screenshots.&lt;br&gt;
Chrome ate RAM like a monster. Cold starts took 15 seconds. Memory leaks appeared every other week. I SSH'd into servers at 2 AM more times than I'd like to admit, restarting PM2 and praying the process survived till morning.&lt;br&gt;
Then I tore it all down and rebuilt everything on free-tier services only. The result: ShotlyAPI — a screenshot API with 21 parameters, running on Rs.0/month infrastructure. And it's faster than the AWS setup was.&lt;br&gt;
This article is the complete architecture, including the gotchas nobody tells you about.&lt;br&gt;
The Stack&lt;br&gt;
Service Purpose Free Tier&lt;br&gt;
Cloudflare Workers  API gateway, auth, billing  100K requests/day&lt;br&gt;
Cloudflare D1   User data, usage tracking (SQLite)  5GB storage&lt;br&gt;
Cloudflare R2   Screenshot cache (S3-compatible)    10GB storage, zero egress fees&lt;br&gt;
Oracle Cloud ARM    Headless Chromium rendering server  4 OCPUs, 24GB RAM — Always Free&lt;br&gt;
Cloudflare Tunnel   Connects Oracle server to the edge  Free, unlimited&lt;br&gt;
Resend  Transactional email 3K emails/month&lt;br&gt;
Total monthly cost: Rs.0.&lt;br&gt;
Why This Combo Works&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cloudflare Workers as the front door
The Worker does everything except rendering: API key authentication, plan/usage checks against D1, rate limiting, Razorpay billing (we're targeting Indian developers, so INR pricing matters), and caching lookups against R2.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified request flow&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;ctx&lt;/span&gt;&lt;span class="p"&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;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;user&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;getUserByApiKey&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;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// D1 lookup&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;used&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;upgradePrompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;// sales happens here&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;SCREENSHOTS&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="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// R2 hit?&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// &amp;lt;500ms response&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;forwardToOracleServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;// ~3s response&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;The genius of Workers here: your API lives on Cloudflare's edge network — the same 300+ locations serving their DNS. Latency to Indian users is under 20ms.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;R2 for the screenshot cache (the secret weapon)
Every screenshot gets stored in R2 with a 7-day lifecycle rule. A cache key is built from the URL + all parameters, so identical requests are cache hits.
This changed the economics completely:
First request for a URL: ~3 seconds (full Chromium render)
Every repeat request: under 500ms (R2 edge cache, zero egress fees)
Zero egress fees is the killer feature. On S3, serving screenshots would cost money per GB transferred. On R2, it's free — reads from the free tier don't count against you the way S3 does.&lt;/li&gt;
&lt;li&gt;The Oracle Cloud Always Free tier (the part nobody believes)
This is where the actual Chromium rendering happens. Oracle's Always Free tier includes:
4 OCPUs (Ampere ARM)
24GB RAM
200GB storage
No time limit — "always" actually means always
24GB of RAM is genuinely enough to run a Puppeteer fleet. Chromium instances for screenshot capture are short-lived — launch, render, capture, kill. With proper process management (PM2 + &lt;code&gt;--no-sandbox&lt;/code&gt; flags tuned for ARM), the instance comfortably handles hundreds of concurrent captures.
The gotchas:
ARM64, not x86. &lt;code&gt;apt install chromium-browser&lt;/code&gt; on Ubuntu ARM works, but some Puppeteer versions expect x86 paths. Pin &lt;code&gt;executablePath: '/usr/bin/chromium-browser'&lt;/code&gt; explicitly.
You need a credit card for verification — but they never charge you. A debit card works (this matters in India where credit card penetration is low).
Capacity regions matter. Some regions are perpetually "out of capacity" for the free ARM shapes. Keep trying — it took me three days to get an instance in a usable region.&lt;/li&gt;
&lt;li&gt;Cloudflare Tunnel: no open ports, no public IP exposure
The Oracle server never exposes a port to the internet. Instead, &lt;code&gt;cloudflared&lt;/code&gt; runs as a systemd service and creates an outbound-only tunnel:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tunnel.shotlyapi.in  →  localhost:3000 (Node.js + Puppeteer)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The Worker forwards render requests to this tunnel URL with a shared secret header. If the secret doesn't match, the request dies at the door. No port scanning, no DDoS surface, no certificate management.&lt;br&gt;
The Request Flow (Step by Step)&lt;br&gt;
Client sends &lt;code&gt;GET /api/screenshot?url=https://example.com&amp;amp;full_page=true&lt;/code&gt; with an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header&lt;br&gt;
Worker validates the API key against D1 (keys are stored as SHA-256 hashes — never plaintext)&lt;br&gt;
Worker checks plan limits: free (20), trial (100/7 days), starter (2000/mo), etc.&lt;br&gt;
Worker builds a cache key from URL + params and checks R2&lt;br&gt;
Cache hit → return image from edge in &amp;lt;500ms. Done.&lt;br&gt;
Cache miss → forward to the Oracle server via the Tunnel&lt;br&gt;
Puppeteer launches Chromium, renders the page, captures PNG/JPEG/PDF&lt;br&gt;
Screenshot stored in R2 (7-day lifecycle), returned to the client (~3s)&lt;br&gt;
Usage logged to D1 for analytics and billing&lt;br&gt;
The Business Layer&lt;br&gt;
Because the Worker handles auth and billing, the architecture supports a full SaaS:&lt;br&gt;
Free tier: 20 screenshots, no credit card — signup takes 30 seconds, you get an API key immediately&lt;br&gt;
Trial: Rs.99 one-time for 100 screenshots/7 days&lt;br&gt;
Subscriptions: Starter Rs.499/mo, Growth Rs.899/mo, Pro Rs.1,799/mo via Razorpay&lt;br&gt;
The upgrade path is organic: when a free user hits 20 screenshots, the API returns a friendly error with the billing link. The limit IS the salesperson.&lt;br&gt;
Everything is tracked in D1: signups, usage, payments (via Razorpay webhooks), page views. A separate admin dashboard reads all of it — same free stack, same Rs.0.&lt;br&gt;
What I'd Do Differently&lt;br&gt;
Three honest lessons:&lt;br&gt;
I'd add rate limiting on day one, not month three. Free tiers attract scrapers. A simple IP-based limiter in D1 (10 requests/minute) killed 95% of abuse.&lt;br&gt;
ARM compatibility took longer than expected. Budget a full day for Puppeteer-on-ARM debugging before the architecture feels stable.&lt;br&gt;
The 10GB R2 free tier fills faster than you'd think at high volume. The 7-day lifecycle rule is what keeps it sustainable — set it up before launch, not after your first alert.&lt;br&gt;
The Numbers&lt;br&gt;
Metric  AWS (old)   Free-tier (new)&lt;br&gt;
Monthly cost    Rs.15,000+  Rs.0&lt;br&gt;
Cold start  ~15s    ~3s (warm server)&lt;br&gt;
Cached response ~2s &amp;lt;500ms&lt;br&gt;
Maintenance hours/week  5-8 ~0&lt;br&gt;
Uptime  Heart-monitor   99.9%+&lt;br&gt;
Try It&lt;br&gt;
The API is live at shotlyapi.in. There's a free tier — 20 screenshots, no credit card, no expiry:&lt;br&gt;
Signup (30 seconds): shotlyapi.in/signup&lt;br&gt;
Try without an account: shotlyapi.in/playground&lt;br&gt;
Code examples (cURL, Node, Python, PHP, Go): github.com/MyWorld0007/shotlyapi-examples&lt;br&gt;
One line of code:&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;-G&lt;/span&gt; &lt;span class="s2"&gt;"https://api.shotlyapi.in/api/screenshot"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"url=https://example.com"&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;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; screenshot.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Happy to answer any architecture questions in the comments — especially about the Oracle ARM setup, since that's the part most people ask about.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>api</category>
      <category>indiehackers</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
