<?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: Ram Chandra Giri</title>
    <description>The latest articles on DEV Community by Ram Chandra Giri (@ram_chandragiri).</description>
    <link>https://dev.to/ram_chandragiri</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%2F4133359%2F5932574e-34cd-4849-91a6-50cb6b6273ae.png</url>
      <title>DEV Community: Ram Chandra Giri</title>
      <link>https://dev.to/ram_chandragiri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ram_chandragiri"/>
    <language>en</language>
    <item>
      <title>What I learned making 20 free LLM APIs behave like one</title>
      <dc:creator>Ram Chandra Giri</dc:creator>
      <pubDate>Wed, 14 Oct 2026 02:47:28 +0000</pubDate>
      <link>https://dev.to/ram_chandragiri/what-i-learned-making-20-free-llm-apis-behave-like-one-k7b</link>
      <guid>https://dev.to/ram_chandragiri/what-i-learned-making-20-free-llm-apis-behave-like-one-k7b</guid>
      <description>&lt;p&gt;In September I &lt;a href="https://dev.to/ram_chandragiri/i-open-sourced-3-small-node-utilities-i-pulled-out-of-building-an-edtech-platform-28jd"&gt;shared a few small Node packages&lt;/a&gt;. One of them was &lt;a href="https://github.com/rcgiri-physics/llm-free-cascade" rel="noopener noreferrer"&gt;llm-free-cascade&lt;/a&gt;. The idea is simple: send a chat request to Groq. If Groq says no, try Gemini. Then Cerebras. And so on, through every free tier you have a key for.&lt;/p&gt;

&lt;p&gt;The idea was simple. Getting it right was not. Here is what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Don't keep retrying the same key
&lt;/h2&gt;

&lt;p&gt;My first version always started with key #1. Once key #1 ran out for the day, every request still tried it first, failed, and only then moved on. More keys made it slower, not faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; take turns between keys. When a key is rate-limited, let it rest and skip it until it's ready again.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Limits are usually per model
&lt;/h2&gt;

&lt;p&gt;A 429 rarely means "this key is done." It usually means "this model is done for this key." So try another model on the same key before giving up on the provider.&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;new&lt;/span&gt; &lt;span class="nc"&gt;LLMCascade&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;groq&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;GROQ_API_KEY&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;groq&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="s1"&gt;openai/gpt-oss-120b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai/gpt-oss-20b&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="c1"&gt;// 2nd is tried if the 1st is limited&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Know how long to wait
&lt;/h2&gt;

&lt;p&gt;A fixed cooldown is wrong. It's too long for a per-minute limit and too short for a daily one.&lt;/p&gt;

&lt;p&gt;Providers tell you when to come back, each in their own way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;Retry-After&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;a message like &lt;em&gt;"try again in 2m59s"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;words like &lt;em&gt;"per minute"&lt;/em&gt; or &lt;em&gt;"per day"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;a known daily reset time (Gemini resets at midnight Pacific)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cascade reads all of these. A per-minute limit waits about a minute. A daily limit waits until that provider resets.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Not every error means the provider is broken
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;What the cascade does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt too long&lt;/td&gt;
&lt;td&gt;Skips that provider for this one request only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429 / quota&lt;/td&gt;
&lt;td&gt;Rests that key and model until the reset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401 / 403&lt;/td&gt;
&lt;td&gt;Stops using that one key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404 (model removed)&lt;/td&gt;
&lt;td&gt;Stops using that model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeouts, 5xx&lt;/td&gt;
&lt;td&gt;After 2 in a row, skips the provider for a minute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last one matters. Without it, a provider that is down makes every request wait for the full timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. One trick I chose not to use
&lt;/h2&gt;

&lt;p&gt;You could send the same request to two providers at once and keep the faster answer. On free tiers that spends twice the quota for one answer, so I left it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Free tiers change all the time
&lt;/h2&gt;

&lt;p&gt;This year alone, Cerebras' free tier became a 30-day trial, SambaNova started asking for a card, and GitHub Models shut down. Model names change even more often.&lt;/p&gt;

&lt;p&gt;So the provider list lives in one data file, &lt;code&gt;providers.json&lt;/code&gt;. Each entry says when it was last checked and where the numbers came from. It also warns you about things like "non-commercial use only" or "needs a phone number". A small script checks every model name against each provider's live list.&lt;/p&gt;

&lt;p&gt;One note: many providers don't allow several free accounts to get around their limits. Check each provider's terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Use it from any language
&lt;/h2&gt;

&lt;p&gt;Someone opened an issue asking to use it like the OpenAI API, from their own tools. So now you can run it as a local server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx llm-free-cascade serve        &lt;span class="c"&gt;# http://127.0.0.1:8787/v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point any OpenAI client at it, for example from Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8787/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unused&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cascade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# or "groq" to use only Groq
&lt;/span&gt;    &lt;span class="n"&gt;messages&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;role&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;user&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;content&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;Explain photosynthesis in one sentence.&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&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;It works with streaming too. The server only listens on your own machine by default, since it holds all your keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. A local backup with Ollama
&lt;/h2&gt;

&lt;p&gt;If every free tier is busy at the same time, &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; can answer from your own computer. No limits, just slower.&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;new&lt;/span&gt; &lt;span class="nc"&gt;LLMCascade&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:11434/v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llama3.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ollama&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// any text works, but it can't be empty&lt;/span&gt;
      &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// try it last&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. You can see what it's doing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx llm-free-cascade keys     &lt;span class="c"&gt;# which providers you're missing, and where to sign up&lt;/span&gt;
npx llm-free-cascade status   &lt;span class="c"&gt;# your providers, keys and limits&lt;/span&gt;
npx llm-free-cascade probe    &lt;span class="c"&gt;# quick live test of every key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In code, &lt;code&gt;cascade.stats()&lt;/code&gt; shows calls, failures, speed and cooldowns for each provider and key.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;llm-free-cascade&lt;/strong&gt; is free (MIT), has zero dependencies, and runs on Node 18+.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/rcgiri-physics/llm-free-cascade" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/llm-free-cascade" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · &lt;a href="https://github.com/rcgiri-physics/llm-free-cascade/blob/master/docs/GUIDE.md" rel="noopener noreferrer"&gt;Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Found a model name that no longer works? Fixing it is a one-file PR. Want tool calling in the server? Open an issue and tell me what you're using it with.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>node</category>
      <category>opensource</category>
      <category>llm</category>
    </item>
    <item>
      <title>Stop launching Chromium for every PDF — a tiny pool for Puppeteer HTML-to-PDF</title>
      <dc:creator>Ram Chandra Giri</dc:creator>
      <pubDate>Mon, 12 Oct 2026 19:28:40 +0000</pubDate>
      <link>https://dev.to/ram_chandragiri/stop-launching-chromium-for-every-pdf-a-tiny-pool-for-puppeteer-html-to-pdf-39dd</link>
      <guid>https://dev.to/ram_chandragiri/stop-launching-chromium-for-every-pdf-a-tiny-pool-for-puppeteer-html-to-pdf-39dd</guid>
      <description>&lt;p&gt;My exam platform, &lt;a href="https://mcqplex.com" rel="noopener noreferrer"&gt;mcqplex.com&lt;/a&gt;, turns HTML templates (reports, certificates) into PDFs with Puppeteer. Like many tutorials, my code launched and closed a full Chromium for every PDF:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&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;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// ...render...&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked, but it was slow, and memory jumped when many downloads came in at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping one browser warm
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&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;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// once, at startup&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&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;context&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBrowserContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// separate cookies/storage&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;domcontentloaded&lt;/span&gt;&lt;span class="dl"&gt;'&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;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;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A4&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="k"&gt;finally&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my 2-core laptop, a render went from about 1.9 s to about 0.66 s. Your numbers will depend on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Images still delayed printing
&lt;/h2&gt;

&lt;p&gt;My templates load a school logo from a remote URL. I used &lt;code&gt;domcontentloaded&lt;/code&gt; so a slow image host wouldn't block the render, but &lt;code&gt;page.pdf()&lt;/code&gt; still waited for the pending image. It waits for fonts to be ready by default, and that didn't finish while the request was hanging.&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;window.stop()&lt;/code&gt; before printing stopped the wait, but it also removed logos that loaded only a little slowly. A short grace period worked better for me:&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;waitForNetworkIdle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;idleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Slow logos still show up, and a broken one is left blank after 3 seconds. I only found both issues by testing with real Chromium. My mocked tests didn't catch them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The package
&lt;/h2&gt;

&lt;p&gt;In production I also needed a concurrency limit, a queue limit, timeouts and crash recovery, so I put it all into a small package, &lt;a href="https://www.npmjs.com/package/pdf-render-pool" rel="noopener noreferrer"&gt;&lt;code&gt;pdf-render-pool&lt;/code&gt;&lt;/a&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPdfRenderPool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf-render-pool&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;renderPdf&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createPdfRenderPool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/rcgiri-physics/pdf-render-pool" rel="noopener noreferrer"&gt;https://github.com/rcgiri-physics/pdf-render-pool&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it, I'd be glad to hear how it works for you, especially on Linux servers.&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>pdf</category>
      <category>puppeteer</category>
    </item>
    <item>
      <title>I open-sourced 3 small Node utilities I pulled out of building an edtech platform</title>
      <dc:creator>Ram Chandra Giri</dc:creator>
      <pubDate>Sat, 19 Sep 2026 20:08:57 +0000</pubDate>
      <link>https://dev.to/ram_chandragiri/i-open-sourced-3-small-node-utilities-i-pulled-out-of-building-an-edtech-platform-28jd</link>
      <guid>https://dev.to/ram_chandragiri/i-open-sourced-3-small-node-utilities-i-pulled-out-of-building-an-edtech-platform-28jd</guid>
      <description>&lt;p&gt;I've spent the last while building &lt;a href="https://mcqplex.com" rel="noopener noreferrer"&gt;MCQplex&lt;/a&gt;, an exam-prep&lt;br&gt;
platform for Nepal's NEB curriculum. Along the way I built a few pieces of&lt;br&gt;
infrastructure that had nothing to do with exams specifically, so I pulled&lt;br&gt;
them out into their own packages:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;a href="https://github.com/rcgiri-physics/llm-free-cascade" rel="noopener noreferrer"&gt;llm-free-cascade&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Free-tier LLM APIs (Gemini, Groq, Cerebras, and others) are individually&lt;br&gt;
rate-limited, but between them they add up to a lot of free inference if you&lt;br&gt;
just fail over from one to the next. This cascades a chat completion across&lt;br&gt;
however many you've got keys for, with automatic multi-key rotation (if&lt;br&gt;
you've made more than one free account on a provider) and cooldown for a&lt;br&gt;
provider that's structurally broken. Zero dependencies.&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LLMCascade&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;llm-free-cascade&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;cascade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LLMCascade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEnv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;}&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;cascade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;a href="https://github.com/rcgiri-physics/pdf-render-pool" rel="noopener noreferrer"&gt;pdf-render-pool&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Launching a fresh headless Chromium per PDF (the naive way to use Puppeteer)&lt;br&gt;
costs 1-2 seconds and ~200MB of RAM every time. This keeps one browser warm&lt;br&gt;
and gives each render its own page, with a concurrency cap so a burst of PDF&lt;br&gt;
requests queues instead of taking the server down.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/rcgiri-physics/mongo-job-lock" rel="noopener noreferrer"&gt;mongo-job-lock&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;If you run &lt;code&gt;node-cron&lt;/code&gt; jobs on more than one instance (PM2 cluster, several&lt;br&gt;
servers), every instance fires the job unless you do something about it.&lt;br&gt;
This is a tiny MongoDB-backed advisory lock — steal-on-expiry, so a crashed&lt;br&gt;
holder can't deadlock the job forever — for whatever Mongo connection you&lt;br&gt;
already have.&lt;/p&gt;

&lt;p&gt;All three: zero or optional-peer dependencies, MIT licensed, tested, on npm.&lt;br&gt;
Feedback / PRs welcome, especially on &lt;code&gt;llm-free-cascade&lt;/code&gt; since free-tier&lt;br&gt;
model names drift constantly and &lt;code&gt;providers.json&lt;/code&gt; is set up so a one-file PR&lt;br&gt;
fixes a stale one.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>node</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
