<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Voroa Blog</title>
    <link>https://getvoroa.com/blog/</link>
    <description>Practical cloud hosting and deployment guides for developers in India. Deploy from GitHub and host Node.js, Python, Django, Docker and Next.js apps.</description>
    <language>en-IN</language>
    <lastBuildDate>Sat, 14 Nov 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://getvoroa.com/blog/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Netlify Alternative for India: Backend, Postgres and INR Billing</title>
      <link>https://getvoroa.com/blog/netlify-alternative-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/netlify-alternative-india/</guid>
      <pubDate>Sat, 14 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[A Netlify alternative for Indian teams: when a static site needs a long-running API and Postgres in Mumbai, how Netlify credits work, and how to move.]]></description>
      <content:encoded><![CDATA[<p>Most people look for a Netlify alternative when the project stops being only a static site. The landing
page now needs an API, a database, a background job and a bill your accountant can file. This guide is for
Indian developers at that point. It explains what Netlify does well, where its model gets tight for an
India-first app, and how to move without breaking anything. Every Netlify fact here comes from Netlify’s own
pages, checked on 14 November 2026.</p>
<h2 id="when-netlify-is-still-the-right-choice" tabindex="-1"><a class="header-anchor" href="#when-netlify-is-still-the-right-choice">When Netlify Is Still the Right Choice</a></h2>
<p>Be honest with yourself first. If your project is a marketing site, a blog or docs, Netlify is good at
that. It builds your site on every push, gives every pull request a deploy preview, and has built-in
form handling: you turn on form detection, add a <code>data-netlify=&quot;true&quot;</code> attribute to a <code>&lt;form&gt;</code> tag, and
submissions show up in the dashboard. Since April 2026, form submissions are free on its credit plans.</p>
<p>If that is all you need, you do not have to move. The rest of this post is for when the backend becomes
the main part of the app.</p>
<h2 id="where-indian-teams-outgrow-netlify" tabindex="-1"><a class="header-anchor" href="#where-indian-teams-outgrow-netlify">Where Indian Teams Outgrow Netlify</a></h2>
<h3 id="a-backend-that-has-to-keep-running" tabindex="-1"><a class="header-anchor" href="#a-backend-that-has-to-keep-running">A Backend That Has to Keep Running</a></h3>
<p>Netlify runs backend code as functions. Its functions reference lists a 60-second limit for a normal
function, 30 seconds for a scheduled function and 15 minutes for a background function. A function
responds and finishes, so it cannot hold a WebSocket open or keep a queue consumer alive.</p>
<p>Netlify announced <strong>Netlify Server</strong> on 10 November 2026: a long-running Node.js process that can hold
WebSockets. It is in Early Access. Netlify’s docs say it always scales to zero after 5 minutes without
requests, runs one instance on the Free and Personal plans, and does not run Next.js, Astro or Nuxt yet.
Netlify itself describes it as meant for “evaluation, experimentation, and low-volume production use”.</p>
<h3 id="no-india-region-for-functions-or-servers" tabindex="-1"><a class="header-anchor" href="#no-india-region-for-functions-or-servers">No India Region for Functions or Servers</a></h3>
<p>New Netlify functions run in <code>cmh</code> (US East, Ohio) by default, per the
<a href="https://docs.netlify.com/build/functions/configuration/">Netlify functions configuration reference</a>. The self-serve region list has Singapore
and Tokyo in Asia, but no India region, and changing the region needs a Pro or Enterprise plan. Netlify
Server supports only Ohio and Frankfurt today.</p>
<p>For a user in Pune, every API call then travels to the US or Singapore and back, and so does every query
if your database sits next to the function.</p>
<h3 id="credits-that-can-pause-every-site" tabindex="-1"><a class="header-anchor" href="#credits-that-can-pause-every-site">Credits That Can Pause Every Site</a></h3>
<p>Accounts created on or after 4 September 2025 are on credit-based plans. The Free plan gets 300 credits a
month as a hard limit. The
<a href="https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/billing-faq-for-credit-based-plans/">Netlify billing FAQ for credit plans</a>
says that when the credits are used up, “all of your web
projects (sites/apps) are paused” and visitors see a <code>Site not available</code> page. One busy project pauses
all of them, and the Free plan cannot buy more credits, so you upgrade or wait for the next cycle.</p>
<h3 id="dollar-billing-for-an-indian-business" tabindex="-1"><a class="header-anchor" href="#dollar-billing-for-an-indian-business">Dollar Billing for an Indian Business</a></h3>
<p>Netlify plans are priced in US dollars, and its billing FAQ lists credit cards, Apple Pay and Google Pay
as payment methods. We did not find a Netlify page about GST, a GSTIN field or rupee invoices. If you pay a
foreign host as a business, read <a href="/docs/reverse-charge-foreign-cloud-hosting/">how reverse charge works on foreign hosting</a>
before your next filing.</p>
<h2 id="static-site-api-and-postgres-in-one-place" tabindex="-1"><a class="header-anchor" href="#static-site-api-and-postgres-in-one-place">Static Site, API and Postgres in One Place</a></h2>
<p><img src="https://media.getvoroa.com/img/5d2414b647bcace0.png" alt="A browser loads a static site and calls an API web service in one Voroa workspace in Mumbai, with PostgreSQL, a worker and a cron job beside it; a side note shows Netlify compute in Ohio and Frankfurt" width="700" loading="eager" decoding="async"></p>
<p>On Voroa, each part of the app is its own service in one workspace, all in Mumbai:</p>
<ul>
<li><strong>Static site.</strong> Your built frontend, served as files. Static sites are free to host and do not use the
free tier’s running hours. See <a href="/static-sites/">static site hosting on Voroa</a>.</li>
<li><strong>Web service.</strong> Your Express, Fastify, Django or Next.js server as a normal long-running process.
Voroa sets <code>PORT</code>, so a server that reads <code>process.env.PORT</code> works as it is. Paid sizes never sleep,
and a web service can hold <a href="/docs/websockets/">WebSocket connections</a> open.</li>
<li><strong>PostgreSQL.</strong> A <a href="/managed-postgresql-india/">managed Postgres database in Mumbai</a>, close to the API
that queries it.</li>
<li><strong>Workers and cron.</strong> A queue consumer runs as a background worker, and a nightly job runs as a
<a href="/docs/scheduled-tasks-cron/">scheduled task</a> instead of a 30-second function.</li>
</ul>
<p>With auto-deploy on, each push builds a new release, the new release starts before the old one stops, and
you can roll back in one click.</p>
<h2 id="netlify-vs-voroa-at-a-glance" tabindex="-1"><a class="header-anchor" href="#netlify-vs-voroa-at-a-glance">Netlify vs Voroa at a Glance</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Feature</th>
<th>Netlify</th>
<th>Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Built for</td>
<td>Frontend sites plus functions</td>
<td>Static sites, web services, workers, Postgres</td>
</tr>
<tr>
<td>Backend model</td>
<td>Functions; Netlify Server in Early Access</td>
<td>Long-running web service</td>
</tr>
<tr>
<td>Function or server region</td>
<td>Ohio by default; no India region</td>
<td>Mumbai (IN), the only region</td>
</tr>
<tr>
<td>Postgres</td>
<td>Netlify Database, credit plans only</td>
<td>First-party managed PostgreSQL</td>
</tr>
<tr>
<td>Free plan</td>
<td>300 credits; all sites pause when used up</td>
<td>Nano size; free services stop at 750 hours or 5 GB</td>
</tr>
<tr>
<td>Entry paid</td>
<td>Personal $9/month, 1,000 credits</td>
<td>Web service from Rs 298/month plus GST</td>
</tr>
<tr>
<td>Billing</td>
<td>USD; cards, Apple Pay, Google Pay</td>
<td>INR with a GST invoice; UPI, cards, netbanking</td>
</tr>
</tbody>
</table></div>
<p>Netlify prices and limits were taken from its pricing page and docs on 14 November 2026. They change, so
check again before you decide.</p>
<h2 id="what-netlify-credits-cost-compared-with-rupee-prices" tabindex="-1"><a class="header-anchor" href="#what-netlify-credits-cost-compared-with-rupee-prices">What Netlify Credits Cost Compared With Rupee Prices</a></h2>
<p>Netlify’s pricing page lists these rates: 15 credits per production deploy, 20 credits per GB of
bandwidth, 10 credits per GB-hour of compute and 2 credits per 10,000 web requests. Personal is $9 a month
with 1,000 credits; Pro is $20 a month with 3,000 credits and unlimited members.</p>
<p>Do the arithmetic for a small app. Twenty production deploys a month use 300 credits, which is the whole
Free plan. One deploy a day plus 30 GB of bandwidth comes to at least 1,050 credits before web requests
and compute, already over the 1,000 in Personal, and the rest is paid through credit packs.</p>
<p>Voroa prices a service by its size, in rupees, and deploys are not charged one by one: the Free plan
includes 500 build minutes a month. It has limits too. Free services stop when the workspace uses its 750
free hours or its 5 GB of bandwidth for the month. A static site is free to host. A paid web service
starts at Rs 298/month and a paid PostgreSQL size at Rs 299/month, both plus GST. Our
<a href="/blog/netlify-pricing-india/">Netlify pricing breakdown for India</a> works through more bills, and the
<a href="/pricing/">Voroa pricing page</a> lists every size.</p>
<h2 id="what-voroa-static-sites-do-not-do" tabindex="-1"><a class="header-anchor" href="#what-voroa-static-sites-do-not-do">What Voroa Static Sites Do Not Do</a></h2>
<p>Know this before you move. A Voroa static site serves the files in your repository as they are. <strong>No build
runs.</strong> Commit your <code>dist</code> or <code>build</code> folder, or deploy the same repository as a web service if you want
the build to run on every push.</p>
<p>There is also no <code>_redirects</code> file and no rewrite rules yet. If someone opens <code>/dashboard</code> directly on a
single-page app, they get a 404. Use hash routing, or run the app as a web service that answers every path.
There is no built-in form handling either: point your forms at an endpoint on your own API.</p>
<h2 id="how-to-move-from-netlify-to-a-netlify-alternative" tabindex="-1"><a class="header-anchor" href="#how-to-move-from-netlify-to-a-netlify-alternative">How to Move From Netlify to a Netlify Alternative</a></h2>
<ol>
<li><strong>List what Netlify does for you.</strong> Check <code>netlify.toml</code> and the dashboard for functions, scheduled
functions, forms, redirects and environment variables.</li>
<li><strong>Build the frontend.</strong> For a Next.js site, <code>output: 'export'</code> in <code>next.config.js</code> writes static files
to <code>out</code>, as the <a href="https://nextjs.org/docs/app/guides/static-exports">Next.js static export guide</a>
explains. Commit that folder and follow the <a href="/docs/deploy-static-site/">static site deploy guide</a>.</li>
<li><strong>Turn functions into routes.</strong> Each function in <code>netlify/functions/</code> becomes a route in one Express or
Fastify app. Deploy it as a web service with the <a href="/docs/deploy-nodejs/">Node.js guide</a>.</li>
<li><strong>Move scheduled functions to cron</strong> and long jobs to a background worker.</li>
<li><strong>Copy environment variables</strong>, test on the Voroa URLs, then move your domain.</li>
</ol>
<p>You do not have to move everything at once. You can keep the marketing site on Netlify and point it at
an API and database in Mumbai. Our <a href="/blog/vercel-alternative-india/">Vercel alternative guide</a> covers the
same split for a Next.js app. If you are moving paid workloads, you can also
<a href="/migration-credits/">apply for migration credits</a>, granted by hand up to Rs 10,000.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="does-netlify-have-servers-in-india" tabindex="-1"><a class="header-anchor" href="#does-netlify-have-servers-in-india">Does Netlify have servers in India?</a></h3>
<p>Not for compute you can choose. Netlify’s function region list has no India region, and Netlify Server
runs in Ohio or Frankfurt. Functions default to Ohio.</p>
<h3 id="what-happens-when-a-netlify-free-plan-runs-out-of-credits" tabindex="-1"><a class="header-anchor" href="#what-happens-when-a-netlify-free-plan-runs-out-of-credits">What happens when a Netlify Free plan runs out of credits?</a></h3>
<p>Netlify’s billing FAQ says every project on the team is paused and visitors see a <code>Site not available</code>
page. You can upgrade to a paid plan or wait until the next billing cycle starts.</p>
<h3 id="can-i-keep-my-frontend-on-netlify-and-run-the-api-on-voroa" tabindex="-1"><a class="header-anchor" href="#can-i-keep-my-frontend-on-netlify-and-run-the-api-on-voroa">Can I keep my frontend on Netlify and run the API on Voroa?</a></h3>
<p>Yes. Deploy the API as a web service, allow your Netlify domain in the API’s CORS settings in your code,
and call it from the frontend. The browser talks to the API in Mumbai directly.</p>
<h3 id="does-a-free-voroa-service-sleep" tabindex="-1"><a class="header-anchor" href="#does-a-free-voroa-service-sleep">Does a free Voroa service sleep?</a></h3>
<p>A free web service with a health check path can sleep after 15 minutes without requests, and wakes on
the next one. Static sites, workers, cron jobs and paid sizes never sleep. The
<a href="/docs/idle-sleep/">idle sleep guide</a> lists every case.</p>
<h2 id="run-your-whole-stack-in-mumbai" tabindex="-1"><a class="header-anchor" href="#run-your-whole-stack-in-mumbai">Run Your Whole Stack in Mumbai</a></h2>
<p>Put the static site, the API, the worker and the database in one workspace in Mumbai, on one rupee bill
with a GST invoice. Start free with a static site, then add a web service when the backend needs to keep
running.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Netlify Backend Hosting: What Functions Can and Cannot Do</title>
      <link>https://getvoroa.com/blog/netlify-backend-hosting/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/netlify-backend-hosting/</guid>
      <pubDate>Sat, 14 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>APIs</category>
      <category>India</category>
      <category>Node.js</category>
      <description><![CDATA[Can a Netlify backend run Express, Postgres, WebSockets or a worker? Function time limits, Netlify Server, regions, and how to run the backend in India.]]></description>
      <content:encoded><![CDATA[<p>A Netlify backend is usually a set of serverless functions that sit next to your frontend and answer API
calls. That works well for small jobs, and it runs into hard limits once your backend needs a
long-running process, a queue worker, WebSockets or a database close to Indian users. This guide is for
developers whose frontend already lives on Netlify and who want to know what the backend can run there,
what it cannot, and where to put the rest.</p>
<p>All Netlify limits below are from Netlify’s own documentation as of November 2026, mainly its
<a href="https://docs.netlify.com/build/functions/configuration/">functions configuration reference</a>. Netlify changes these
numbers over time, so check their docs before you design around one.</p>
<h2 id="can-you-host-a-backend-on-netlify" tabindex="-1"><a class="header-anchor" href="#can-you-host-a-backend-on-netlify">Can you host a backend on Netlify?</a></h2>
<p>Yes, with conditions. Netlify runs backend code as functions: short-lived handlers that start for a
request and stop after it. A normal request can run for 60 seconds, a background task for 15 minutes,
and a scheduled task for 30 seconds. A long-running Node.js server is only available through Netlify
Server, which is in Early Access.</p>
<p>So the honest answer depends on what “backend” means for your app. A contact form, a payment webhook or
a small JSON API fits well. A chat server, a queue consumer, a job that takes 40 minutes, or an API
that keeps a pool of database connections open does not fit functions, and needs either Netlify Server
or a host that runs ordinary processes.</p>
<h2 id="the-five-places-backend-code-can-run-on-netlify" tabindex="-1"><a class="header-anchor" href="#the-five-places-backend-code-can-run-on-netlify">The five places backend code can run on Netlify</a></h2>
<p>Netlify has five different runtimes, and each has its own rules. Mixing them up is the most common
reason a backend works on a laptop and fails after deploy.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Runtime</th>
<th>How long it can run</th>
<th>Good for</th>
</tr>
</thead>
<tbody>
<tr>
<td>Synchronous function</td>
<td>60 seconds per request</td>
<td>API routes, form handlers, webhooks</td>
</tr>
<tr>
<td>Background function</td>
<td>15 minutes, client gets an empty <code>202</code></td>
<td>Slow follow-up work after a request</td>
</tr>
<tr>
<td>Scheduled function</td>
<td>30 seconds, on a cron schedule in UTC</td>
<td>Small periodic jobs</td>
</tr>
<tr>
<td>Edge function</td>
<td>50 ms of CPU time per request</td>
<td>Redirects, headers, personalisation</td>
</tr>
<tr>
<td>Netlify Server (Early Access)</td>
<td>Long-lived process; a request up to 30 minutes (2 hours on Pro)</td>
<td>Express, Fastify, WebSockets</td>
</tr>
</tbody>
</table></div>
<h3 id="synchronous-functions" tabindex="-1"><a class="header-anchor" href="#synchronous-functions">Synchronous functions</a></h3>
<p>This is the default. A function gets a <code>Request</code> and returns a <code>Response</code>. Netlify lists the
synchronous execution limit as 60 seconds and marks it as not configurable. A buffered request or
response can be up to 6 MB, and binary uploads are Base64-encoded, which brings the real upload limit
closer to 4.5 MB. Functions get 1 GB of memory by default; raising it (up to 4 GB) is a Pro and
Enterprise setting.</p>
<p>Older answers on Netlify’s forum give lower limits. The limit has moved over time, so trust the current
docs, not an old thread.</p>
<h3 id="background-functions" tabindex="-1"><a class="header-anchor" href="#background-functions">Background functions</a></h3>
<p>Add <code>background: true</code> and the function can run for up to 15 minutes. The catch: the caller gets an
empty <code>202</code> straight away, so the result has to go somewhere else, such as a database row the
frontend polls. If the function throws, Netlify retries it after one minute and again two minutes
later, so the work must be safe to run twice. The request payload for a background function is capped
at 256 KB.</p>
<h3 id="scheduled-functions" tabindex="-1"><a class="header-anchor" href="#scheduled-functions">Scheduled functions</a></h3>
<p>A scheduled function runs on a cron expression, has a 30-second limit, and runs in UTC. It cannot
receive a payload, cannot be called by URL, and only fires for published deploys, not previews. For
Indian teams the UTC part matters: <code>0 9 * * *</code> is 2:30 PM IST, not 9 AM.</p>
<h3 id="edge-functions" tabindex="-1"><a class="header-anchor" href="#edge-functions">Edge functions</a></h3>
<p>Edge functions run close to the visitor but get 50 ms of CPU time per request. They are built for
rewriting a request or a response, not for running your API.</p>
<h3 id="netlify-server" tabindex="-1"><a class="header-anchor" href="#netlify-server">Netlify Server</a></h3>
<p>Netlify Server is the newest option and the one that changes the answer most. It runs a normal Node.js
server (Express, Hono or Fastify) without a wrapper, keeps state in memory between requests, and holds
WebSocket connections open. The limits are worth reading before you plan around it:</p>
<ul>
<li>It is in <strong>Early Access</strong>. Netlify says it is “meant for evaluation, experimentation, and low-volume
production use”, and that its APIs and behaviour might change.</li>
<li>It <strong>always scales to zero</strong>: an instance stops after 5 minutes without requests. There is no
always-on setting yet.</li>
<li>It runs <strong>up to 3 instances</strong>, or 1 on the Free and Personal plans.</li>
<li>An instance is replaced after 8 hours even when busy, and an outbound connection, such as one to your
database, is closed after 60 seconds with no traffic.</li>
<li>It runs in <strong>two regions only</strong>: US East (Ohio) and EU (Frankfurt).</li>
<li>Full-stack frameworks such as Next.js and Astro do not run on it yet.</li>
</ul>
<p>As of November 2026 it is free during Early Access, and Netlify says pricing in credits will be announced before general
availability.</p>
<h2 id="running-express-on-netlify-functions" tabindex="-1"><a class="header-anchor" href="#running-express-on-netlify-functions">Running Express on Netlify Functions</a></h2>
<p>Netlify’s own Express guide uses the <code>serverless-http</code> package to turn the whole app into one
function:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// netlify/functions/api.js
import express, { Router } from &quot;express&quot;;
import serverless from &quot;serverless-http&quot;;

const api = express();
const router = Router();

router.get(&quot;/hello&quot;, (req, res) =&gt; res.send(&quot;Hello World!&quot;));
api.use(&quot;/api/&quot;, router);

export const handler = serverless(api);
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">toml</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-toml"># netlify.toml
[functions]
  external_node_modules = [&quot;express&quot;]
  node_bundler = &quot;esbuild&quot;

[[redirects]]
  force = true
  from = &quot;/api/*&quot;
  status = 200
  to = &quot;/.netlify/functions/api/:splat&quot;
</code></pre>
</div>
<p>Netlify’s guide is clear that every function limit still applies, and that Express should not be
deployed as a background or scheduled function.</p>
<h2 id="what-breaks-when-an-express-app-becomes-a-function" tabindex="-1"><a class="header-anchor" href="#what-breaks-when-an-express-app-becomes-a-function">What breaks when an Express app becomes a function</a></h2>
<p>Your Express code does not change, but the way it runs does. These are the problems people hit most.</p>
<h3 id="memory-does-not-survive-between-requests" tabindex="-1"><a class="header-anchor" href="#memory-does-not-survive-between-requests">Memory does not survive between requests</a></h3>
<p>A function instance can be reused or thrown away at any time. An in-memory session store, a rate-limit
counter in a JavaScript object, or a cache in a global variable will look fine in testing and then lose
data in production. Keep state in a database, a key value store or a cookie.</p>
<h3 id="websockets-have-nowhere-to-live" tabindex="-1"><a class="header-anchor" href="#websockets-have-nowhere-to-live">WebSockets have nowhere to live</a></h3>
<p>A function answers one request and ends, so it cannot hold a socket open. <a href="http://Socket.IO">Socket.IO</a> and the <code>ws</code> module
need a process that stays up. On Netlify, that means Netlify Server, or a separate managed WebSocket
service with a function that only issues tokens, which is the approach Netlify’s own blog describes.</p>
<h3 id="long-jobs-hit-the-wall" tabindex="-1"><a class="header-anchor" href="#long-jobs-hit-the-wall">Long jobs hit the wall</a></h3>
<p>Generating a large report, resizing a batch of images or calling a slow AI API can pass 60 seconds.
Moving it to a background function buys 15 minutes, but you lose the response. Anything longer, or
anything that should keep working through a queue all day, needs a worker process.</p>
<h3 id="database-connections-pile-up" tabindex="-1"><a class="header-anchor" href="#database-connections-pile-up">Database connections pile up</a></h3>
<p>PostgreSQL gives every connection its own process and memory. When many function instances each open
their own connection, the database runs out of slots long before it runs out of capacity. Use a
connection pooler in front of the database if your backend stays on functions.</p>
<h3 id="the-first-request-is-slow" tabindex="-1"><a class="header-anchor" href="#the-first-request-is-slow">The first request is slow</a></h3>
<p>A function that has not run recently has to start before it can answer. A reply on Netlify’s forum to a
user asking for an India region in 2023 said cold starts, not distance, were the cause of the slowness
that user saw. On a backend built from many small functions, this adds up.</p>
<h2 id="databases-for-a-netlify-backend" tabindex="-1"><a class="header-anchor" href="#databases-for-a-netlify-backend">Databases for a Netlify backend</a></h2>
<p>Netlify now has three answers for data:</p>
<ul>
<li><strong>Netlify Database</strong>: managed Postgres, built in. Netlify’s launch post says its partnership
with Neon powers the new databases. It is on credit-based plans only.
The Free plan allows 1 compute unit per database, 48 compute units per billing period and 5 GB of
storage, and the database sleeps after 5 minutes of no queries. Netlify says waking it typically adds
“well under a second, but this is not guaranteed”. Only Pro and Enterprise can keep it always on. Netlify’s docs do
not say which region it runs in.</li>
<li><strong>Netlify Blobs</strong>: a key/value store for files and unstructured data. Netlify describes it as
optimised for frequent reads and infrequent writes, with no concurrency control: the last write to a
key wins. It is not a relational database.</li>
<li><strong>An external database</strong>: any Postgres you can reach over TLS.</li>
</ul>
<p>Whichever you choose, put the database in the same region as the code that queries it. A single API
request often runs several queries one after another, and every one of them pays the round trip.</p>
<h2 id="where-netlify-runs-your-backend-seen-from-india" tabindex="-1"><a class="header-anchor" href="#where-netlify-runs-your-backend-seen-from-india">Where Netlify runs your backend, seen from India</a></h2>
<p>Netlify’s CDN serves static files from many locations, but backend code runs in one region you pick.</p>
<ul>
<li>Functions default to <strong>US East (Ohio)</strong>. Changing the region is a Pro and Enterprise feature.</li>
<li>The self-serve function regions in Asia are <strong>Singapore</strong>, <strong>Tokyo</strong> and <strong>Sydney</strong>. There is no India
region in the list.</li>
<li>Netlify Server runs in <strong>Ohio</strong> or <strong>Frankfurt</strong> only.</li>
</ul>
<p>For an app with users in India, that means every API call travels to another country and back. If the
function also talks to a database in a third place, each query adds another long round trip. Moving the
function to Singapore helps; it does not remove the distance.</p>
<h2 id="the-common-split-frontend-on-netlify-backend-elsewhere" tabindex="-1"><a class="header-anchor" href="#the-common-split-frontend-on-netlify-backend-elsewhere">The common split: frontend on Netlify, backend elsewhere</a></h2>
<p>You can keep what Netlify does best, which is building and serving the frontend, and run the API,
database and workers on a platform built for long-running processes. The frontend calls the API over
HTTPS.</p>
<p><img src="https://media.getvoroa.com/img/ffa2dd4e9f50e7d2.png" alt="The browser loads the frontend from Netlify and calls an API web service in Mumbai, which uses managed PostgreSQL, hands slow jobs to a background worker, and shares the database with a cron job" width="700" loading="eager" decoding="async"></p>
<p>Two small changes make the split work. First, the frontend reads the API address from an environment
variable set in Netlify’s site settings, so you never hard-code it:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// frontend, built with Vite
const API_URL = import.meta.env.VITE_API_URL; // e.g. https://api.example.in

const res = await fetch(`${API_URL}/api/orders`, { credentials: &quot;include&quot; });
</code></pre>
</div>
<p>Second, the API allows the frontend’s exact origin, including <code>https://</code>:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// server.js on the backend
import cors from &quot;cors&quot;;

app.use(cors({
  origin: [&quot;https://example.in&quot;, &quot;https://your-site.netlify.app&quot;],
  credentials: true,
}));
</code></pre>
</div>
<p>If you are weighing a full move instead of a split, the
<a href="/blog/netlify-alternative-india/">Netlify alternatives for Indian teams</a> post compares the options.</p>
<p>If you use cookies across two domains, put the API on a subdomain of your main domain (for example
<code>api.example.in</code>), so the browser treats the cookie as same-site.</p>
<h2 id="running-the-backend-postgres-and-workers-in-mumbai" tabindex="-1"><a class="header-anchor" href="#running-the-backend-postgres-and-workers-in-mumbai">Running the backend, Postgres and workers in Mumbai</a></h2>
<p>This is where Voroa fits. Each Netlify runtime has a direct equivalent that runs as a normal process in
Mumbai:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>On Netlify</th>
<th>On Voroa</th>
<th>What changes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Synchronous function or Express via <code>serverless-http</code></td>
<td>A web service</td>
<td>One long-running server that reads <code>PORT</code>; no per-request wrapper</td>
</tr>
<tr>
<td>Netlify Server WebSockets</td>
<td>WebSockets on the same web service</td>
<td>No fixed maximum connection length; send a ping every 30 seconds</td>
</tr>
<tr>
<td>Background function</td>
<td>A background worker</td>
<td>A process that stays running, so a job is not cut off at 15 minutes</td>
</tr>
<tr>
<td>Scheduled function</td>
<td>A scheduled task or a cron job</td>
<td>Schedules run in IST, not UTC</td>
</tr>
<tr>
<td>Netlify Database</td>
<td>Managed PostgreSQL</td>
<td>Runs in Mumbai, next to the API</td>
</tr>
</tbody>
</table></div>
<h3 id="turn-the-function-back-into-a-server" tabindex="-1"><a class="header-anchor" href="#turn-the-function-back-into-a-server">Turn the function back into a server</a></h3>
<p>Split the Express app from the way it is started. Then the same <code>app</code> can run as a server on Voroa and,
while you migrate, still as a Netlify function:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// app.js - your routes, unchanged
import express from &quot;express&quot;;
export const app = express();
app.use(express.json());
app.get(&quot;/api/health&quot;, (req, res) =&gt; res.json({ ok: true }));
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// server.js - the start command on Voroa: node server.js
import { app } from &quot;./app.js&quot;;

const port = process.env.PORT || 3000;
app.listen(port, &quot;0.0.0.0&quot;, () =&gt; console.log(`listening on ${port}`));
</code></pre>
</div>
<p>Voroa sets <code>PORT</code> for you, so reading it is all the server needs. The steps for a first deploy are in
the <a href="/docs/deploy-nodejs/">Node.js deploy guide</a>, and the <a href="https://nodejs.org/en/docs">Node.js documentation</a>
covers the runtime itself. Express’s own
<a href="https://expressjs.com/en/advanced/best-practice-performance.html">production best practices</a> apply as
they would on any server, such as setting <code>NODE_ENV=production</code>.</p>
<h3 id="add-websockets-to-the-same-service" tabindex="-1"><a class="header-anchor" href="#add-websockets-to-the-same-service">Add WebSockets to the same service</a></h3>
<p>A Voroa web service accepts WebSocket connections on the same port as HTTP. With the <code>ws</code> module:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">import { createServer } from &quot;http&quot;;
import { WebSocketServer } from &quot;ws&quot;;
import { app } from &quot;./app.js&quot;;

const server = createServer(app);
const wss = new WebSocketServer({ server, path: &quot;/ws&quot; });

wss.on(&quot;connection&quot;, (ws) =&gt; {
  ws.on(&quot;message&quot;, (msg) =&gt; ws.send(`echo: ${msg}`));
});

server.listen(process.env.PORT || 3000, &quot;0.0.0.0&quot;);
</code></pre>
</div>
<p>A connection with no traffic for about 100 seconds can be closed on its way through the edge, so send a
ping every 30 seconds and reconnect after a deploy. The <a href="/docs/websockets/">WebSockets guide</a> has the
full client side.</p>
<h3 id="replace-background-functions-with-a-worker" tabindex="-1"><a class="header-anchor" href="#replace-background-functions-with-a-worker">Replace background functions with a worker</a></h3>
<p>A worker has no web address. It runs your start command and keeps running, which suits a job queue.
Here is a minimal one that uses a Postgres table as the queue, so it needs nothing beyond the database:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// worker.js - start command: node worker.js
import pg from &quot;pg&quot;;
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

async function next() {
  const { rows } = await pool.query(`
    UPDATE jobs SET status = 'running'
    WHERE id = (
      SELECT id FROM jobs WHERE status = 'queued'
      ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 1
    )
    RETURNING *`);
  return rows[0];
}

for (;;) {
  const job = await next();
  if (!job) { await new Promise((r) =&gt; setTimeout(r, 2000)); continue; }
  await handle(job); // your slow work, no 15-minute cut-off
  await pool.query(&quot;UPDATE jobs SET status = 'done' WHERE id = $1&quot;, [job.id]);
}
</code></pre>
</div>
<p>The API inserts a row and answers at once; the worker picks it up. <code>SKIP LOCKED</code> lets you run more than
one worker without two of them taking the same job. Voroa’s
<a href="/background-workers/">background workers</a> cost the same as a web service of the same size.</p>
<p>If a job is meant to run and finish on a timetable instead, use a scheduled task or a cron job, not a
worker. A worker that exits gets started again. The guide on
<a href="/docs/worker-or-cron-job/">choosing between a worker and a cron job</a> explains the difference.</p>
<h3 id="put-postgres-next-to-the-api" tabindex="-1"><a class="header-anchor" href="#put-postgres-next-to-the-api">Put Postgres next to the API</a></h3>
<p>Create a <a href="/managed-postgresql-india/">managed PostgreSQL database in India</a> and set its URL as
<code>DATABASE_URL</code> on the web service and the worker. Because the API and the database are both in Mumbai,
a request that runs five queries does not pay five trips abroad. If part of your backend stays on
Netlify Functions for a while, connect those functions through the pooled port when your database
lists one; the <a href="/docs/postgresql-connection-pooling/">connection pooling guide</a> explains which endpoint
to use when.</p>
<h2 id="what-a-mumbai-backend-costs" tabindex="-1"><a class="header-anchor" href="#what-a-mumbai-backend-costs">What a Mumbai backend costs</a></h2>
<p>You can try the whole setup at Rs 0. The free Nano size has 512 MB of memory and 0.1 vCPU, and a free
web service sleeps after 15 minutes without requests. The free tier also includes one managed Postgres
with 1 GB of storage, subject to free capacity.</p>
<p>For production, paid web sizes start at Rs 298 a month plus GST for 1 GB and 1 vCPU, always on. Adding
the DB Starter Postgres (Rs 299) brings an API with its database to Rs 597 per month before GST. You pay in
rupees by UPI, Indian card or netbanking, and get a GST invoice. See <a href="/pricing/">Voroa pricing</a> for every
size.</p>
<h2 id="run-your-netlify-backend-on-voroa" tabindex="-1"><a class="header-anchor" href="#run-your-netlify-backend-on-voroa">Run your Netlify backend on Voroa</a></h2>
<p>Keep the frontend on Netlify if you like it there. Move the API, the database and the workers to Voroa,
where they run as normal processes in Mumbai, deploy from GitHub on every push when auto-deploy is on, and are billed in INR.
See what a <a href="/web-services/">Voroa web service includes</a>, or read the wider
<a href="/blog/backend-api-hosting-india/">backend API hosting guide</a> first.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-netlify-good-for-backend-development" tabindex="-1"><a class="header-anchor" href="#is-netlify-good-for-backend-development">Is Netlify good for backend development?</a></h3>
<p>It is good for small, request-shaped backend work: webhooks, form handlers, and short API routes. It is
a weaker fit for long-running servers, queues and WebSockets, unless you use Netlify Server, which is
still in Early Access.</p>
<h3 id="can-i-run-socketio-on-netlify" tabindex="-1"><a class="header-anchor" href="#can-i-run-socketio-on-netlify">Can I run <a href="http://Socket.IO">Socket.IO</a> on Netlify?</a></h3>
<p>Not on ordinary Netlify Functions, because a function cannot keep a connection open. Netlify Server
supports WebSockets, but an Express app exported as it is answers WebSocket requests with a <code>501</code>; wrap
it in <code>http.createServer()</code> and export the server, or start it with <code>app.listen(process.env.PORT)</code>. Any host
that runs a long-lived Node.js process also works.</p>
<h3 id="does-netlify-have-a-server-in-india" tabindex="-1"><a class="header-anchor" href="#does-netlify-have-a-server-in-india">Does Netlify have a server in India?</a></h3>
<p>Netlify’s documented function regions do not include India; the closest are Singapore and Tokyo.
Netlify Server lists only Ohio and Frankfurt.</p>
<h3 id="can-i-use-netlify-for-the-frontend-and-another-host-for-the-api" tabindex="-1"><a class="header-anchor" href="#can-i-use-netlify-for-the-frontend-and-another-host-for-the-api">Can I use Netlify for the frontend and another host for the API?</a></h3>
<p>Yes. Set the API URL as a build-time environment variable on Netlify, and
allow your Netlify domain in the API’s CORS settings.</p>
<h3 id="do-i-need-to-rewrite-my-express-app-to-leave-netlify-functions" tabindex="-1"><a class="header-anchor" href="#do-i-need-to-rewrite-my-express-app-to-leave-netlify-functions">Do I need to rewrite my Express app to leave Netlify Functions?</a></h3>
<p>No. Remove the <code>serverless-http</code> wrapper, call <code>app.listen(process.env.PORT)</code>, and your routes stay the
same.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Netlify Drop: How It Works and When to Switch to Git Deploys</title>
      <link>https://getvoroa.com/blog/netlify-drop-alternative/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/netlify-drop-alternative/</guid>
      <pubDate>Sat, 14 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Deploys</category>
      <category>GitHub</category>
      <description><![CDATA[Netlify Drop explained: how drag-and-drop deploys work, claiming a site, the size and update limits, and when to move a static site to Git-based deploys.]]></description>
      <content:encoded><![CDATA[<p>Netlify Drop is the page where you drag a folder into the browser and get a live website a few seconds
later. It is how many students and first-time developers in India publish their first site. This guide
explains how it works today, what happens if you never claim the site, where drag-and-drop starts to
hurt, and how to move the same site to Git-based deploys when you are ready, on Netlify or anywhere else.</p>
<p>Netlify facts here come from Netlify’s own documentation and changelog, checked on 14 November 2026.</p>
<h2 id="what-is-netlify-drop" tabindex="-1"><a class="header-anchor" href="#what-is-netlify-drop">What is Netlify Drop?</a></h2>
<p>Drop is Netlify’s drag-and-drop deploy page at <a href="http://app.netlify.com/drop">app.netlify.com/drop</a>. You drop a folder or a zip file of
your site, Netlify uploads it, and you get a URL ending in netlify.app. You do not need a Git repository,
and you do not need an account to start. If you are signed in, Netlify can also run your build for you.</p>
<p>It is old and popular. Netlify’s own history post traces it back to a drag-and-drop tool called
BitBalloon, first committed in 2013, and says more than 60 percent of people who signed up in June 2026
made their first deploy this way.</p>
<h2 id="how-a-drag-and-drop-deploy-works" tabindex="-1"><a class="header-anchor" href="#how-a-drag-and-drop-deploy-works">How a drag-and-drop deploy works</a></h2>
<ol>
<li>Open the Drop page in your browser.</li>
<li>Drag your project folder, or a zip of it, onto the drop zone. The folder should have an <code>index.html</code>
at its top level.</li>
<li>Wait for the upload. Netlify gives you a preview URL ending in netlify.app.</li>
<li>Sign up or log in to claim the site so it stays online.</li>
<li>To update it later, open the project’s dashboard and drop the new folder onto the area under
<strong>Production deploys</strong>. The changes publish straight to production.</li>
</ol>
<h3 id="signed-in-versus-not-signed-in" tabindex="-1"><a class="header-anchor" href="#signed-in-versus-not-signed-in">Signed in versus not signed in</a></h3>
<p>What happens to your files depends on whether you are logged in.</p>
<ul>
<li><strong>Not signed in.</strong> Netlify publishes the files exactly as they are. Drop a finished output folder, such
as <code>dist</code> or <code>build</code>, not your source code. The URL is protected with a temporary password until you
claim it.</li>
<li><strong>Signed in.</strong> You can drop a project that still needs building. Netlify
detects the framework, installs dependencies and runs the build before it publishes. Dropping a
pre-built folder still works.</li>
</ul>
<h2 id="claiming-a-dropped-site-and-what-happens-if-you-do-not" tabindex="-1"><a class="header-anchor" href="#claiming-a-dropped-site-and-what-happens-if-you-do-not">Claiming a dropped site, and what happens if you do not</a></h2>
<p>An unclaimed drop is temporary by design. Netlify’s history post says the claim window was cut from 24
hours to one hour in January 2022, anonymous sites were capped at three per IP address, and unclaimed sites were later
suspended and then deleted. For anonymous deploys from its command-line tool, the documentation states the
window plainly: you can claim the project within one hour. A reply on Netlify’s forum says one
user’s anonymous drop “was automatically deleted because it was not claimed”.</p>
<p>So claim the site the moment it works. Two more things to know:</p>
<ul>
<li><strong>Claimed does not always mean public.</strong> Teams created on or after 28 July 2026 make new projects private
by default, drops included. On the free plan only the team owner can see a private project until you
select <strong>Make public</strong>.</li>
<li><strong>An unclaimed drop can hold your domain.</strong> One forum thread from June 2026 describes a custom domain
that could not be added to a new project because an old, unclaimed anonymous drop still had it. The user had
to ask Netlify support to release it.</li>
</ul>
<h2 id="where-drag-and-drop-deploys-run-out" tabindex="-1"><a class="header-anchor" href="#where-drag-and-drop-deploys-run-out">Where drag-and-drop deploys run out</a></h2>
<p>Drop is excellent for a first deploy. It gets harder the second, tenth and fiftieth time.</p>
<p><strong>Size.</strong> Netlify says uploads under 50 MB work best, and single files over 10 MB may get the deploy stuck.
Your computer also needs free memory of roughly four times the uncompressed project size. For anything
larger, Netlify points you to its command-line tool.</p>
<p><strong>Every update is manual.</strong> You rebuild on your laptop, find the right folder and drag it again. Nothing
records which version of your code is live or who changed it.</p>
<p><strong>One wrong drop replaces a live site.</strong> Updates go straight to production. In a July 2026 forum post, a
beginner developer at a family-run business described dragging a hobby project’s files “directly on top of my live” business
site. Git gives you a commit and a review step before that can happen.</p>
<p><strong>Each update is a production deploy.</strong> On Netlify’s credit-based free plan, every successful production
deploy costs 15 of the month’s 300 credits, and there is no preview step on a drop. Our guide to
<a href="/blog/netlify-free-plan-limits/">Netlify free plan limits and credits</a> shows how fast that adds up.</p>
<p><strong>No collaboration.</strong> A teammate cannot propose a change. They send you files, and you drop them.</p>
<h2 id="when-to-move-from-drag-and-drop-to-git" tabindex="-1"><a class="header-anchor" href="#when-to-move-from-drag-and-drop-to-git">When to move from drag-and-drop to Git</a></h2>
<p>Move when any of these is true:</p>
<ul>
<li>You update the site more than once or twice a month.</li>
<li>More than one person edits it.</li>
<li>It is a client or business site that must not break by accident.</li>
<li>You want to see what changed, and put the last good version back quickly.</li>
<li>You are tired of rebuilding locally before every update.</li>
</ul>
<p>Netlify’s guide on how to deploy a site says the same: Git deploys are “the right choice for any project with a repo
behind it”. If your site is a one-time event page or a quick demo, Drop is still fine.</p>
<p><img src="https://media.getvoroa.com/img/b5ab0a0310b9dd27.png" alt="Diagram: four steps from dropping a folder and claiming it, to pushing the site to GitHub so every push deploys a new release that can be rolled back" width="700" loading="eager" decoding="async"></p>
<h3 id="step-1-put-the-folder-in-a-git-repository" tabindex="-1"><a class="header-anchor" href="#step-1-put-the-folder-in-a-git-repository">Step 1: put the folder in a Git repository</a></h3>
<p>If your project has never been in Git, these commands create a repository and push it to GitHub. Create an
empty repository on GitHub first (GitHub explains how in
<a href="https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository">creating a new repository</a>),
then run:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">cd my-site
git init
git add .
git commit -m &quot;First version of my site&quot;
git branch -M main
git remote add origin https://github.com/your-name/my-site.git
git push -u origin main
</code></pre>
</div>
<p>The <a href="https://git-scm.com/docs/git-init">git init reference</a> covers the options if you need them.</p>
<h3 id="step-2-connect-the-repository-to-your-host" tabindex="-1"><a class="header-anchor" href="#step-2-connect-the-repository-to-your-host">Step 2: connect the repository to your host</a></h3>
<p>On Netlify you can do this from the dropped project itself: go to <strong>Project configuration</strong>, <strong>Developer
settings</strong>, <strong>Continuous deployment</strong>, <strong>Repository</strong>, and choose <strong>Push to new repository</strong>. Netlify
creates the repository on GitHub and pushes your files; that path supports GitHub only. After that, every
push rebuilds and redeploys the site.</p>
<p>On any other host, you create a new site from the repository. How that works end to end, including what
happens between <code>git push</code> and a live URL, is in our guide to
<a href="/blog/deploy-from-github-india/">push-to-deploy from GitHub</a>.</p>
<h2 id="how-static-sites-deploy-on-voroa" tabindex="-1"><a class="header-anchor" href="#how-static-sites-deploy-on-voroa">How static sites deploy on Voroa</a></h2>
<p>Voroa does not have a drag-and-drop or upload deploy. Every Voroa site deploys from a Git repository,
which is the workflow this article recommends moving to anyway. Here is how a static site works:</p>
<ol>
<li>Connect GitHub and pick the repository and branch.</li>
<li>Set the folder to serve: <code>/</code> for plain HTML at the top level, or a committed build folder such as
<code>/dist</code> or <code>/build</code>.</li>
<li>Deploy. You get a live URL with HTTPS.</li>
<li>Turn on <a href="/docs/auto-deploy/">auto-deploy on every push</a>, so each push to that branch publishes a new
release.</li>
</ol>
<p>One difference from a signed-in drop matters: <strong>a Voroa static site does not run a build.</strong> It serves the
files in your repository as they are, just like a drop made without signing in. Commit your built output,
or, if you want the build to run on every push, deploy the same repository as a
<a href="/web-services/">web service</a>, which installs dependencies and builds for you.</p>
<p>If your code is not on GitHub, Voroa can also
<a href="/docs/deploy-from-git-url/">build from any Git URL over https</a>, public or private with an access token.
Those services do not deploy on push by themselves; you deploy from the dashboard or call a deploy hook
from your CI.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What</th>
<th>Drag-and-drop on Netlify</th>
<th>Static site on Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>How you deploy</td>
<td>Drag a folder in the browser</td>
<td>Push to a GitHub branch</td>
</tr>
<tr>
<td>Account needed</td>
<td>Not at first, claim later</td>
<td>Yes</td>
</tr>
<tr>
<td>Build step</td>
<td>Only when signed in</td>
<td>None; commit the output, or use a web service</td>
</tr>
<tr>
<td>Update</td>
<td>Drag the folder again</td>
<td><code>git push</code></td>
</tr>
<tr>
<td>Undo a bad update</td>
<td>Pick an older deploy in the dashboard</td>
<td><a href="/docs/rollbacks/">Roll back to the previous release</a></td>
</tr>
<tr>
<td>Custom domain</td>
<td>Yes, after you claim the site</td>
<td>2 on the Free plan, with HTTPS</td>
</tr>
<tr>
<td>Where it runs</td>
<td>Global CDN</td>
<td>Mumbai (IN)</td>
</tr>
<tr>
<td>Bill when you grow</td>
<td>US dollars</td>
<td>Rupees, with a GST invoice</td>
</tr>
</tbody>
</table></div>
<p>Static hosting on Voroa is free and needs no card. Single-page apps need one note: a static site serves
only files that exist, so a visitor who refreshes on a route such as <code>/dashboard</code> gets a 404. Use hash
routing, or deploy the app as a web service.</p>
<h2 id="publish-your-static-site-from-github" tabindex="-1"><a class="header-anchor" href="#publish-your-static-site-from-github">Publish your static site from GitHub</a></h2>
<p>If you are moving off drag-and-drop, put the folder in a repository, connect it, and every push becomes a
deploy you can roll back. See what <a href="/static-sites/">Voroa static site hosting</a> includes, or follow the
step-by-step guide to <a href="/docs/deploy-static-site/">deploy a static site from a repository</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-netlifys-drag-and-drop-deploy-free" tabindex="-1"><a class="header-anchor" href="#is-netlifys-drag-and-drop-deploy-free">Is Netlify’s drag-and-drop deploy free?</a></h3>
<p>Yes. It works on the free plan. Once the site is claimed, it counts towards the plan’s 300 monthly
credits like any other site, and each update you drop is a production deploy.</p>
<h3 id="can-i-deploy-a-react-or-vite-app-by-dragging-the-folder" tabindex="-1"><a class="header-anchor" href="#can-i-deploy-a-react-or-vite-app-by-dragging-the-folder">Can I deploy a React or Vite app by dragging the folder?</a></h3>
<p>If you are signed in, Netlify can detect the framework and build it. If you are not, drop the built
output folder (<code>dist</code> or <code>build</code>), not the source folder.</p>
<h3 id="why-does-my-dropped-site-ask-for-a-password" tabindex="-1"><a class="header-anchor" href="#why-does-my-dropped-site-ask-for-a-password">Why does my dropped site ask for a password?</a></h3>
<p>Anonymous drops are password-protected until you claim them by signing up or logging in.</p>
<h3 id="can-i-connect-a-custom-domain-to-a-dropped-site" tabindex="-1"><a class="header-anchor" href="#can-i-connect-a-custom-domain-to-a-dropped-site">Can I connect a custom domain to a dropped site?</a></h3>
<p>Yes, after you claim it. If Netlify says another project is already using the domain, an old unclaimed
deploy may still hold it, and you need to ask Netlify support to release it.</p>
<h3 id="does-voroa-have-a-drag-and-drop-deploy" tabindex="-1"><a class="header-anchor" href="#does-voroa-have-a-drag-and-drop-deploy">Does Voroa have a drag-and-drop deploy?</a></h3>
<p>No. Voroa deploys from GitHub, or from any https Git URL. There is no upload page.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Netlify Free Plan Limits: 300 Credits, Pauses and Real Numbers</title>
      <link>https://getvoroa.com/blog/netlify-free-plan-limits/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/netlify-free-plan-limits/</guid>
      <pubDate>Sat, 14 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Free tier</category>
      <category>India</category>
      <description><![CDATA[Netlify free plan limits in plain words: what 300 credits buy, why a whole team pauses at zero, the legacy 100 GB plan, and what it means for Indian developers.]]></description>
      <content:encoded><![CDATA[<p>Netlify free plan limits changed shape in 2025. There is no longer a separate allowance for bandwidth and
build minutes; new accounts get one pool of 300 credits a month, and everything your site does spends from
it. This guide is for developers in India deciding whether a portfolio, a client site or a side project
can live on that pool. You will learn what each action costs, what 300 credits really buy, what happens at
zero, and how the older free plan still on many accounts is different.</p>
<p>Every Netlify figure here comes from Netlify’s own pricing page and documentation, checked on 14 November
2026. Netlify changes plans often, so check your <strong>Usage &amp; billing</strong> page before you rely on a number.</p>
<h2 id="netlify-free-plan-limits-in-one-table" tabindex="-1"><a class="header-anchor" href="#netlify-free-plan-limits-in-one-table">Netlify free plan limits in one table</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What</th>
<th>Netlify Free (accounts created on or after 4 September 2025)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Price</td>
<td>$0, never charged</td>
</tr>
<tr>
<td>Monthly allowance</td>
<td>300 credits, hard limit</td>
</tr>
<tr>
<td>Buying more credits</td>
<td>Not allowed on Free</td>
</tr>
<tr>
<td>Credits roll over</td>
<td>No, they reset each billing cycle</td>
</tr>
<tr>
<td>Production deploy</td>
<td>15 credits each</td>
</tr>
<tr>
<td>Bandwidth</td>
<td>20 credits per GB</td>
</tr>
<tr>
<td>Web requests</td>
<td>2 credits per 10,000</td>
</tr>
<tr>
<td>Functions and other compute</td>
<td>10 credits per GB-hour</td>
</tr>
<tr>
<td>Netlify Database compute</td>
<td>10 credits per compute unit</td>
</tr>
<tr>
<td>Deploy Previews and branch deploys</td>
<td>0 credits</td>
</tr>
<tr>
<td>Failed deploys and rollbacks</td>
<td>0 credits</td>
</tr>
<tr>
<td>Form submissions</td>
<td>Free</td>
</tr>
<tr>
<td>Team seats</td>
<td>1 owner, no extra seats (free Reviewer roles allowed)</td>
</tr>
<tr>
<td>Usage emails</td>
<td>Sent as credit use climbs, including at 50%, 75% and 100%</td>
</tr>
<tr>
<td>At zero credits</td>
<td>Every project on the team pauses until the next cycle</td>
</tr>
</tbody>
</table></div>
<p>Build minutes are not counted at all on the credit plans. A production deploy costs a flat 15 credits
whether the build takes one minute or ten.</p>
<h2 id="what-300-credits-actually-buy" tabindex="-1"><a class="header-anchor" href="#what-300-credits-actually-buy">What 300 credits actually buy</a></h2>
<p>The pool is shared, so the honest way to read it is: if you spent all 300 credits on one thing, how much
of it would you get?</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Spent only on</th>
<th>You get about</th>
</tr>
</thead>
<tbody>
<tr>
<td>Production deploys</td>
<td>20 deploys a month</td>
</tr>
<tr>
<td>Bandwidth</td>
<td>15 GB a month</td>
</tr>
<tr>
<td>Web requests</td>
<td>1.5 million requests a month</td>
</tr>
<tr>
<td>Function compute at the default 1 GB memory</td>
<td>30 hours of run time a month</td>
</tr>
</tbody>
</table></div>
<p>Nobody spends credits on only one thing, so the real numbers are lower on every line.</p>
<h3 id="deploys-are-the-expensive-part-for-a-small-site" tabindex="-1"><a class="header-anchor" href="#deploys-are-the-expensive-part-for-a-small-site">Deploys are the expensive part for a small site</a></h3>
<p>For a low-traffic site, deploys usually cost more than traffic. Ten pushes to your production branch use
150 credits, which is half the month. If you push small fixes straight to production, you can run out
before your visitors do. Pull requests are different: Deploy Previews and branch deploys cost nothing, so
review changes on a preview and merge in batches.</p>
<h3 id="every-file-is-a-request" tabindex="-1"><a class="header-anchor" href="#every-file-is-a-request">Every file is a request</a></h3>
<p>Netlify counts page views, asset requests, API calls and redirects as web requests. A page that loads its
HTML, a stylesheet, a few scripts and a dozen images is around 20 requests, not one. That is still cheap
(2 credits per 10,000), but a busy page with many small files grows the count faster than page views
suggest.</p>
<h3 id="functions-bill-by-memory-and-time" tabindex="-1"><a class="header-anchor" href="#functions-bill-by-memory-and-time">Functions bill by memory and time</a></h3>
<p>Functions, preview servers and the database feature all count as compute. A function runs with 1 GB of
memory by default, so one hour of total run time across all calls is one GB-hour, which is 10 credits.
Edge functions are counted as web requests instead.</p>
<p><img src="https://media.getvoroa.com/img/e03eb398b96a34fd.png" alt="Diagram: one month of 300 Netlify credits - 10 production deploys use 150, 5 GB of bandwidth uses 100, and about 250,000 requests use the last 50, after which every project on the team pauses" width="700" loading="eager" decoding="async"></p>
<h3 id="a-sample-month" tabindex="-1"><a class="header-anchor" href="#a-sample-month">A sample month</a></h3>
<p>Here is a realistic month for a small portfolio or client site:</p>
<ol>
<li>Ten production deploys: 150 credits.</li>
<li>5 GB of images, scripts and pages sent to visitors: 100 credits.</li>
<li>That leaves 50 credits, or about 250,000 requests, for everything else.</li>
</ol>
<p>A site with mostly text and compressed images fits. A site with large photos, a launch-day spike or a
habit of deploying every small change does not.</p>
<h2 id="netlify-database-and-netlify-server-on-the-free-plan" tabindex="-1"><a class="header-anchor" href="#netlify-database-and-netlify-server-on-the-free-plan">Netlify Database and Netlify Server on the free plan</a></h2>
<p>Netlify now sells two backend products, and both can be used on Free.</p>
<h3 id="netlify-database" tabindex="-1"><a class="header-anchor" href="#netlify-database">Netlify Database</a></h3>
<p>Netlify Database is a managed Postgres database, generally available since 28 April 2026. According to its
<a href="https://docs.netlify.com/build/data-and-storage/netlify-database/billing-and-usage/">billing and limits page</a>,
the free plan allows:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Netlify Database on Free</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>Databases</td>
<td>3 per account</td>
</tr>
<tr>
<td>Size</td>
<td>1 compute unit, sleeps after 5 minutes without use</td>
</tr>
<tr>
<td>Compute</td>
<td>48 compute units per database a month</td>
</tr>
<tr>
<td>Storage</td>
<td>5 GB per database (storage is not charged for now)</td>
</tr>
<tr>
<td>Data written</td>
<td>5 GB per database a month</td>
</tr>
<tr>
<td>Bandwidth out</td>
<td>5 GB per database a month</td>
</tr>
<tr>
<td>Backups</td>
<td>Kept 3 days</td>
</tr>
</tbody>
</table></div>
<p>The catch is the cost. Each compute unit costs 10 credits and database bandwidth costs 20 credits per GB,
from the same 300-credit pool your site uses. The 48-unit monthly cap is worth 480 credits, more than the
whole pool, so on Free you run out of credits before you reach the database’s own cap. A database that
wakes often can pause your site.</p>
<h3 id="netlify-server" tabindex="-1"><a class="header-anchor" href="#netlify-server">Netlify Server</a></h3>
<p>Netlify Server runs a long-lived Node.js backend, such as an existing Express, Hono, Fastify or Koa app.
It entered Early Access on 10 November 2026. On Free you get one instance, it scales to zero after 5
minutes without requests, and it is free to use during Early Access; Netlify says it will be charged in
credits once it is generally available. Full-stack frameworks such as Next.js, Astro and Nuxt do not run
on it yet. Because its APIs may change, treat it as something to try, not something to put a client on.</p>
<h2 id="what-happens-when-the-credits-run-out" tabindex="-1"><a class="header-anchor" href="#what-happens-when-the-credits-run-out">What happens when the credits run out</a></h2>
<p>On the free plan there is no overage bill and no way to buy more. Instead, Netlify pauses every project
that belongs to the team, not only the one that used the credits. A paused project stops taking web
requests, form submissions and other traffic, and all deploys stop too, including Deploy Previews.
Visitors see a <strong>Site not available</strong> page.</p>
<p>You have two ways back: upgrade to a paid plan, or wait. At the start of the next billing cycle the
credits reset and, according to Netlify’s documentation, the projects resume on their own.</p>
<h3 id="when-the-pause-does-not-lift-on-its-own" tabindex="-1"><a class="header-anchor" href="#when-the-pause-does-not-lift-on-its-own">When the pause does not lift on its own</a></h3>
<p>Netlify’s community forum has many threads from August and September 2026 where free teams report
staying paused after the reset, with 300 of 300 credits showing and the dashboard saying the team has
exceeded its limit. Others describe a banner saying the team is “running on operational credits”, with
production deploys paused while their live sites keep serving. We did not find official documentation of
either state, and the error some posters quote tells them to contact support. If a client’s site depends on the free plan, plan
for a few days of downtime, not a few hours.</p>
<h2 id="the-older-free-plan-many-accounts-are-still-on" tabindex="-1"><a class="header-anchor" href="#the-older-free-plan-many-accounts-are-still-on">The older free plan many accounts are still on</a></h2>
<p>Accounts created before 4 September 2025 are on a legacy plan, and Netlify does not move them unless the
owner chooses to. The legacy free plan works on separate meters:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Legacy Free</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bandwidth</td>
<td>100 GB a month, hard limit</td>
</tr>
<tr>
<td>Build minutes</td>
<td>300 a month, hard limit</td>
</tr>
<tr>
<td>Serverless functions</td>
<td>125,000 invocations per site a month</td>
</tr>
<tr>
<td>Edge functions</td>
<td>1 million invocations a month</td>
</tr>
<tr>
<td>Form submissions</td>
<td>100 per site a month</td>
</tr>
<tr>
<td>Members and concurrent builds</td>
<td>1 each</td>
</tr>
</tbody>
</table></div>
<p>This explains why articles disagree about “100 GB or 15 GB”. Both are right, for different accounts. If
your old account fits comfortably in 100 GB, think before switching: Netlify’s billing FAQ says moving to
a credit plan is permanent.</p>
<h2 id="can-you-use-the-free-plan-for-client-or-commercial-work" tabindex="-1"><a class="header-anchor" href="#can-you-use-the-free-plan-for-client-or-commercial-work">Can you use the free plan for client or commercial work?</a></h2>
<p>We found no rule against commercial use on Netlify’s free plan in its pricing page, Terms of Use or
Acceptable Use Policy. That is different from Vercel, whose
<a href="https://vercel.com/docs/plans/hobby">Hobby plan</a> is limited to non-commercial, personal use. Netlify’s Acceptable Use Policy does forbid using the platform for things unrelated to
websites, such as general file storage or distribution, and allows suspension for abuse.</p>
<p>The real limit for client work is practical, not legal. The free plan has one seat, so you cannot add a
client or a colleague as a member (free Reviewer roles can view and comment). New teams created since 28
July 2026 also start with projects private by default, so a new project stays private until you select
<strong>Make public</strong>. And one busy client site can pause every other project on your team.</p>
<h2 id="what-this-means-if-you-build-from-india" tabindex="-1"><a class="header-anchor" href="#what-this-means-if-you-build-from-india">What this means if you build from India</a></h2>
<p>Netlify prices are in US dollars, and its billing FAQ lists credit cards, Apple Pay and Google Pay as
payment methods. It does not mention UPI or Indian tax invoices. That does not matter while you stay on
Free, but it matters the day you upgrade: a dollar card charge, a bank’s foreign transaction markup, and
no GST invoice your business can claim input credit on. Our post on
<a href="/blog/netlify-pricing-india/">Netlify pricing in rupees</a> works through the paid plans.</p>
<p>If you are still choosing a free host for a project, our guide to
<a href="/blog/free-web-app-hosting-india/">free web app hosting in India</a> compares the options by what they let
you run, not only by price.</p>
<h2 id="where-voroas-free-tier-is-different" tabindex="-1"><a class="header-anchor" href="#where-voroas-free-tier-is-different">Where Voroa’s free tier is different</a></h2>
<p>Voroa’s free tier is not bigger on every line, so here is the plain comparison, taken from
<a href="/docs/free-instances/">what the Voroa free tier includes</a>:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What</th>
<th>Netlify Free</th>
<th>Voroa Free</th>
</tr>
</thead>
<tbody>
<tr>
<td>How it is measured</td>
<td>One 300-credit pool</td>
<td>Separate limits per resource</td>
</tr>
<tr>
<td>Bandwidth</td>
<td>Up to about 15 GB, less after deploys</td>
<td>5 GB a month</td>
</tr>
<tr>
<td>Deploys</td>
<td>15 credits each</td>
<td>Not charged one by one; builds count as build minutes</td>
</tr>
<tr>
<td>Build minutes</td>
<td>Not metered</td>
<td>500 a month; new builds pause until the 1st when used up</td>
</tr>
<tr>
<td>Sites and services</td>
<td>No project limit listed</td>
<td>Up to 15 per workspace</td>
</tr>
<tr>
<td>Custom domains</td>
<td>Included, with HTTPS</td>
<td>2, with automatic HTTPS</td>
</tr>
<tr>
<td>Region</td>
<td>Global CDN</td>
<td>Mumbai (IN)</td>
</tr>
</tbody>
</table></div>
<p>Two things are similar. Both stop free projects when a monthly limit runs out, and both bring them back
at the next cycle without deleting anything. On Voroa, when a Free workspace has sent its 5 GB, its free
services stop until 12 midnight IST on the 1st, and the owner gets an email listing what stopped.</p>
<p>The difference is what can take your site down. On Netlify, deploying often eats the same credits your
traffic needs. On Voroa, a deploy is not charged on its own: builds count as
build minutes. When a Free workspace uses its 500, new builds pause until the 1st, but sites that are
already live keep serving. Static sites also do not use Voroa’s 750 free running hours, which are for free
web services, workers and databases. When you upgrade, the bill is in rupees, paid by UPI or card, with a
GST invoice; see <a href="/docs/usage-and-billing/">usage and billing on Voroa</a>.</p>
<p>If you need a quick one-off upload rather than a site you keep updating, read our note on
<a href="/blog/netlify-drop-alternative/">Netlify Drop and when to move to Git deploys</a>.</p>
<h2 id="host-a-free-static-site-in-mumbai" tabindex="-1"><a class="header-anchor" href="#host-a-free-static-site-in-mumbai">Host a free static site in Mumbai</a></h2>
<p>Connect a GitHub repository, choose the folder to serve, and your site is live with HTTPS on Voroa’s free
tier, no card needed. Start with <a href="/static-sites/">static site hosting on Voroa</a>, or follow the steps to
<a href="/docs/deploy-static-site/">deploy a static site from GitHub</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="how-many-sites-can-i-host-on-the-netlify-free-plan" tabindex="-1"><a class="header-anchor" href="#how-many-sites-can-i-host-on-the-netlify-free-plan">How many sites can I host on the Netlify free plan?</a></h3>
<p>Netlify’s pricing page does not list a project limit for Free. The limit that matters is the 300 credits,
which every project on the team shares.</p>
<h3 id="do-deploy-previews-use-netlify-credits" tabindex="-1"><a class="header-anchor" href="#do-deploy-previews-use-netlify-credits">Do Deploy Previews use Netlify credits?</a></h3>
<p>No. Deploy Previews and branch deploys cost 0 credits. Only successful production deploys cost 15 credits.
Failed deploys and rollbacks are also free.</p>
<h3 id="does-netlify-delete-my-site-when-the-free-credits-run-out" tabindex="-1"><a class="header-anchor" href="#does-netlify-delete-my-site-when-the-free-credits-run-out">Does Netlify delete my site when the free credits run out?</a></h3>
<p>No. The site is paused and shows “Site not available”. It comes back when the credits reset at the next
billing cycle, or straight away if you upgrade.</p>
<h3 id="is-my-account-on-the-legacy-plan-or-the-credit-plan" tabindex="-1"><a class="header-anchor" href="#is-my-account-on-the-legacy-plan-or-the-credit-plan">Is my account on the legacy plan or the credit plan?</a></h3>
<p>If you created it before 4 September 2025 and never switched, it is on the legacy plan with 100 GB of
bandwidth and 300 build minutes. Your <strong>Usage &amp; billing</strong> page shows which plan you have.</p>
<h3 id="can-i-use-a-custom-domain-on-netlifys-free-plan" tabindex="-1"><a class="header-anchor" href="#can-i-use-a-custom-domain-on-netlifys-free-plan">Can I use a custom domain on Netlify’s free plan?</a></h3>
<p>Yes. Custom domains with SSL are included on Free, according to Netlify’s pricing page. Traffic to the
custom domain spends the same credits.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Netlify Pricing in India 2026: Plans and Credits in INR</title>
      <link>https://getvoroa.com/blog/netlify-pricing-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/netlify-pricing-india/</guid>
      <pubDate>Sat, 14 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Payments</category>
      <description><![CDATA[Netlify pricing for India: Free, Personal and Pro plans, how credits are spent, what they cost in rupees with GST and forex, and three bills worked out.]]></description>
      <content:encoded><![CDATA[<p>Netlify pricing is no longer a list of limits. Since September 2025, every new account gets a monthly pool of
credits, and everything you use - deploys, bandwidth, requests, function time - is paid for out of that pool.</p>
<p>This guide is for Indian developers and small teams who want to know what a Netlify plan really costs in
rupees. It covers each plan, what a credit buys, what happens when credits run out, and the GST and card
charges that sit on top of the dollar price. Then it works three monthly bills through to the amount on your
card statement.</p>
<p>All Netlify figures are from Netlify’s own <a href="https://www.netlify.com/pricing/">pricing page</a> and
<a href="https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/">credit-based billing documentation</a>,
checked on 14 November 2026. Netlify changed its credit rates in April 2026, so re-check before you budget.</p>
<h2 id="netlify-pricing-plans-in-2026-in-dollars-and-rupees" tabindex="-1"><a class="header-anchor" href="#netlify-pricing-plans-in-2026-in-dollars-and-rupees">Netlify pricing plans in 2026, in dollars and rupees</a></h2>
<p>Rupee amounts use Rs 94.20 to the dollar, the RBI reference rate on 13 November 2026. They are before GST
and card charges, which are covered further down.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Plan detail</th>
<th>Free</th>
<th>Personal</th>
<th>Pro</th>
<th>Enterprise</th>
</tr>
</thead>
<tbody>
<tr>
<td>Price</td>
<td>$0</td>
<td>$9 per month</td>
<td>$20 per month</td>
<td>Custom</td>
</tr>
<tr>
<td>In rupees, before tax</td>
<td>Rs 0</td>
<td>Rs 848</td>
<td>Rs 1,884</td>
<td>Custom</td>
</tr>
<tr>
<td>Credits each month</td>
<td>300</td>
<td>1,000</td>
<td>3,000 (higher tiers up to 20,000)</td>
<td>Custom</td>
</tr>
<tr>
<td>Team members</td>
<td>1 owner</td>
<td>1 owner</td>
<td>Unlimited, at no extra cost</td>
<td>Custom</td>
</tr>
<tr>
<td>When credits run out</td>
<td>Sites pause until next cycle</td>
<td>Pause, or auto recharge</td>
<td>Pause, or auto recharge</td>
<td>Custom</td>
</tr>
<tr>
<td>Extra credits</td>
<td>Not available</td>
<td>500 for $5</td>
<td>1,500 for $10</td>
<td>Custom</td>
</tr>
<tr>
<td>Concurrent builds</td>
<td>1</td>
<td>1, then $40 per month each (up to 3)</td>
<td>3, then $40 per month each</td>
<td>Custom</td>
</tr>
<tr>
<td>Private organisation repos</td>
<td>No</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
</tr>
</tbody>
</table></div>
<p>Two things changed on 14 April 2026 that older articles still get wrong. Pro stopped charging $20 per team
member, so a team of five now pays the same $20 as a team of one. And form submissions became free on every
credit plan.</p>
<h2 id="how-netlify-credits-work" tabindex="-1"><a class="header-anchor" href="#how-netlify-credits-work">How Netlify credits work</a></h2>
<p>A credit is Netlify’s unit of usage. Your plan gives you a fixed number at the start of each billing cycle,
and every metered action takes some away. Your billing cycle starts on the day you created the team, not on
the 1st of the month.</p>
<h3 id="what-each-action-costs-in-credits" tabindex="-1"><a class="header-anchor" href="#what-each-action-costs-in-credits">What each action costs in credits</a></h3>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What you use</th>
<th>Credits</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Production deploy</td>
<td>15 per deploy</td>
<td>Deploy previews, branch deploys and failed deploys are free</td>
</tr>
<tr>
<td>Bandwidth</td>
<td>20 per GB</td>
<td>Data sent to visitors</td>
</tr>
<tr>
<td>Web requests</td>
<td>2 per 10,000</td>
<td>Edge function requests count here</td>
</tr>
<tr>
<td>Compute</td>
<td>10 per GB-hour</td>
<td>Serverless functions, preview servers, databases</td>
</tr>
<tr>
<td>AI inference</td>
<td>180 per $1 of model usage</td>
<td>Agent Runners and AI Gateway</td>
</tr>
<tr>
<td>Form submissions</td>
<td>Free</td>
<td>Since April 2026</td>
</tr>
</tbody>
</table></div>
<p>The production deploy line is the one Indian freelancers notice first. Every push to your main branch that
publishes the site costs 15 credits, however small the change. Twenty production deploys use the whole Free
allowance before a single visitor arrives.</p>
<h3 id="what-a-credit-is-worth-in-rupees" tabindex="-1"><a class="header-anchor" href="#what-a-credit-is-worth-in-rupees">What a credit is worth in rupees</a></h3>
<p>Netlify prices extra credits at $10 for 1,500 on Pro, which is about 0.67 US cents a credit. At that rate,
and at Rs 94.20 to the dollar:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What you use</th>
<th>US dollars</th>
<th>Rupees, before tax</th>
</tr>
</thead>
<tbody>
<tr>
<td>One production deploy</td>
<td>$0.10</td>
<td>Rs 9.42</td>
</tr>
<tr>
<td>1 GB of bandwidth</td>
<td>$0.13</td>
<td>Rs 12.56</td>
</tr>
<tr>
<td>1 million web requests</td>
<td>$1.33</td>
<td>Rs 125.60</td>
</tr>
<tr>
<td>1 GB-hour of compute</td>
<td>$0.07</td>
<td>Rs 6.28</td>
</tr>
</tbody>
</table></div>
<p>On Personal, extra credits cost $5 for 500, or 1 cent each, so the same gigabyte of bandwidth costs $0.20
(about Rs 18.84). Credits included in your plan are a little cheaper than bought ones: Pro’s 3,000 credits for
$20 work out at the same 0.67 cents, and Personal’s 1,000 for $9 at 0.9 cents.</p>
<h2 id="what-300-free-credits-actually-cover" tabindex="-1"><a class="header-anchor" href="#what-300-free-credits-actually-cover">What 300 free credits actually cover</a></h2>
<p>The Free plan is $0 with 300 credits a month and a hard limit: you cannot buy more. Here is how quickly a
small site can use them:</p>
<ul>
<li>20 production deploys, and nothing else, or</li>
<li>15 GB of bandwidth, and nothing else, or</li>
<li>a realistic mix: 8 production deploys (120), 6 GB of bandwidth (120) and 300,000 requests (60).</li>
</ul>
<p>That is enough for a portfolio you update once a week. It is not enough for a site that deploys daily or that
gets shared widely. If the Free limit is what you are deciding on, our guide to
<a href="/blog/netlify-free-plan-limits/">Netlify free plan limits</a> goes through it in detail.</p>
<h2 id="what-happens-when-your-credits-run-out" tabindex="-1"><a class="header-anchor" href="#what-happens-when-your-credits-run-out">What happens when your credits run out</a></h2>
<p>When the balance reaches zero, Netlify pauses <strong>every</strong> project on the team, not just the one that used the
credits. Visitors see a “Site not available” page, the projects stop taking requests and form submissions,
and you cannot publish a production deploy. Netlify emails you as credit use climbs, including at 50%, 75% and 100%.</p>
<p>What you can do next depends on the plan:</p>
<ol>
<li><strong>Free:</strong> wait for the next billing cycle, or upgrade.</li>
<li><strong>Personal or Pro, auto recharge off:</strong> buy a credit pack by hand, or wait.</li>
<li><strong>Personal or Pro, auto recharge on:</strong> Netlify buys a pack for you each time the balance runs out and
charges your saved card. Auto recharge is off by default and only the team owner can turn it on.</li>
</ol>
<p>Bought credits do not expire, so a pack bought on the 28th is still there next month. Plan credits reset each
cycle and are lost if unused, except on the larger Pro tiers.</p>
<p>This is a real change from Netlify’s old plans, which billed overage with no cap. In February 2024, a
Starter-plan user reported a bill of about $104,000 after a traffic spike, which Netlify later said it would
forgive. On a credit plan, the worst case is a paused site, not a five-figure invoice.</p>
<h2 id="pro-credit-tiers-and-paid-add-ons" tabindex="-1"><a class="header-anchor" href="#pro-credit-tiers-and-paid-add-ons">Pro credit tiers and paid add-ons</a></h2>
<p>Pro starts at 3,000 credits for $20. A team owner can move to 5,000, 10,000, 15,000 or 20,000 credits a month
for a single flat price. Tiers above 3,000 also carry unused credits forward for one extra billing cycle; the
base tier does not. Netlify shows the price of each tier in the dashboard’s plan selector, which is the place
to check it, because the public page renders it in the browser rather than printing it.</p>
<p>An upgrade takes effect immediately and resets your billing cycle. A downgrade waits for the end of the cycle,
and any unused plan credits, rollover included, are removed when it happens.</p>
<p>Two add-ons are charged in dollars on top of the plan:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Add-on</th>
<th>Price</th>
<th>In rupees, before tax</th>
</tr>
</thead>
<tbody>
<tr>
<td>Extra concurrent build (Personal, Pro)</td>
<td>$40 per month each</td>
<td>Rs 3,768</td>
</tr>
<tr>
<td>Extra preview server (Pro only)</td>
<td>$15 per month each</td>
<td>Rs 1,413</td>
</tr>
</tbody>
</table></div>
<h2 id="netlify-database-and-netlify-server-what-they-cost" tabindex="-1"><a class="header-anchor" href="#netlify-database-and-netlify-server-what-they-cost">Netlify Database and Netlify Server: what they cost</a></h2>
<p>Netlify now sells two backend products, and both draw from the same credit pool. Neither is offered on
legacy plans.</p>
<p><strong>Netlify Database</strong> is managed Postgres, generally available since 28 April 2026. It costs 10 credits per
database compute unit per hour while the database is awake, and 20 credits per GB of data it sends out.
Storage is free for now; Netlify says it will give notice before it starts charging for it. On Free and
Personal, a database sleeps after 5 minutes idle, and only Pro and Enterprise can keep one always on. Free also caps each
database at 48 compute units per cycle and 5 GB of stored data.</p>
<p>An always-on database at the minimum size runs about 720 hours a month, or around 7,200 credits. That is
more than Pro’s base 3,000. On base Pro, the other 4,200 means three 1,500-credit packs: $30, or Rs 2,826
before tax, for the database’s compute alone.</p>
<p><strong>Netlify Server</strong> runs a long-lived Node.js server such as Express, Fastify or Hono, WebSockets included.
It entered Early Access on 10 November 2026 and is free while in Early Access. At general availability Netlify
will charge credits for reserved memory (GB-hours) and active CPU time (vCPU-hours), but it has not
published the rates. A server scales to zero after 5 minutes without requests, and scales up to 3 instances,
or 1 on Free and Personal.</p>
<h2 id="legacy-plans-starter-and-the-old-19-pro" tabindex="-1"><a class="header-anchor" href="#legacy-plans-starter-and-the-old-19-pro">Legacy plans: Starter and the old $19 Pro</a></h2>
<p>Teams created before 4 September 2025 can stay on a legacy plan. These use limits instead of credits:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Plan detail</th>
<th>Legacy Free / Starter</th>
<th>Legacy Pro</th>
</tr>
</thead>
<tbody>
<tr>
<td>Price</td>
<td>$0</td>
<td>$19 per member per month</td>
</tr>
<tr>
<td>Bandwidth</td>
<td>100 GB, then $55 per 100 GB on Starter</td>
<td>1 TB, then $55 per 100 GB</td>
</tr>
<tr>
<td>Build minutes</td>
<td>300, then $7 per 500 on Starter</td>
<td>25,000, then $7 per 500</td>
</tr>
</tbody>
</table></div>
<p>Moving a team from a legacy plan to a credit plan is permanent. Netlify’s billing FAQ says you cannot go back,
and functions lose their free allowance and are metered in GB-hours from the first second. Work out your
credit use from last month’s usage before you switch.</p>
<h2 id="what-a-netlify-bill-costs-on-an-indian-card" tabindex="-1"><a class="header-anchor" href="#what-a-netlify-bill-costs-on-an-indian-card">What a Netlify bill costs on an Indian card</a></h2>
<p>Netlify bills in US dollars and takes cards, Apple Pay and Google Pay. There is no UPI, no netbanking and no
rupee price. Four things turn the dollar amount into what leaves your account.</p>
<p><strong>GST.</strong> Netlify’s Self-Serve Subscription Agreement says its fees do not include taxes and that it may
invoice them. We did not find a Netlify page that says whether it charges Indian GST, so look at your own
invoice. If it adds 18% GST, that is part of the price. If you are a GST-registered business and the invoice
has no GST, you usually pay 18% yourself under reverse charge and claim it back as input tax credit; our guide
to <a href="/docs/reverse-charge-foreign-cloud-hosting/">reverse charge on foreign hosting bills</a> explains the
steps. Check your own case with your accountant.</p>
<p><strong>The exchange rate.</strong> Your bank converts at its own rate on the day of the charge. We use the RBI reference
rate of Rs 94.20 from 13 November 2026.</p>
<p><strong>The card’s forex markup.</strong> Many standard Indian credit cards add 3.5% on foreign-currency spends, plus 18%
GST on that fee, which is about 4.13% in total. Premium cards are often near 2% (2.36% with GST), and a few
banks offer zero-markup cards. The worked bills below use 4.13%.</p>
<p><strong>Recurring payments and declined cards.</strong> Under the RBI’s
<a href="https://www.rbi.org.in/Scripts/BS_ViewMasDirections.aspx?id=13374">Digital Payments - E-mandate Framework, 2026</a>,
an automatic card payment above Rs 15,000 needs your approval each time. Indian users have also reported on
Netlify’s community forum, from 2021 to 2025, that recurring charges on Indian cards were declined, and a
Netlify support reply in December 2024 said it was “unable to collect payment from any Netlify customers
in India”. Netlify’s terms allow it to suspend service when a payment fails, so do not rely on auto recharge
from an Indian card without testing it first.</p>
<h2 id="three-netlify-bills-worked-out-in-rupees" tabindex="-1"><a class="header-anchor" href="#three-netlify-bills-worked-out-in-rupees">Three Netlify bills worked out in rupees</a></h2>
<p><img src="https://media.getvoroa.com/img/e6ef9c31b856fc5c.png" alt="Diagram: a Netlify bill is the plan fee, plus credit packs bought when the monthly credits run out, plus add-ons, then 18% GST and the card forex markup turn the dollar total into rupees" width="700" loading="eager" decoding="async"></p>
<p>The usage below is an assumption for illustration. Your real numbers are on the <strong>Usage &amp; billing</strong> page of
your Netlify team.</p>
<p><strong>Bill 1: a freelancer on Personal.</strong> 30 production deploys (450 credits), 20 GB of bandwidth (400) and 1
million requests (200) come to 1,050 credits, 50 over the allowance. With auto recharge on, Netlify buys one
500-credit pack. Total: $9 + $5 = <strong>$14</strong>. The 450 unused pack credits stay for next month.</p>
<p><strong>Bill 2: a three-person team on Pro.</strong> 60 production deploys (900), 60 GB of bandwidth (1,200), 3 million
requests (600) and 20 GB-hours of functions (200) come to 2,900 credits, inside the 3,000. Total: <strong>$20</strong>.
Before April 2026, the seat charge alone would have made it $60.</p>
<p><strong>Bill 3: a growing product on base Pro.</strong> 150 production deploys (2,250), 250 GB of bandwidth (5,000), 10
million requests (2,000) and 100 GB-hours of functions (1,000) come to 10,250 credits. After the 3,000 included,
auto recharge buys five 1,500-credit packs to cover the other 7,250. Total: $20 + $50 = <strong>$70</strong>. The 250 unused pack
credits carry over to the next month.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Bill</th>
<th>At Rs 94.20</th>
<th>Plus 18% GST</th>
<th>Plus 4.13% card charges</th>
<th>Approx. on your card</th>
</tr>
</thead>
<tbody>
<tr>
<td>$14</td>
<td>Rs 1,319</td>
<td>Rs 1,556</td>
<td>Rs 64</td>
<td>Rs 1,620</td>
</tr>
<tr>
<td>$20</td>
<td>Rs 1,884</td>
<td>Rs 2,223</td>
<td>Rs 92</td>
<td>Rs 2,315</td>
</tr>
<tr>
<td>$70</td>
<td>Rs 6,594</td>
<td>Rs 7,781</td>
<td>Rs 321</td>
<td>Rs 8,102</td>
</tr>
</tbody>
</table></div>
<p>If your invoice carries no Indian GST and you are not registered, leave out the GST column: the $20 Pro bill
then comes to about Rs 1,962.</p>
<p>Bill 3 is where the dollar model hurts most. Bandwidth alone is 5,000 of its 10,250 credits, because each
gigabyte costs 20 credits whether your visitors are in Pune or Paris.</p>
<h2 id="how-voroas-rupee-pricing-compares" tabindex="-1"><a class="header-anchor" href="#how-voroas-rupee-pricing-compares">How Voroa’s rupee pricing compares</a></h2>
<p>Voroa is a hosting platform that runs in Mumbai and bills in rupees, with a GST invoice for every payment and
payment by UPI, Indian card or netbanking. There is no dollar conversion and no forex markup. For the same
kinds of project, the pieces work differently:</p>
<ul>
<li><strong>Static sites are free to host</strong> and are not charged per deploy. Voroa serves the files in your repository
as they are and runs no build, so you commit your built <code>dist</code> or <code>build</code> folder. If you want the build run on
every push, deploy the same repository as a web service instead.</li>
<li><strong>Bandwidth is pooled per workspace.</strong> The Free plan includes 5 GB a month; Growth (Rs 499 per month plus
GST) includes 25 GB; Scale (Rs 1,499 per month plus GST) includes 1,024 GB. Extra bandwidth on a paid plan is
Rs 8 per GB plus GST, billed after the month ends, and only up to the plan’s billing ceiling: on Growth,
nothing above 100 GB a month is charged (on Scale, 10,240 GB). A Free workspace that uses its 5 GB is stopped until the 1st,
much like a Netlify pause; paid plans are never stopped for bandwidth.</li>
<li><strong>Server code runs as an always-on service</strong> at a fixed monthly price, not on credits. The smallest
always-on size is Rs 298 a month plus GST.</li>
</ul>
<p>Take Bill 3’s traffic: 250 GB of bandwidth on Growth is billed only up to the 100 GB ceiling, so it is Rs 499
plus 75 extra GB at Rs 8, which is Rs 1,099, or about Rs 1,297 with GST. On Scale, the 1,024 GB allowance covers it, and Scale plus one Starter service for an API
is Rs 1,797, or about Rs 2,120 with GST.</p>
<p>That is not a like-for-like swap, and the differences matter. Netlify serves your site from a global CDN and
builds it for you; Voroa serves from Mumbai, which suits an Indian audience but is farther from visitors
elsewhere. A Voroa static site does not send unknown paths to <code>index.html</code>, so a single-page app needs hash
routing or a web service. Netlify Forms, Edge Functions and its AI features have no direct equivalent. See the
<a href="/static-sites/">static site hosting page</a> for what is included and <a href="/pricing/">Voroa’s full price list</a> for every
plan. If you are weighing a move, read <a href="/blog/netlify-alternative-india/">when a Netlify alternative makes sense</a>
first.</p>
<h2 id="netlify-pricing-faq" tabindex="-1"><a class="header-anchor" href="#netlify-pricing-faq">Netlify pricing FAQ</a></h2>
<h3 id="can-i-buy-more-credits-on-the-netlify-free-plan" tabindex="-1"><a class="header-anchor" href="#can-i-buy-more-credits-on-the-netlify-free-plan">Can I buy more credits on the Netlify Free plan?</a></h3>
<p>No. Credit packs and auto recharge are only for Personal and Pro. On Free, the 300 credits are a hard limit:
once they are spent, every site on the team pauses until the next cycle unless you upgrade.</p>
<h3 id="is-netlify-pro-20-per-user" tabindex="-1"><a class="header-anchor" href="#is-netlify-pro-20-per-user">Is Netlify Pro $20 per user?</a></h3>
<p>No, not any more. Since 14 April 2026 the credit-based Pro plan is $20 a month for the whole team with
unlimited members. Legacy Pro teams created before September 2025 still pay $19 per member.</p>
<h3 id="does-netlify-charge-gst-in-india" tabindex="-1"><a class="header-anchor" href="#does-netlify-charge-gst-in-india">Does Netlify charge GST in India?</a></h3>
<p>Netlify’s agreement says its prices exclude taxes and that it may invoice them, but it publishes nothing
India-specific. If your invoice has no GST and you are registered, you usually pay it under reverse charge. A
<a href="/docs/gst-invoice-cloud-hosting/">GST invoice for cloud hosting</a> shows what a valid Indian invoice must
contain.</p>
<h3 id="can-i-pay-netlify-with-upi" tabindex="-1"><a class="header-anchor" href="#can-i-pay-netlify-with-upi">Can I pay Netlify with UPI?</a></h3>
<p>No. Netlify accepts credit cards, Apple Pay and Google Pay on self-serve plans, and ACH or wire transfer on
Enterprise. Use a card that is enabled for international and recurring payments.</p>
<h3 id="why-was-my-netlify-site-paused" tabindex="-1"><a class="header-anchor" href="#why-was-my-netlify-site-paused">Why was my Netlify site paused?</a></h3>
<p>Your team’s credit balance reached zero. Check the usage page for the meter that grew - usually bandwidth or
production deploys - then buy a pack, turn on auto recharge, move to a higher tier, or wait for the cycle to reset.</p>
<h2 id="host-a-static-site-in-rupees" tabindex="-1"><a class="header-anchor" href="#host-a-static-site-in-rupees">Host a static site in rupees</a></h2>
<p>You can put a static site on Voroa for free, in Mumbai, and pay in rupees only when you add paid services.
<a href="/docs/deploy-static-site/">Deploy a static site from GitHub</a> in a few minutes, and keep the
<a href="/docs/outbound-bandwidth/">guide to outbound bandwidth</a> handy so you know what counts.</p>
<p><em>Last checked: 14 November 2026, against Netlify’s pricing page, credit-based billing documentation, legacy plan
documentation and Self-Serve Subscription Agreement. Comparing dollar hosts? The same working for Vercel is in our
<a href="/blog/vercel-pricing-india/">Vercel pricing guide for India</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Netlify vs Vercel in India: Next.js, Free Plans, Regions, INR</title>
      <link>https://getvoroa.com/blog/netlify-vs-vercel-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/netlify-vs-vercel-india/</guid>
      <pubDate>Sat, 14 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Next.js</category>
      <description><![CDATA[Netlify vs Vercel for Indian developers: Next.js support, function limits, free plan rules, India regions, team pricing in rupees and GST, side by side.]]></description>
      <content:encoded><![CDATA[<p>Netlify vs Vercel is the first choice most Indian frontend developers face once a project leaves
localhost. Both deploy from Git, put your site on a global CDN and run server code as functions. They
differ in the places that matter here: how well Next.js runs, how long a function may run, whether the
free plan allows a client project, whether code can run in Mumbai, and what a team pays in rupees. Every
Netlify and Vercel fact below comes from their own pricing pages and documentation, read on
14 November 2026.</p>
<h2 id="netlify-vs-vercel-in-one-paragraph" tabindex="-1"><a class="header-anchor" href="#netlify-vs-vercel-in-one-paragraph">Netlify vs Vercel in One Paragraph</a></h2>
<p>Pick <strong>Vercel</strong> if your app is Next.js with server rendering, you want functions in Mumbai, or a
function must run longer than a minute. Pick <strong>Netlify</strong> if you need a free plan with no rule against
commercial work, a team of several people on one flat fee, or Postgres built into the platform.</p>
<p>Both have only just started to run long-lived processes, and both still do it in early form: Netlify
Server is in Early Access and Vercel Daemons are in beta. If an always-on backend is your main need, see
the last section.</p>
<h2 id="nextjs-on-each-platform" tabindex="-1"><a class="header-anchor" href="#nextjs-on-each-platform">Next.js on Each Platform</a></h2>
<p>Vercel builds Next.js. The <a href="https://nextjs.org/docs/app/getting-started/deploying">Next.js deployment docs</a>
list Vercel as a verified adapter, which means it runs the full Next.js test suite before major releases.</p>
<p>Netlify runs Next.js through its own OpenNext adapter, with zero configuration, for Next.js 13.5 and
later. Its docs mark the App Router, ISR and image optimisation as full support, and middleware runs as
Netlify Edge Functions. They also list four gaps:</p>
<ul>
<li>Pages that use the edge runtime for server rendering run on Node.js in your functions region, not at
the edge.</li>
<li>Headers and redirects are applied after middleware, which differs from a standalone Next.js server.</li>
<li>Node.js middleware cannot use C++ addons or the filesystem.</li>
<li>Rewrites in the Next.js config cannot point to static files in the <code>public</code> folder.</li>
</ul>
<p>The Next.js docs say Netlify is working on a verified adapter, and until then its integration is “not
verified by the Next.js team”. For a static Next.js export, a Vite app or Astro, both platforms are
equally at home.</p>
<h2 id="functions-and-edge-code-limits-that-decide-real-apps" tabindex="-1"><a class="header-anchor" href="#functions-and-edge-code-limits-that-decide-real-apps">Functions and Edge Code: Limits That Decide Real Apps</a></h2>
<p>This is where the two differ most for backend work.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Limit</th>
<th>Netlify</th>
<th>Vercel</th>
</tr>
</thead>
<tbody>
<tr>
<td>Normal function, max run time</td>
<td>60 seconds</td>
<td>300 seconds on Hobby, 800 on Pro</td>
</tr>
<tr>
<td>Long jobs</td>
<td>Background functions, 15 minutes</td>
<td>Pro beta up to 1,800 seconds</td>
</tr>
<tr>
<td>Function memory</td>
<td>1 GB; up to 4 GB on credit-based Pro and Enterprise</td>
<td>2 GB on Hobby; up to 4 GB on Pro</td>
</tr>
<tr>
<td>Request payload</td>
<td>6 MB (20 MB streamed)</td>
<td>4.5 MB (streamed responses exempt)</td>
</tr>
<tr>
<td>Middleware and edge code</td>
<td>Edge Functions on Deno, 50 ms CPU per request</td>
<td>Routing Middleware on Node.js or Bun, billed by compute</td>
</tr>
<tr>
<td>Long-lived process</td>
<td>Netlify Server, Early Access</td>
<td>Daemons (background only), beta</td>
</tr>
</tbody>
</table></div>
<p>Netlify’s scheduled functions stop at 30 seconds.</p>
<p>If an API calls a slow payment gateway or generates a PDF, the 60-second cap on Netlify is the one to
check first.</p>
<h2 id="can-your-code-run-in-india" tabindex="-1"><a class="header-anchor" href="#can-your-code-run-in-india">Can Your Code Run in India?</a></h2>
<p>Static files are served from each platform’s CDN. Your server code is a different story.</p>
<ul>
<li><strong>Vercel</strong> lists Mumbai (<code>bom1</code>) among 19 compute regions. Its
<a href="https://vercel.com/docs/regions">regions page</a> says functions default to Washington, D.C. (<code>iad1</code>),
so change the function region to Mumbai yourself. Mumbai rates are slightly higher: $0.14 per hour of
Active CPU and $0.20 per GB of data transfer on Pro.</li>
<li><strong>Netlify</strong> lists 12 self-serve regions in its
<a href="https://docs.netlify.com/build/functions/configuration/">function configuration docs</a>. None is in
India; the nearest are Singapore and Tokyo. Functions default to Ohio, and changing the region needs a
Pro or Enterprise plan. Netlify Server, its new long-running option, offers only Ohio and Frankfurt.</li>
</ul>
<p>If your database sits in India, a Vercel function in Mumbai saves a round trip abroad on every query. A
Netlify function in Singapore cannot avoid it.</p>
<p><img src="https://media.getvoroa.com/img/dcc2f3aafd170c89.png" alt="A request from a user in India: static files served by the CDN, server code running in Mumbai on one platform and in Singapore on the other, both reaching a database in India" width="700" loading="eager" decoding="async"></p>
<h2 id="builds-and-deploy-limits" tabindex="-1"><a class="header-anchor" href="#builds-and-deploy-limits">Builds and Deploy Limits</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Build and deploy</th>
<th>Netlify</th>
<th>Vercel</th>
</tr>
</thead>
<tbody>
<tr>
<td>Build time limit</td>
<td>15 minutes by default, up to 30</td>
<td>45 minutes</td>
</tr>
<tr>
<td>Concurrent builds, free</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>Concurrent builds, Pro</td>
<td>3 included, $40 each extra</td>
<td>On-demand, billed per CPU minute</td>
</tr>
<tr>
<td>Deploy previews</td>
<td>Free on every plan</td>
<td>Included</td>
</tr>
<tr>
<td>Production deploys, free</td>
<td>15 credits each</td>
<td>Up to 100 deployments a day</td>
</tr>
</tbody>
</table></div>
<p>Netlify’s 15-minute figure comes from its support guide, not its docs. On new credit-based plans,
Netlify no longer counts build minutes. It charges 15 credits per successful production deploy, and
deploy previews cost nothing.</p>
<h2 id="free-plan-rules-commercial-use-and-running-out" tabindex="-1"><a class="header-anchor" href="#free-plan-rules-commercial-use-and-running-out">Free Plan Rules: Commercial Use and Running Out</a></h2>
<p>This is the clearest difference.</p>
<p><strong>Vercel Hobby</strong> is free, but Vercel’s terms say you “shall only use the Services under a Hobby plan for
your personal or non-commercial use.” It includes 100 GB of data transfer, 1,000,000 function
invocations and 4 hours of Active CPU a month. Go over and you wait until 30 days have passed. You cannot
buy more on Hobby, and Hobby cannot deploy a private repository owned by a GitHub organisation.</p>
<p><strong>Netlify Free</strong> has no such rule. Netlify’s 2024 Free plan announcement said you can “deploy commercial
projects”, and nothing on the current pricing page restricts it. Vercel’s own comparison page lists free
commercial use as a Netlify strength. Free gives 300 credits a month with a hard limit. When they run
out, Netlify’s docs say every project is paused and visitors see a “Site not available” page until the
cycle resets. Each production deploy costs 15 credits and each GB of bandwidth costs 20, so 20
production deploys use the whole month on their own.</p>
<p>Accounts created before 4 September 2025 stay on Netlify’s legacy plans unless they switch: 100 GB of
bandwidth and 300 build minutes on Free. Our <a href="/blog/netlify-free-plan-limits/">Netlify free plan limits guide</a>
works through the credit maths.</p>
<h2 id="paid-plans-in-rupees" tabindex="-1"><a class="header-anchor" href="#paid-plans-in-rupees">Paid Plans in Rupees</a></h2>
<p>Both price in US dollars. The rupee figures below are list prices only: plan fee converted at
Rs 94.20 to the dollar (the RBI reference rate on 13 November 2026), before any usage over the
allowance, 18% GST and your card’s forex markup. GST and a typical 4.13% card markup alone add about 23%.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>List price, before GST</th>
<th>Netlify</th>
<th>Vercel</th>
</tr>
</thead>
<tbody>
<tr>
<td>Entry paid plan</td>
<td>Personal $9 (Rs 848), 1,000 credits</td>
<td>Pro $20 (Rs 1,884), $20 usage credit</td>
</tr>
<tr>
<td>Team plan</td>
<td>Pro $20 (Rs 1,884), 3,000 credits</td>
<td>Pro, $20 per developer seat</td>
</tr>
<tr>
<td>Team of four developers</td>
<td>$20 (Rs 1,884)</td>
<td>$80 (Rs 7,536)</td>
</tr>
<tr>
<td>Extra usage</td>
<td>1,500 credits for $10 on Pro</td>
<td>Pay as you go after the credit</td>
</tr>
</tbody>
</table></div>
<p>Netlify Pro includes unlimited team members. Vercel charges each developer seat; viewer seats are free.
For a small agency in Pune or Bengaluru, that seat price is often the deciding line. Our
<a href="/blog/netlify-pricing-india/">Netlify pricing guide for India</a> and
<a href="/blog/vercel-pricing-india/">Vercel pricing in rupees</a> work full bills through, with usage, GST and
card charges added. That is why a Netlify Pro bill of $20 lands at about Rs 2,315 on an Indian card there.</p>
<h2 id="billing-gst-and-invoices-from-india" tabindex="-1"><a class="header-anchor" href="#billing-gst-and-invoices-from-india">Billing, GST and Invoices From India</a></h2>
<p>Vercel’s tax page says prices exclude GST and that it began collecting VAT, GST or equivalent from
international customers on 1 April 2026, where it is registered to collect. Its tax-ID guidance names EU
and UK VAT numbers. It does not mention a GSTIN or rupee invoices.</p>
<p>Netlify’s pricing page shows dollars. Its Self-Serve Subscription Agreement says fees exclude taxes and
that Netlify may invoice them, but we could not find an official Netlify page on Indian GST.</p>
<p>For a GST-registered business, a dollar invoice without GST usually means paying 18% yourself under
reverse charge. Our guide to <a href="/docs/reverse-charge-foreign-cloud-hosting/">reverse charge on foreign hosting</a>
explains the steps.</p>
<h2 id="databases-and-storage" tabindex="-1"><a class="header-anchor" href="#databases-and-storage">Databases and Storage</a></h2>
<p>Netlify Database is a managed Postgres built into Netlify, generally available since April 2026 on
credit-based plans. Each deploy preview gets its own database branch. It uses credits for compute and
bandwidth; storage is free for now. Netlify’s docs we read do not say which region the database runs in.</p>
<p>Vercel’s first-party storage on Hobby and Pro is Blob. For Postgres or Redis, its pricing page points to
Marketplace Integrations.</p>
<h2 id="netlify-vs-vercel-at-a-glance" tabindex="-1"><a class="header-anchor" href="#netlify-vs-vercel-at-a-glance">Netlify vs Vercel at a Glance</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Feature</th>
<th>Netlify</th>
<th>Vercel</th>
</tr>
</thead>
<tbody>
<tr>
<td>Next.js</td>
<td>OpenNext adapter, 13.5+, a few gaps</td>
<td>Verified adapter, full support</td>
</tr>
<tr>
<td>Function run time</td>
<td>60 s (background 15 min)</td>
<td>300 s Hobby, 800 s Pro</td>
</tr>
<tr>
<td>India function region</td>
<td>None; Singapore nearest</td>
<td>Mumbai, set by you</td>
</tr>
<tr>
<td>Free plan</td>
<td>No commercial-use restriction, 300 credits</td>
<td>Personal and non-commercial only</td>
</tr>
<tr>
<td>Team of four</td>
<td>$20 a month</td>
<td>$80 a month</td>
</tr>
<tr>
<td>Long-running server</td>
<td>Netlify Server, Early Access, Ohio or Frankfurt</td>
<td>Daemons for background work, beta</td>
</tr>
<tr>
<td>Postgres</td>
<td>Netlify Database, built in (GA)</td>
<td>Marketplace</td>
</tr>
<tr>
<td>Currency</td>
<td>USD</td>
<td>USD, GST added since April 2026</td>
</tr>
</tbody>
</table></div>
<h2 id="when-you-need-neither" tabindex="-1"><a class="header-anchor" href="#when-you-need-neither">When You Need Neither</a></h2>
<p>Most code on both platforms runs as functions that start on a request and stop. That fits a frontend
with a few API routes. A backend that must stay up is a harder fit:</p>
<ul>
<li>an Express or Django API holding database connections open,</li>
<li>a queue worker that runs all day,</li>
<li>a <a href="http://Socket.IO">Socket.IO</a> server, or</li>
<li>Postgres that must sit next to the API in India.</li>
</ul>
<p>Both have begun to close this gap. <strong>Netlify Server</strong>, in Early Access since November 2026, runs any
Node.js server such as Express, Fastify or Hono, and holds WebSocket connections open. It is free during
Early Access and will be charged in credits later. Each instance stops after 5 minutes without requests,
a server scales to 1 instance on Free and Personal plans (3 on others), full-stack frameworks such as
Next.js do not run on it yet, and its regions are Ohio and Frankfurt.</p>
<p><strong>Vercel Daemons</strong>, in beta on all plans, run containerised background processes that pull their own
work and do not receive traffic. On Hobby they stop at 5 Active CPU-hours or 420 GB-hours of memory in a
month.</p>
<p>So you can now run a server on either, but not one that stays up in India. A common split is to keep
the frontend on Netlify or Vercel and run the API, workers and database somewhere built for processes,
near your users and your data.</p>
<h2 id="run-the-backend-on-voroa-in-mumbai" tabindex="-1"><a class="header-anchor" href="#run-the-backend-on-voroa-in-mumbai">Run the Backend on Voroa, in Mumbai</a></h2>
<p>Voroa runs your API as a <a href="/web-services/">long-running web service</a> in Mumbai, next to a
<a href="/managed-postgresql-india/">managed PostgreSQL database in India</a>. A Next.js app runs with
<code>next build</code> and <code>next start</code>, the Node.js server mode the Next.js docs say supports every feature; the <a href="/docs/deploy-nextjs/">Next.js deploy guide</a>
has the settings. Pull requests can get their own
<a href="/docs/pr-preview-environments/">preview environments</a>.</p>
<p>The free Nano size is Rs 0 with 512 MB and 0.1 vCPU shared, and free web services sleep after 15 minutes
without requests; see <a href="/docs/free-instances/">what the free tier includes</a>. Paid web sizes start at
Rs 298 a month plus GST, billed in rupees with a GST invoice, and you can pay by UPI. Compare sizes on the
<a href="/pricing/">Voroa pricing page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-netlify-or-vercel-better-for-a-nextjs-app" tabindex="-1"><a class="header-anchor" href="#is-netlify-or-vercel-better-for-a-nextjs-app">Is Netlify or Vercel better for a Next.js app?</a></h3>
<p>Vercel, for most server-rendered Next.js apps. Next.js lists it as a verified adapter, and its
functions can run in Mumbai. Netlify works well for Next.js 13.5 and later, with the few gaps its docs
list.</p>
<h3 id="can-i-use-vercels-free-plan-for-a-client-project" tabindex="-1"><a class="header-anchor" href="#can-i-use-vercels-free-plan-for-a-client-project">Can I use Vercel’s free plan for a client project?</a></h3>
<p>No. Vercel’s terms limit Hobby to personal or non-commercial use. Netlify’s Free plan has no such
restriction: its 2024 launch post said commercial projects are fine, and the current pricing page does not
limit them. It is capped at 300 credits a month.</p>
<h3 id="does-netlify-have-servers-in-india" tabindex="-1"><a class="header-anchor" href="#does-netlify-have-servers-in-india">Does Netlify have servers in India?</a></h3>
<p>Netlify’s documented function regions do not include India. The nearest are Singapore and Tokyo, and
choosing a region needs a Pro or Enterprise plan. Netlify Server runs only in Ohio or Frankfurt.</p>
<h3 id="do-netlify-and-vercel-charge-gst-to-indian-customers" tabindex="-1"><a class="header-anchor" href="#do-netlify-and-vercel-charge-gst-to-indian-customers">Do Netlify and Vercel charge GST to Indian customers?</a></h3>
<p>Vercel says it collects GST or equivalent from international customers since 1 April 2026, where it is
registered. Netlify’s public pages do not say. Both bill in US dollars.</p>
]]></content:encoded>
    </item>
    <item>
      <title>APScheduler Guide: Python Cron Jobs Tested for Missed Runs</title>
      <link>https://getvoroa.com/blog/apscheduler/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/apscheduler/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>Python</category>
      <description><![CDATA[An APScheduler guide for Python: cron jobs, job stores and overlap, plus our tests of duplicate runs, missed runs after a restart and the replace_existing trap.]]></description>
      <content:encoded><![CDATA[<p>APScheduler is one of the most popular ways to run scheduled jobs inside a Python app: a report every morning,
a cleanup every hour, a sync every five minutes. It works with Flask, Django and FastAPI, and the basics
take a few lines.</p>
<p>This guide covers those basics, then what we saw when we tested restarts, overlap and two processes.
One common setting quietly throws away every missed run.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#a-cron-job-in-five-lines">A cron job in five lines</a></li>
<li><a href="#schedulers-triggers-and-job-stores">Schedulers, triggers and job stores</a></li>
<li><a href="#test-1-two-processes-every-job-twice">Test 1: two processes, every job twice</a></li>
<li><a href="#test-2-overlap-is-blocked-by-default">Test 2: overlap is blocked by default</a></li>
<li><a href="#test-3-what-a-restart-does-to-missed-runs">Test 3: what a restart does to missed runs</a></li>
<li><a href="#apscheduler-schedule-or-system-cron">APScheduler, schedule or system cron?</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="a-cron-job-in-five-lines" tabindex="-1"><a class="header-anchor" href="#a-cron-job-in-five-lines">A cron job in five lines</a></h2>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">pip install &quot;apscheduler&lt;4&quot;
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python">from apscheduler.schedulers.blocking import BlockingScheduler

sched = BlockingScheduler(timezone=&quot;Asia/Kolkata&quot;)

@sched.scheduled_job(&quot;cron&quot;, hour=9, minute=0)
def daily_digest():
    send_digest()          # your code

sched.start()
</code></pre>
</div>
<p>That runs <code>daily_digest</code> at 9:00 am India time every day. Version 3.11 is the current stable release.
Version 4 is still in alpha with a different API, so pin <code>&lt;4</code> for now.</p>
<h2 id="schedulers-triggers-and-job-stores" tabindex="-1"><a class="header-anchor" href="#schedulers-triggers-and-job-stores">Schedulers, triggers and job stores</a></h2>
<p>The library has three parts you choose between:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Part</th>
<th>Common choices</th>
<th>Use</th>
</tr>
</thead>
<tbody>
<tr>
<td>Scheduler</td>
<td><code>BlockingScheduler</code>, <code>BackgroundScheduler</code>, <code>AsyncIOScheduler</code></td>
<td>Blocking for a script that only schedules; Background inside a web app; AsyncIO with FastAPI</td>
</tr>
<tr>
<td>Trigger</td>
<td><code>cron</code>, <code>interval</code>, <code>date</code></td>
<td>Calendar times, fixed gaps, or one run at a set time</td>
</tr>
<tr>
<td>Job store</td>
<td>Memory (default), SQLAlchemy, Redis</td>
<td>Whether jobs survive a restart</td>
</tr>
</tbody>
</table></div>
<p>A cron trigger takes the same fields as a cron line, by name: <code>minute=&quot;*/5&quot;</code> for every five minutes,
<code>day_of_week=&quot;mon-fri&quot;, hour=9</code> for weekday mornings. Or build one from a crontab string with
<code>CronTrigger.from_crontab(&quot;0 9 * * 1-5&quot;)</code>.</p>
<h2 id="test-1-two-processes-every-job-twice" tabindex="-1"><a class="header-anchor" href="#test-1-two-processes-every-job-twice">Test 1: two processes, every job twice</a></h2>
<p><strong>Setup for all tests:</strong> version 3.11.3 on Python 3.14, a cron trigger firing every second so
results appear quickly, and Redis 8.10.3 to count runs. Measured 7 November 2026 on a laptop.</p>
<p>A web app usually runs as several worker processes. Gunicorn with four workers loads your app four
times (without <code>--preload</code>). If the scheduler starts at import time, there are now four schedulers.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setup (20 seconds)</th>
<th>Runs</th>
<th>Seconds with more than one run</th>
</tr>
</thead>
<tbody>
<tr>
<td>One process</td>
<td>20</td>
<td>0</td>
</tr>
<tr>
<td>Two processes</td>
<td><strong>40</strong></td>
<td><strong>20 of 20</strong></td>
</tr>
</tbody>
</table></div>
<p><img src="https://media.getvoroa.com/img/c666a2c9a291a225.webp" alt="Diagram: one process runs each scheduled time once; two processes run it twice; Gunicorn with four workers would start four schedulers" width="700" loading="eager" decoding="async"></p>
<p>Every job ran twice. With four Gunicorn workers it would run four times. Two fixes work:</p>
<ul>
<li><strong>Run the scheduler in one dedicated process</strong>, not inside every web worker.</li>
<li><strong>Or take a lock</strong> before the work, for example a Redis key set with <a href="https://redis.io/docs/latest/commands/set/"><code>NX</code></a>, so only one process wins
each run.</li>
</ul>
<h2 id="test-2-overlap-is-blocked-by-default" tabindex="-1"><a class="header-anchor" href="#test-2-overlap-is-blocked-by-default">Test 2: overlap is blocked by default</a></h2>
<p>A job that takes 2.5 seconds, scheduled every second. This is where it differs from many libraries.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setup (15 seconds)</th>
<th>Runs</th>
<th>Most running at once</th>
</tr>
</thead>
<tbody>
<tr>
<td>Default (<code>max_instances=1</code>)</td>
<td>5</td>
<td>1</td>
</tr>
<tr>
<td><code>max_instances=3</code></td>
<td>15</td>
<td>3</td>
</tr>
</tbody>
</table></div>
<p>By default, the scheduler will not start a job while the previous run is still going. It skips that run
and logs a warning that the maximum number of running instances was reached.</p>
<p>That is usually the safe choice. Raise <code>max_instances</code> only if the job is safe to run in parallel.</p>
<h2 id="test-3-what-a-restart-does-to-missed-runs" tabindex="-1"><a class="header-anchor" href="#test-3-what-a-restart-does-to-missed-runs">Test 3: what a restart does to missed runs</a></h2>
<p>We stopped the scheduler for about 5 seconds, as a deploy would, and started it again. Then we tried
the settings people usually recommend.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Job store and settings</th>
<th>Runs (20 s)</th>
<th>Missed runs</th>
</tr>
</thead>
<tbody>
<tr>
<td>Memory store (default)</td>
<td>14</td>
<td>Lost</td>
</tr>
<tr>
<td>SQLite store, <code>add_job(..., replace_existing=True)</code></td>
<td>14</td>
<td><strong>Lost</strong></td>
</tr>
<tr>
<td>SQLite store, job kept, <code>coalesce=False</code></td>
<td>20</td>
<td>Replayed: 7 runs at once on restart</td>
</tr>
<tr>
<td>SQLite store, job kept, <code>coalesce=True</code></td>
<td>14</td>
<td>No burst; consistent with one merged catch-up run</td>
</tr>
</tbody>
</table></div>
<p>The memory store forgets everything on restart, which most people expect.</p>
<p>The second row is the trap. A persistent store is meant to remember missed runs. But the common pattern
<code>add_job(..., id=&quot;x&quot;, replace_existing=True)</code> at start-up overwrites the stored job with a fresh one, so
its missed runs are thrown away. It looked identical to the memory store.</p>
<p>Missed runs only came back when the code left the stored job alone:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python">import time
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore

sched = BackgroundScheduler(
    jobstores={&quot;default&quot;: SQLAlchemyJobStore(url=&quot;sqlite:///jobs.sqlite&quot;)},
    timezone=&quot;Asia/Kolkata&quot;,
)
sched.start(paused=True)                 # load stored jobs without running them yet
if sched.get_job(&quot;digest&quot;) is None:      # first start only
    sched.add_job(daily_digest, &quot;cron&quot;, hour=9, id=&quot;digest&quot;,
                  misfire_grace_time=3600, coalesce=True)
sched.resume()                           # a stored job keeps its missed runs
while True:
    time.sleep(60)                       # keep the process alive
</code></pre>
</div>
<p>We ran this pattern through three restarts in a row: no error, and the stored job carried on each time.
The paused start needs <code>BackgroundScheduler</code>; with <code>BlockingScheduler</code>, <code>start()</code> never returns.</p>
<p>Then choose what a catch-up means. With <code>coalesce=False</code>, every missed run fires at once on restart;
we saw a burst of 7.</p>
<p>With <code>coalesce=True</code>, the documentation says they collapse into one run, which is
right for most reports and syncs. On a job firing every second our test could not tell that merged
run apart from a normal one, but there was no burst. <code>misfire_grace_time</code> sets how late a run may be and still count.</p>
<p><strong>Timing:</strong> runs fired a median of 8 to 15 ms after the scheduled second with the memory store, and
33 to 50 ms with the SQLite store.</p>
<h2 id="apscheduler-schedule-or-system-cron" tabindex="-1"><a class="header-anchor" href="#apscheduler-schedule-or-system-cron">APScheduler, schedule or system cron?</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>This library</th>
<th><code>schedule</code> library</th>
<th>System cron or a platform cron job</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cron-style expressions</td>
<td>Yes</td>
<td>No, readable intervals only</td>
<td>Yes</td>
</tr>
<tr>
<td>Survives restarts</td>
<td>With a persistent store</td>
<td>No</td>
<td>Yes, runs from outside your app</td>
</tr>
<tr>
<td>Duplicate runs with many web workers</td>
<td>Yes, unless you isolate it</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Overlap protection</td>
<td>Built in</td>
<td>Manual</td>
<td>Manual</td>
</tr>
<tr>
<td>Best for</td>
<td>Schedules inside a long-running app</td>
<td>Small scripts</td>
<td>Jobs that must run on time regardless of the app</td>
</tr>
</tbody>
</table></div>
<p>For queued work rather than timed work, a job queue fits better. Our
<a href="/blog/celery-redis/">Celery and Redis guide</a> covers the Python option, including Celery beat for
periodic tasks.</p>
<h2 id="run-python-scheduled-jobs-on-voroa" tabindex="-1"><a class="header-anchor" href="#run-python-scheduled-jobs-on-voroa">Run Python scheduled jobs on Voroa</a></h2>
<p>Two ways, depending on your code. If your job is a script that runs and ends, create a <strong>Cron Job</strong> from
your repository with a command such as <code>python digest.py</code> and a schedule. Schedules run in India
Standard Time.</p>
<p>If the scheduler lives inside a long-running process, deploy that process as a
<a href="/background-workers/">background worker</a>, a single instance kept running, so the scheduler is never
duplicated by web traffic.</p>
<p>Either way, keep jobs safe to run twice. Setup is in
<a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a> and
<a href="/docs/worker-or-cron-job/">worker or cron job</a>. The <a href="/blog/cron-job-in-node-js/">Node.js version of these tests</a>
found the same duplicate-run problem.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>What is it used for?</strong>
Running Python functions on a schedule inside your app: at set times (cron), at fixed intervals, or once
at a given moment.</p>
<p><strong>Why does my scheduled job run twice?</strong>
The scheduler started in more than one process, often one per Gunicorn or Uvicorn worker. Run it in a
single process or use a lock.</p>
<p><strong>Does it run missed jobs after a restart?</strong>
Only with a persistent job store, and only if you do not replace the stored job at start-up. In our
test, <code>replace_existing=True</code> threw the missed runs away.</p>
<p><strong>What does coalesce do?</strong>
With <code>coalesce=True</code>, several missed runs of the same job collapse into one when the scheduler catches
up. With <code>False</code>, each missed run fires separately.</p>
<p><strong>How do I stop overlapping runs?</strong>
You usually do not need to: <code>max_instances</code> defaults to 1, so it skips a run while the last
one is still going.</p>
<p><strong>Should I use version 4?</strong>
Not yet for production. It is still an alpha with a different API; 3.11 is the stable line.</p>
<p><em>Tested 7 November 2026 with version 3.11.3 on Python 3.14. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Background Jobs Explained: Queues, Workers and Retries</title>
      <link>https://getvoroa.com/blog/background-jobs/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/background-jobs/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Reliability</category>
      <description><![CDATA[What background jobs are, when to use them, how a queue and worker fit together, why a job can run twice, and which tool fits Node.js, Python or Postgres.]]></description>
      <content:encoded><![CDATA[<p>Background jobs are pieces of work your app does outside the web request: sending an email, resizing an
image, generating a PDF report. The request saves a small note saying “do this”, answers the user at once,
and a separate process does the work a moment later.</p>
<p>This guide explains how that works, when it is worth it, and the one rule most teams learn the hard way:
a job can run more than once. We measured both the speed-up and the double run with a real queue.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#why-move-work-out-of-the-request">Why move work out of the request?</a></li>
<li><a href="#the-three-parts-producer-queue-worker">The three parts: producer, queue, worker</a></li>
<li><a href="#triggered-jobs-and-scheduled-jobs">Triggered jobs and scheduled jobs</a></li>
<li><a href="#a-crashed-worker-means-a-second-run">A crashed worker means a second run</a></li>
<li><a href="#rules-for-jobs-that-survive-production">Rules for jobs that survive production</a></li>
<li><a href="#choosing-a-job-queue">Choosing a job queue</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="why-move-work-out-of-the-request" tabindex="-1"><a class="header-anchor" href="#why-move-work-out-of-the-request">Why move work out of the request?</a></h2>
<p>Users wait for every millisecond of a web request. If signing up sends a welcome email through a provider
that takes 2 seconds, the sign-up page takes 2 seconds too. If the provider is down, sign-up fails.</p>
<p>Put the email on a queue instead and the request only waits for the enqueue. With BullMQ 6.7 on a local
Redis, adding a job took <strong>0.65 ms</strong> at the median and 1.44 ms at the 99th percentile, over 200 jobs. Over
a network, add one round trip.</p>
<p>Use a background job when the work is slow, can fail and be retried, or the user does not need the result
in the same response.</p>
<h2 id="the-three-parts-producer-queue-worker" tabindex="-1"><a class="header-anchor" href="#the-three-parts-producer-queue-worker">The three parts: producer, queue, worker</a></h2>
<p><img src="https://media.getvoroa.com/img/bc6a28590ef6e01e.webp" alt="Diagram: a web request (the producer) adds a job to a queue in about a millisecond and replies to the user; a separate worker process takes the job from the queue, does the slow work, and marks it done or retries it" width="700" loading="eager" decoding="async"></p>
<ul>
<li><strong>Producer</strong>: your web app. It adds a job with the data needed, such as a user ID, not the whole user.</li>
<li><strong>Queue</strong>: where jobs wait. Usually Redis or a Postgres table, so jobs survive a restart of your app.</li>
<li><strong>Worker</strong>: a separate process that takes jobs one by one, runs them, and marks each done or failed.</li>
</ul>
<p>The worker is a long-running program with no web address. It is deployed separately from the web app, so a
slow job never blocks a page, and you can run more workers when the queue grows.</p>
<h2 id="triggered-jobs-and-scheduled-jobs" tabindex="-1"><a class="header-anchor" href="#triggered-jobs-and-scheduled-jobs">Triggered jobs and scheduled jobs</a></h2>
<p>Jobs start in two ways:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Type</th>
<th>Started by</th>
<th>Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td>Triggered</td>
<td>Something a user or system did</td>
<td>Welcome email, image resize, webhook processing</td>
</tr>
<tr>
<td>Scheduled</td>
<td>The clock</td>
<td>Nightly report, hourly cleanup, monthly invoices</td>
</tr>
</tbody>
</table></div>
<p>Triggered jobs need a queue and a worker. Scheduled jobs can be a plain cron job that starts, works and
exits; our guide to <a href="/blog/what-is-a-cron-job/">what a cron job is</a> covers that side. Many queues also
accept delayed and repeating jobs, so one worker can handle both.</p>
<h2 id="a-crashed-worker-means-a-second-run" tabindex="-1"><a class="header-anchor" href="#a-crashed-worker-means-a-second-run">A crashed worker means a second run</a></h2>
<p>We started a job that takes 10 seconds, then killed the worker process 3 seconds in, the way an
out-of-memory kill or a deploy would, and started a second worker right after the kill.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Time</th>
<th>Event</th>
</tr>
</thead>
<tbody>
<tr>
<td>0.8 s</td>
<td>Worker A starts the job (attempt 1)</td>
</tr>
<tr>
<td>3.5 s</td>
<td>Worker A is killed mid-job</td>
</tr>
<tr>
<td>64.5 s</td>
<td>Worker B starts the <strong>same job again</strong> (attempt 2, marked stalled once)</td>
</tr>
<tr>
<td>74.5 s</td>
<td>Worker B completes it</td>
</tr>
</tbody>
</table></div>
<p>BullMQ noticed the dead worker’s lock had expired and handed the job out again, about a minute later with
default settings. That is the right behaviour, because the work was never finished. But the first 3
seconds of the job had already happened.</p>
<p>If those 3 seconds sent an email or charged a card, the customer now gets it twice. Every queue that
retries works this way. It is called <strong>at-least-once</strong> delivery, and it is why the first rule below matters.</p>
<h2 id="rules-for-jobs-that-survive-production" tabindex="-1"><a class="header-anchor" href="#rules-for-jobs-that-survive-production">Rules for jobs that survive production</a></h2>
<p><strong>1. Make jobs idempotent.</strong> Running a job twice must do no extra harm. Record that the email was sent, or
use a unique key with your payment provider, and check it before acting.</p>
<p><strong>2. Pass IDs, not objects.</strong> Put <code>userId: 42</code> in the job, and load fresh data when the job runs. The user
may have changed their email since the job was queued.</p>
<p><strong>3. Retry with backoff, then stop.</strong> Retry a failed job a few times with growing delays, then move it to a
failed list a person can look at. Retrying forever hides the problem.</p>
<p><strong>4. Keep jobs short.</strong> One long job holds a worker and is the most likely to be cut off by a deploy.
Split a big import into many small jobs.</p>
<p><strong>5. Watch the queue length.</strong> A queue that keeps growing means workers cannot keep up. That is your signal
to add workers or speed the job up.</p>
<h2 id="choosing-a-job-queue" tabindex="-1"><a class="header-anchor" href="#choosing-a-job-queue">Choosing a job queue</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Language</th>
<th>Common choice</th>
<th>Backed by</th>
</tr>
</thead>
<tbody>
<tr>
<td>Node.js</td>
<td>BullMQ</td>
<td>Redis</td>
</tr>
<tr>
<td>Python</td>
<td>Celery, Dramatiq (Redis or RabbitMQ); RQ (Redis)</td>
<td>Redis or RabbitMQ</td>
</tr>
<tr>
<td>Ruby</td>
<td>Sidekiq</td>
<td>Redis</td>
</tr>
<tr>
<td>Already on Postgres</td>
<td>pg-boss (Node), River (Go), Solid Queue (Rails)</td>
<td>Postgres</td>
</tr>
</tbody>
</table></div>
<p>Redis-backed queues are the most common and fast; Redis
<a href="https://redis.io/docs/latest/develop/data-types/lists/">lists</a> are the classic building block. A Postgres
queue means one less service: workers take jobs with <code>FOR UPDATE SKIP LOCKED</code>, described in the
<a href="https://www.postgresql.org/docs/current/sql-select.html">PostgreSQL SELECT documentation</a>, so two workers
never take the same row.</p>
<p>We tested the two most common in depth: <a href="/blog/bullmq-redis-job-queue/">BullMQ with Redis</a> and
<a href="/blog/celery-redis/">Celery with Redis</a>, including how many Redis commands an idle worker sends.</p>
<h2 id="running-background-jobs-on-voroa" tabindex="-1"><a class="header-anchor" href="#running-background-jobs-on-voroa">Running background jobs on Voroa</a></h2>
<p>On Voroa, a worker is a <strong>Background Worker</strong>: built from your GitHub repository, with no web address,
restarted if it exits. Run your web app as a web service, the worker beside it, and a
<a href="/managed-redis-india/">managed Redis</a> or Postgres database as the queue, all in Mumbai.</p>
<p>A program that finishes and exits belongs in a Cron Job instead, or Voroa will keep restarting it. The
guide <a href="/docs/worker-or-cron-job/">worker or cron job</a> helps you pick, and
<a href="/docs/deploy-background-worker/">deploying a background worker</a> has the steps. Plans and sizes are on the
<a href="/background-workers/">background workers page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>What is a background job?</strong>
Work your app does outside the web request, in a separate process, such as sending email or generating a
report. The user gets a response without waiting for it.</p>
<p><strong>What is the difference between a background job and a cron job?</strong>
A background job is usually triggered by an event and picked up by a worker. A cron job is started by the
clock. Many apps use both.</p>
<p><strong>Can a background job run twice?</strong>
Yes. If a worker crashes mid-job, the queue hands the job out again. In our test the second run started
about a minute after the crash. Make jobs safe to repeat.</p>
<p><strong>Do I need Redis for background jobs?</strong>
No. Redis is the most common queue, but a Postgres table with <code>SKIP LOCKED</code> works well at small and
medium scale.</p>
<p><strong>How many workers do I need?</strong>
Start with one. Add more when the queue length keeps growing or jobs wait too long to start.</p>
<p><em>Measured with BullMQ 6.7.1 on a local Redis on 7 November 2026. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>BullMQ Guide: Redis Job Queues in Node.js, Measured</title>
      <link>https://getvoroa.com/blog/bullmq-redis-job-queue/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/bullmq-redis-job-queue/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>Node.js</category>
      <description><![CDATA[A practical BullMQ guide: set up a Redis job queue in Node.js, what each job and idle worker costs in Redis commands and memory, and production settings.]]></description>
      <content:encoded><![CDATA[<p>BullMQ is one of the most widely used job queues for Node.js: you push work onto a queue in Redis, and a
separate worker process picks it up and runs it. This guide is for developers adding background jobs
to a Node.js app, such as sending email, resizing images or calling a slow API. Every tutorial shows
the same ten lines of code. This one also shows what the queue costs your Redis, because we measured
it: how many commands each job uses, how many an idle worker sends, and how much memory finished jobs
keep. Those numbers decide which Redis you can run it on.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#when-a-nodejs-app-needs-a-job-queue">When a Node.js app needs a job queue</a></li>
<li><a href="#a-working-queue-in-two-files">A working queue in two files</a></li>
<li><a href="#what-each-job-costs-in-redis-measured">What each job costs in Redis, measured</a></li>
<li><a href="#the-idle-worker-that-is-never-idle">The idle worker that is never idle</a></li>
<li><a href="#finished-jobs-fill-your-memory">Finished jobs fill your memory</a></li>
<li><a href="#concurrency-how-many-jobs-at-once">Concurrency: how many jobs at once</a></li>
<li><a href="#choosing-a-redis-for-your-queue">Choosing a Redis for your queue</a></li>
<li><a href="#production-checklist">Production checklist</a></li>
<li><a href="#bull-rabbitmq-or-something-else">Bull, RabbitMQ or something else?</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="when-a-nodejs-app-needs-a-job-queue" tabindex="-1"><a class="header-anchor" href="#when-a-nodejs-app-needs-a-job-queue">When a Node.js app needs a job queue</a></h2>
<p>A web request should answer in well under a second. Some work takes longer than that or can fail and
need a retry: sending an email through a provider, generating a PDF invoice, resizing an upload,
calling a payment or AI API, or sending a webhook to a customer. Doing that inside the request makes
the user wait and loses the work if the server restarts.</p>
<p>A queue splits it in two. The web server adds a small job record to Redis and answers at once. A
worker process takes jobs off the queue, runs them, retries failures with a delay, and records the
result. If the worker restarts, unfinished jobs are still in Redis.</p>
<p><img src="https://media.getvoroa.com/img/c9fa72ccd10e81f1.webp" alt="Diagram: the web service adds a job to a queue in Redis and answers the user at once; a background worker takes the job, runs it, retries on failure and records the result" width="700" loading="eager" decoding="async"></p>
<h2 id="a-working-queue-in-two-files" tabindex="-1"><a class="header-anchor" href="#a-working-queue-in-two-files">A working queue in two files</a></h2>
<p>Install the package (it brings its own Redis client, <code>ioredis</code>):</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">npm install bullmq
</code></pre>
</div>
<p><strong>The shared setup</strong>, one file both processes import:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// queue.js
import { Queue, Worker } from 'bullmq';
import IORedis from 'ioredis';

// Web side: fail fast if Redis is down, so a request never hangs waiting for it.
// Trade-off: an add() before the first connect completes throws instead of waiting.
const queueConnection = new IORedis(process.env.REDIS_URL, { enableOfflineQueue: false });
export const emailQueue = new Queue('email', { connection: queueConnection });

// Worker side: keep retrying through a Redis restart instead of crashing
export const workerConnection = () =&gt;
  new IORedis(process.env.REDIS_URL, { maxRetriesPerRequest: null });
export { Worker };
</code></pre>
</div>
<p><strong>The producer</strong>, inside a route handler in your web app. It adds a job and returns:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">import { emailQueue } from './queue.js';

await emailQueue.add('welcome', { userId: 42 }, {
  attempts: 5,
  backoff: { type: 'exponential', delay: 2000 },
  removeOnComplete: { count: 1000 },
  removeOnFail: { age: 7 * 24 * 3600 },
});
</code></pre>
</div>
<p><strong>The worker</strong>, a separate long-running process:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// worker.js
import { Worker, workerConnection } from './queue.js';

const worker = new Worker('email', async (job) =&gt; {
  await sendWelcomeEmail(job.data.userId);   // your code
}, { connection: workerConnection(), concurrency: 10 });

worker.on('failed', (job, err) =&gt; console.error(job?.id, err.message));

process.on('SIGTERM', async () =&gt; { await worker.close(); process.exit(0); });
</code></pre>
</div>
<p>That is the whole pattern. The options in the producer are the ones that matter in production, and
the sections below explain why.</p>
<h2 id="what-each-job-costs-in-redis-measured" tabindex="-1"><a class="header-anchor" href="#what-each-job-costs-in-redis-measured">What each job costs in Redis, measured</a></h2>
<p>We ran version 6.7.1 of the library from Node.js 24 on a Windows laptop against Redis 8.10.3 in a Linux
virtual machine (WSL2) on the same laptop, and counted every command the server executed, using
Redis’s own command statistics and <code>MONITOR</code>. Measured 7 November 2026.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What we measured</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Commands per job (add, process, complete), with <code>removeOnComplete: true</code></td>
<td>32 (33 with defaults)</td>
</tr>
<tr>
<td>…of which your app sends over the network</td>
<td>2</td>
</tr>
<tr>
<td>…of which run inside the library’s Lua scripts</td>
<td>30</td>
</tr>
<tr>
<td>Most common commands</td>
<td><code>XADD</code> (4 per job, the event stream), <code>HMGET</code>, <code>HGET</code>, <code>HMSET</code>, <code>ZADD</code></td>
</tr>
</tbody>
</table></div>
<p>Two round trips per job is efficient: the library does its bookkeeping inside Lua scripts on the
server, so a job costs your app very little network time.</p>
<p>The 32 is what matters if your Redis host charges per command. Upstash, for example, bills a script as
one command plus one for every Redis call the script makes. At about 32 commands per job, the
500,000 commands in Upstash’s free tier cover roughly 15,600 jobs a month, before counting the idle
cost below.</p>
<h2 id="the-idle-worker-that-is-never-idle" tabindex="-1"><a class="header-anchor" href="#the-idle-worker-that-is-never-idle">The idle worker that is never idle</a></h2>
<p>A worker with nothing to do still talks to Redis. It waits for new jobs with a blocking command that
times out every few seconds, and it checks for stalled jobs on a timer.</p>
<p>We started one worker on an empty queue and counted for two minutes:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Idle worker, empty queue</th>
<th>Per minute</th>
<th>Per 30-day month</th>
</tr>
</thead>
<tbody>
<tr>
<td>Commands your app sends</td>
<td>26</td>
<td>about 1.1 million</td>
</tr>
<tr>
<td>Commands the server executes, including inside scripts</td>
<td>about 110</td>
<td>about 4.8 million</td>
</tr>
</tbody>
</table></div>
<p>So one idle worker uses nearly ten times a 500,000-command free allowance every month, without
processing a single job. Two workers, double it. This is not a bug; it is how a responsive queue
works. It is the reason Upstash’s own documentation recommends a fixed-price plan for worker-heavy
setups, and the reason we said in our <a href="/blog/free-redis-hosting/">free Redis hosting comparison</a> that
a command-metered tier is the wrong fit for a queue.</p>
<p>The fix is to choose Redis that limits memory, not commands.</p>
<h2 id="finished-jobs-fill-your-memory" tabindex="-1"><a class="header-anchor" href="#finished-jobs-fill-your-memory">Finished jobs fill your memory</a></h2>
<p>By default the library keeps every completed job in Redis, forever. We measured what that costs:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setting</th>
<th>Memory per job after completion</th>
</tr>
</thead>
<tbody>
<tr>
<td>Default (keep completed jobs)</td>
<td>about 469 bytes</td>
</tr>
<tr>
<td><code>removeOnComplete: true</code></td>
<td>about 48 bytes</td>
</tr>
</tbody>
</table></div>
<p>469 bytes sounds small. At 10,000 jobs a day it is about 140 MB a month, which is more than a 128 MB
store holds. Your queue then stops accepting jobs, or, worse, if the store is set to evict keys, it
starts deleting queue data.</p>
<p>Keep a bounded history instead. <code>removeOnComplete: { count: 1000 }</code> keeps the last thousand for your
dashboard, and <code>removeOnFail: { age: 604800 }</code> keeps failures for a week so you can inspect and retry
them.</p>
<h2 id="concurrency-how-many-jobs-at-once" tabindex="-1"><a class="header-anchor" href="#concurrency-how-many-jobs-at-once">Concurrency: how many jobs at once</a></h2>
<p><code>concurrency</code> is the number of jobs one worker runs at the same time. We timed 10,000 jobs that did no
work, with Redis in a virtual machine on the same laptop, so this measures the queue itself:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Concurrency</th>
<th>Jobs per second</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>about 1,950</td>
</tr>
<tr>
<td>10</td>
<td>about 5,870</td>
</tr>
<tr>
<td>50</td>
<td>about 5,830</td>
</tr>
</tbody>
</table></div>
<p>Going from 1 to 10 tripled throughput. It then levelled off at about 5,850 a second, because with jobs
that do no work, Redis, the library’s scripts and the client become the limit, not the worker. Real jobs spend most of their time waiting on an email provider or an API, so for I/O-bound
work a concurrency of 10 to 50 per worker is normal. For CPU-heavy work such as image processing,
keep concurrency near the number of CPU cores and add worker processes instead.</p>
<p>Distance changes these numbers. With concurrency 1, each job needs at least one round trip to Redis.
From a server in Mumbai we measured about 9 ms to a Mumbai region and about 86 ms to Singapore, so a
single-concurrency worker talking to a Singapore store tops out at roughly a dozen jobs a second,
however fast your code is. That is an estimate from the round trip, not a measured queue run, but the
direction is certain: keep the worker and the store in the same region.</p>
<h2 id="choosing-a-redis-for-your-queue" tabindex="-1"><a class="header-anchor" href="#choosing-a-redis-for-your-queue">Choosing a Redis for your queue</a></h2>
<p>A queue has stricter needs than a cache:</p>
<ol>
<li><strong>No eviction.</strong> The library cannot work if Redis deletes keys to free memory. The eviction policy
must be <code>noeviction</code>, so a full store refuses writes rather than silently dropping jobs. See the
<a href="https://redis.io/docs/latest/develop/reference/eviction/">Redis eviction policies</a> for what each
one does.</li>
<li><strong>Data kept on disk.</strong> A store that empties on restart loses every queued job. Use one with
persistence.</li>
<li><strong>Memory, not commands, as the limit.</strong> As measured above, idle workers alone send millions of
commands a month.</li>
<li><strong>Enough connections.</strong> Each <code>Queue</code> and <code>Worker</code> holds its own connection, and a worker uses an
extra one for its blocking wait. Two web instances and two workers need about 6, more if you add
event listeners or a dashboard.</li>
<li><strong>The same region as the worker.</strong> Every job pays round trips to Redis.</li>
</ol>
<p>Valkey and Redis should both work for this, since the queue uses core commands and Lua scripts; test
with your own jobs before you rely on Valkey. Our
<a href="/blog/valkey-vs-redis/">Valkey vs Redis comparison</a> covers the differences that do exist.</p>
<p>On Voroa, a key value store’s <strong>When memory is full</strong> setting defaults to <strong>Keep everything</strong>, which is
the no-eviction behaviour a queue needs. The store keeps data on disk with a nightly snapshot, runs
in Mumbai next to your services, and is limited by memory rather than commands. Details are in the
<a href="/docs/key-value/">Key Value guide</a>.</p>
<h2 id="production-checklist" tabindex="-1"><a class="header-anchor" href="#production-checklist">Production checklist</a></h2>
<ul>
<li><strong>Use separate connections for producers and workers.</strong> The library’s own production guide asks for
<code>maxRetriesPerRequest: null</code> on workers, so they retry through a Redis restart, and for producers that
fail fast, so a web request does not hang while Redis is down. The setup file above does both.</li>
<li><strong>Bound job history</strong> with <code>removeOnComplete</code> and <code>removeOnFail</code>, as above.</li>
<li><strong>Retry with backoff.</strong> <code>attempts: 5</code> with exponential backoff absorbs a provider that is briefly
down.</li>
<li><strong>Shut down gracefully.</strong> Call <code>worker.close()</code> on <code>SIGTERM</code> so a deploy lets running jobs finish
rather than leaving them stalled.</li>
<li><strong>Keep payloads small.</strong> Put an ID in the job and load the record in the worker. Never put
passwords or card data in a job.</li>
<li><strong>Make jobs idempotent.</strong> A job can run twice after a crash or a stall. Sending the same email twice
is annoying; charging a card twice is worse. Check before you act.</li>
<li><strong>Run the worker as its own process.</strong> Deploy it separately from the web server, so a burst of jobs
never slows your pages. On Voroa that is a <a href="/background-workers/">background worker</a>, which never
sleeps; the steps are in <a href="/docs/deploy-background-worker/">deploying a background worker</a>.</li>
<li><strong>Put <code>REDIS_URL</code> in an environment variable</strong>, never in code. See
<a href="/docs/environment-variables/">environment variables on Voroa</a>.</li>
</ul>
<h2 id="bull-rabbitmq-or-something-else" tabindex="-1"><a class="header-anchor" href="#bull-rabbitmq-or-something-else">Bull, RabbitMQ or something else?</a></h2>
<p><strong>Bull</strong> is the older library by the same authors. It is in maintenance mode; start new projects on
the newer one.</p>
<p><strong>RabbitMQ</strong> is a dedicated message broker. It is the better choice when many different services in
different languages exchange messages, or when you need complex routing. For one Node.js app that
needs background jobs with retries, delays and a dashboard, a Redis queue is simpler: you probably
already run Redis, and there is one less server to look after.</p>
<p><strong>Cron</strong> is for work that runs on a clock, such as a nightly report. A queue is for work triggered by
events. Many apps use both; a <a href="/docs/scheduled-tasks-cron/">scheduled task</a> can add jobs to the queue.</p>
<h2 id="host-the-queue-and-the-worker-together" tabindex="-1"><a class="header-anchor" href="#host-the-queue-and-the-worker-together">Host the queue and the worker together</a></h2>
<p>Voroa runs your web service, your background worker and a Redis-compatible key value store side by side
in Mumbai, with no eviction by default and a free store size to start. See the sizes on the
<a href="/managed-redis-india/">managed Redis page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Does the worker need its own server?</strong>
It needs its own process. It can share a machine in development, but in production run it as a
separate service so it can restart and scale on its own.</p>
<p><strong>Can I use it with a free Redis tier?</strong>
Yes, if the tier limits memory rather than commands, keeps data on disk and does not evict. A
command-metered free tier is used up by one idle worker in a few days.</p>
<p><strong>Why are my jobs stuck in “waiting”?</strong>
Usually no worker is running, the worker is connected to a different Redis or queue name, or the
worker crashed on start. Check the worker’s logs first.</p>
<p><strong>Why did Redis run out of memory?</strong>
Completed jobs are kept by default. Set <code>removeOnComplete</code> and <code>removeOnFail</code> with a count or an age.</p>
<p><strong>Does it work with Valkey?</strong>
It relies on core commands and Lua scripts, which Valkey supports. Test with your own jobs before you
switch.</p>
<p><strong>Is it free?</strong>
The library is open source under the MIT licence. Its authors also sell a Pro version with extra
features such as groups and batches.</p>
<p><em>Measured 7 November 2026 with library version 6.7.1 and Redis 8.10.3 in WSL2 on a laptop. Found
something out of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>What Is Celery Beat? Setup, Beat vs Worker and Pitfalls</title>
      <link>https://getvoroa.com/blog/celery-beat/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/celery-beat/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>Python</category>
      <description><![CDATA[Celery beat is the scheduler that sends periodic tasks to your workers. See beat vs worker, a tested setup, why tasks run twice, timezones and missed runs.]]></description>
      <content:encoded><![CDATA[<p>Celery beat is the scheduler that sends periodic tasks to your Celery workers: a report every morning,
a cleanup every hour, a sync every few minutes. It does not run tasks itself. It only puts them on the
queue at the right time.</p>
<p>This guide answers what beat is and how it differs from a worker, shows the setup, then what we saw when
we tested the two mistakes that cause most beat problems: running more than one beat, and restarting it.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#what-is-celery-beat">What is Celery beat?</a></li>
<li><a href="#celery-beat-vs-celery-worker">Celery beat vs Celery worker</a></li>
<li><a href="#how-beat-fits-with-workers">How beat fits with workers</a></li>
<li><a href="#set-up-a-periodic-task">Set up a periodic task</a></li>
<li><a href="#test-1-two-beats-every-task-twice">Test 1: two beats, every task twice</a></li>
<li><a href="#test-2-what-a-restart-does">Test 2: what a restart does</a></li>
<li><a href="#timezones">Timezones</a></li>
<li><a href="#storing-the-schedule-in-djangos-database">Storing the schedule in Django’s database</a></li>
<li><a href="#running-beat-in-production">Running beat in production</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="what-is-celery-beat" tabindex="-1"><a class="header-anchor" href="#what-is-celery-beat">What is Celery beat?</a></h2>
<p>Celery beat is Celery’s built-in scheduler for periodic tasks. It runs as its own process, reads a
schedule of intervals or <code>crontab()</code> times, and when a task is due it sends a message to the broker. A Celery worker then picks the message up and runs
the task. You run exactly one beat per schedule.</p>
<h2 id="celery-beat-vs-celery-worker" tabindex="-1"><a class="header-anchor" href="#celery-beat-vs-celery-worker">Celery beat vs Celery worker</a></h2>
<p>Periodic tasks need both. Beat decides <strong>when</strong>; the worker does the <strong>work</strong>.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Celery beat</th>
<th>Celery worker</th>
</tr>
</thead>
<tbody>
<tr>
<td>Job</td>
<td>Sends due tasks to the broker on a schedule</td>
<td>Takes tasks from the broker and runs them</td>
</tr>
<tr>
<td>Runs your task code</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>How many to run</td>
<td>Exactly one per schedule</td>
<td>As many as the load needs</td>
</tr>
<tr>
<td>Command</td>
<td><code>celery -A proj beat</code></td>
<td><code>celery -A proj worker</code></td>
</tr>
<tr>
<td>Keeps state</td>
<td>Last run times, in <code>celerybeat-schedule</code> by default</td>
<td>None between tasks</td>
</tr>
<tr>
<td>If it is missing</td>
<td>Nothing gets queued on time</td>
<td>Queued tasks wait and never run</td>
</tr>
</tbody>
</table></div>
<p>The Celery docs say it directly: “You have to ensure only a single scheduler is running for a schedule at
a time, otherwise you’d end up with duplicate tasks.” Workers have no such limit.</p>
<h2 id="how-beat-fits-with-workers" tabindex="-1"><a class="header-anchor" href="#how-beat-fits-with-workers">How beat fits with workers</a></h2>
<p>A Celery setup with periodic tasks has three parts:</p>
<ul>
<li><strong>Beat</strong> decides when a task is due and sends a message to the broker.</li>
<li><strong>The broker</strong>, usually Redis or RabbitMQ, holds the message.</li>
<li><strong>Workers</strong> take messages off the broker and run the tasks.</li>
</ul>
<p><img src="https://media.getvoroa.com/img/015cf7b259763ab5.webp" alt="Diagram: one beat process sends a task message to the broker every interval, and workers take and run it; a second beat process would send a second copy of every message" width="700" loading="eager" decoding="async"></p>
<p>You can run many workers. You should run exactly one beat, for reasons the first test makes clear.</p>
<h2 id="set-up-a-periodic-task" tabindex="-1"><a class="header-anchor" href="#set-up-a-periodic-task">Set up a periodic task</a></h2>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python"># tasks.py
import os
from celery import Celery
from celery.schedules import crontab

app = Celery(&quot;tasks&quot;, broker=os.environ[&quot;REDIS_URL&quot;])
app.conf.timezone = &quot;Asia/Kolkata&quot;

app.conf.beat_schedule = {
    &quot;daily-digest&quot;: {
        &quot;task&quot;: &quot;tasks.send_digest&quot;,
        &quot;schedule&quot;: crontab(hour=9, minute=0),          # 9:00 am IST
    },
    &quot;cleanup-every-15-min&quot;: {
        &quot;task&quot;: &quot;tasks.cleanup&quot;,
        &quot;schedule&quot;: crontab(minute=&quot;*/15&quot;),
    },
}

@app.task
def send_digest(): ...

@app.task
def cleanup(): ...
</code></pre>
</div>
<p>Run the worker and beat as two separate processes:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">celery -A tasks worker --loglevel=info
celery -A tasks beat --loglevel=info
</code></pre>
</div>
<p>A schedule can be a <code>crontab(...)</code> for calendar times or a number of seconds for a fixed interval,
such as <code>300.0</code> for every five minutes.</p>
<h2 id="test-1-two-beats-every-task-twice" tabindex="-1"><a class="header-anchor" href="#test-1-two-beats-every-task-twice">Test 1: two beats, every task twice</a></h2>
<p><strong>Setup for both tests:</strong> Celery 5.7.0 on Python 3.14 with Redis 8.10.3 as the broker. One worker
process, and a task scheduled every 2 seconds that records each run. Measured 7 November 2026 on a
laptop.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setup (20 seconds)</th>
<th>Task runs</th>
<th>Expected</th>
</tr>
</thead>
<tbody>
<tr>
<td>One beat</td>
<td>9</td>
<td>about 10</td>
</tr>
<tr>
<td>Two beats</td>
<td><strong>18</strong></td>
<td>about 10</td>
</tr>
</tbody>
</table></div>
<p>Two beats sent every task twice, and the worker ran both copies. Beat does not know another beat
exists.</p>
<p>This happens more easily than it sounds. Deploy beat inside a web service that runs two instances, or
start a worker with <code>-B</code> (which embeds beat) on two machines, and you have two beats.</p>
<h2 id="test-2-what-a-restart-does" tabindex="-1"><a class="header-anchor" href="#test-2-what-a-restart-does">Test 2: what a restart does</a></h2>
<p>We stopped the only beat for 10 seconds and started it again with the same schedule file.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What we checked</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Runs during the 10-second gap</td>
<td>0</td>
</tr>
<tr>
<td>First run after the restart</td>
<td>0.6 seconds after start-up</td>
</tr>
<tr>
<td>Total in 30 seconds (about 15 expected)</td>
<td>9</td>
</tr>
</tbody>
</table></div>
<p>Beat did not replay the runs it missed. A run came 0.6 seconds after start-up, then the normal schedule
carried on. With a 2-second schedule, we cannot tell whether that first run was a catch-up or simply the
next tick.</p>
<p>For a job every few minutes, that is usually fine.</p>
<p>For a daily job it depends on the schedule file,
where beat keeps last-run times.</p>
<p>If the file is lost, for example on a host that wipes files on each
deploy, a beat that is down at 9:00 am skips the whole day. If a daily task must not be missed, record when it last succeeded and check that on
start-up.</p>
<h2 id="timezones" tabindex="-1"><a class="header-anchor" href="#timezones">Timezones</a></h2>
<p>Celery uses UTC unless you set <code>timezone</code> to an IANA zone name, the same names Python’s
<a href="https://docs.python.org/3/library/zoneinfo.html">zoneinfo module</a> uses. Set it once:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python">app.conf.timezone = &quot;Asia/Kolkata&quot;
</code></pre>
</div>
<p>After that, <code>crontab(hour=9, minute=0)</code> means 9:00 am in India. Without it, the same line runs at
2:30 pm Indian time. India has no daylight saving, so the time stays fixed all year.</p>
<p>In Django, the setting is <code>CELERY_TIMEZONE</code> when you load Celery’s settings from Django with the <code>CELERY</code>
namespace. If you do not set it, Celery’s
<a href="https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html">periodic tasks guide</a> says Django’s
<code>TIME_ZONE</code> is used.</p>
<p>If you change the timezone later, the default file scheduler notices and resets itself. The Django
database scheduler does not, so reset its last-run times by hand:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">python manage.py shell -c &quot;from django_celery_beat.models import PeriodicTask; PeriodicTask.objects.update(last_run_at=None)&quot;
</code></pre>
</div>
<h2 id="storing-the-schedule-in-djangos-database" tabindex="-1"><a class="header-anchor" href="#storing-the-schedule-in-djangos-database">Storing the schedule in Django’s database</a></h2>
<p>The <code>django-celery-beat</code> package keeps schedules in your database, so you can add or change them from
the Django admin without redeploying:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">pip install django-celery-beat
celery -A proj beat --scheduler django_celery_beat.schedulers:DatabaseScheduler
</code></pre>
</div>
<p>It changes where the schedule lives, not how many beats you may run. Still run exactly one.</p>
<h2 id="running-beat-in-production" tabindex="-1"><a class="header-anchor" href="#running-beat-in-production">Running beat in production</a></h2>
<ul>
<li><strong>One beat process, always.</strong> Run it as its own service with a single instance, separate from your web
app and your workers.</li>
<li><strong>Do not use <code>worker -B</code> in production</strong> unless you are certain only one such worker exists. Celery’s
own docs say it “isn’t recommended for production use”.</li>
<li><strong>Keep the schedule file on disk</strong> if you use the default scheduler, or use the database scheduler, so
last-run times survive restarts.</li>
<li><strong>Make sure a worker listens on the task’s queue.</strong> If a task is routed to a queue no worker consumes,
beat keeps sending it and nothing runs, with no error.</li>
<li><strong>Make tasks idempotent.</strong> A restart, a retry or a second beat can all cause a repeat run.</li>
<li><strong>Watch the effect.</strong> Check that the report was written or the email sent, not just that beat logged a
message.</li>
</ul>
<p>On Voroa, run beat as a <a href="/background-workers/">background worker</a> with one instance, and your Celery
workers as another. Both reach Redis on its internal address in Mumbai.</p>
<p>A service’s files on Voroa are reset on every deploy, so beat’s schedule file does not survive one. Use
the <code>django-celery-beat</code> database scheduler, or record each task’s last success in your database. Our
<a href="/blog/celery-redis/">Celery and Redis guide</a> covers the worker side, including idle command costs.</p>
<p>If a job is a simple script, a Voroa <strong>Cron Job</strong> can run it on a schedule without beat at all. Schedules
are in IST. See <a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a> and
<a href="/docs/worker-or-cron-job/">worker or cron job</a>. For the same duplicate-run test in a pure-Python
scheduler, see our <a href="/blog/apscheduler/">APScheduler guide</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Is Celery beat the same as a Celery worker?</strong>
No. Beat only schedules: it sends due tasks to the broker. A worker runs them. You need one beat and at
least one worker for periodic tasks.</p>
<p><strong>Why are my Celery periodic tasks running twice?</strong>
Usually because two beat processes are running. In our test, two beats doubled every task. Run exactly
one.</p>
<p><strong>Does Celery beat run missed tasks after a restart?</strong>
Not the missed ones. In our test it skipped the runs it missed, and a run came 0.6 seconds after it started. Whether an
overdue run fires depends on the schedule file surviving the restart.</p>
<p><strong>Should I run beat with <code>worker -B</code>?</strong>
Only in development. In production, run beat as its own single process.</p>
<p><strong>How do I set the timezone for Celery beat?</strong>
Set <code>timezone</code> in the Celery configuration, for example <code>Asia/Kolkata</code>. Otherwise schedules use UTC.</p>
<p><strong>Can I change schedules without redeploying?</strong>
Yes, with <code>django-celery-beat</code>, which stores schedules in the database and shows them in the Django admin.</p>
<p><em>Tested 7 November 2026 with Celery 5.7.0 and Redis 8.10.3; definitions and settings re-checked against
the Celery 5.7 docs on 14 November 2026. Found something out of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Celery Redis Setup Guide: Broker, Results and Costs, Measured</title>
      <link>https://getvoroa.com/blog/celery-redis/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/celery-redis/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>Python</category>
      <description><![CDATA[A Celery Redis setup guide for Python and Django: broker and result backend config, what an idle worker and each task cost in Redis, and production settings.]]></description>
      <content:encoded><![CDATA[<p>A Celery Redis setup is one of the most common ways to run background tasks in Python: Celery sends tasks
through Redis to worker processes, and can store their results there too. This guide is for Python and
Django developers moving slow work, such as emails, reports and image processing, out of the request.
It shows a working configuration, then what we measured about the Redis side that tutorials skip: how
many commands an idle worker sends, what each task costs, and how much memory results keep.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#how-celery-uses-redis">How Celery uses Redis</a></li>
<li><a href="#a-minimal-working-setup">A minimal working setup</a></li>
<li><a href="#django-in-six-lines">Django in six lines</a></li>
<li><a href="#what-an-idle-worker-costs-measured">What an idle worker costs, measured</a></li>
<li><a href="#what-each-task-costs-measured">What each task costs, measured</a></li>
<li><a href="#results-keep-them-only-if-you-read-them">Results: keep them only if you read them</a></li>
<li><a href="#the-visibility-timeout-trap">The visibility timeout trap</a></li>
<li><a href="#settings-for-production">Settings for production</a></li>
<li><a href="#choosing-a-redis-for-celery">Choosing a Redis for Celery</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="how-celery-uses-redis" tabindex="-1"><a class="header-anchor" href="#how-celery-uses-redis">How Celery uses Redis</a></h2>
<p>Redis plays two separate roles, and you can use one or both:</p>
<ul>
<li><strong>Broker.</strong> Your web app puts a task message on a Redis list. Workers take messages off the list and
run them. Without a broker there is no Celery.</li>
<li><strong>Result backend.</strong> When a task finishes, Celery can store its return value and state in Redis so
your app can read it later. This is optional, and often unnecessary.</li>
</ul>
<p><img src="https://media.getvoroa.com/img/8b947f3bc1651aee.webp" alt="Diagram: the Django or Flask app sends a task to a Redis list (the broker); a Celery worker takes it, runs it, and optionally writes the result to Redis (the result backend) where the app can read it" width="700" loading="eager" decoding="async"></p>
<h2 id="a-minimal-working-setup" tabindex="-1"><a class="header-anchor" href="#a-minimal-working-setup">A minimal working setup</a></h2>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">pip install &quot;celery[redis]&quot;
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python"># tasks.py
import os
from celery import Celery

app = Celery(
    &quot;tasks&quot;,
    broker=os.environ[&quot;REDIS_URL&quot;],          # e.g. redis://:password@host:6379/0
    backend=os.environ.get(&quot;RESULT_URL&quot;),    # optional; leave unset if you never read results
)

@app.task(bind=True, max_retries=5, default_retry_delay=10)
def send_welcome_email(self, user_id):
    try:
        deliver_email(user_id)               # your code
    except TemporaryError as exc:
        raise self.retry(exc=exc)
</code></pre>
</div>
<p>Start a worker in its own process:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">celery -A tasks worker --loglevel=info --concurrency=4
</code></pre>
</div>
<p>And queue work from your app:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python">send_welcome_email.delay(user.id)
</code></pre>
</div>
<p>For a TLS connection, use a <code>rediss://</code> URL. For the result backend, add the certificate setting to the
URL, for example <code>rediss://:password@host:6380/1?ssl_cert_reqs=required</code>, or Celery refuses it.</p>
<h2 id="django-in-six-lines" tabindex="-1"><a class="header-anchor" href="#django-in-six-lines">Django in six lines</a></h2>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python"># settings.py
CELERY_BROKER_URL = os.environ[&quot;REDIS_URL&quot;]
CELERY_TASK_IGNORE_RESULT = True
CELERY_TASK_ACKS_LATE = True
CELERY_TASK_REJECT_ON_WORKER_LOST = True
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
</code></pre>
</div>
<p>With the usual <code>celery.py</code> in your project (<code>app.config_from_object(&quot;django.conf:settings&quot;, namespace=&quot;CELERY&quot;)</code> and <code>app.autodiscover_tasks()</code>), any <code>@shared_task</code> in your apps is picked up.
Each setting above is explained below.</p>
<h2 id="what-an-idle-worker-costs-measured" tabindex="-1"><a class="header-anchor" href="#what-an-idle-worker-costs-measured">What an idle worker costs, measured</a></h2>
<p><strong>Setup:</strong> Celery 5.7.0, kombu 5.7.0 and redis-py 8.1.0 on Python 3.14, on a Windows laptop, using the
<code>solo</code> pool because Celery’s default pool does not run on Windows. Redis 8.10.3 ran in a Linux virtual
machine (WSL2) on the same laptop. We counted commands with Redis’s own command statistics. Measured 7
November 2026.</p>
<p>We started one worker with nothing to do, let it settle for 20 seconds, then counted for two minutes.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>One idle worker</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Commands in 2 minutes</td>
<td>200</td>
</tr>
<tr>
<td>Per minute</td>
<td>100</td>
</tr>
<tr>
<td>Per 30-day month</td>
<td>about 4.3 million</td>
</tr>
</tbody>
</table></div>
<p>An idle Celery worker polls the broker for new tasks and talks to other workers every few seconds. That
is normal, and on a store that limits memory it costs nothing. On a host that charges per command, it is
the bill: 4.3 million is more than eight times a 500,000-command free allowance, before a single task
runs. The same is true of Node.js queues; our <a href="/blog/bullmq-redis-job-queue/">BullMQ measurements</a> found
about 110 commands a minute for an idle worker there.</p>
<h2 id="what-each-task-costs-measured" tabindex="-1"><a class="header-anchor" href="#what-each-task-costs-measured">What each task costs, measured</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>2,000 tasks, each <code>add(x, y)</code></th>
<th>Commands per task</th>
<th>Memory left in Redis per task</th>
</tr>
</thead>
<tbody>
<tr>
<td>With a result backend, and the app reading each result</td>
<td>18</td>
<td>about 383 bytes</td>
</tr>
<tr>
<td>With <code>ignore_result=True</code></td>
<td>about 10</td>
<td>none</td>
</tr>
</tbody>
</table></div>
<p>The commands-per-task figures include the worker’s normal polling during the run, so treat them as
upper bounds. Turning results off removed roughly 8 commands per task and all of the leftover memory.</p>
<h2 id="results-keep-them-only-if-you-read-them" tabindex="-1"><a class="header-anchor" href="#results-keep-them-only-if-you-read-them">Results: keep them only if you read them</a></h2>
<p>By default, every result is kept in Redis for one day. We checked: a result key had a remaining
lifetime of 86,396 seconds straight after the task finished.</p>
<p>At about 383 bytes each, 100,000 tasks a day keeps around 38 MB of results in memory at any time, which
is nearly a third of a 128 MB store, holding values nobody reads. Most tasks, such as sending an email or
resizing an image, do not need a stored result at all.</p>
<ul>
<li>Set <code>task_ignore_result = True</code> globally, and turn results on only for the tasks whose return value you
actually fetch.</li>
<li>If you do keep results, lower <code>result_expires</code> to how long you really need them, for example one hour.</li>
<li>If you need to track progress for a user, store it in your database or publish it, rather than polling
the result backend.</li>
</ul>
<h2 id="the-visibility-timeout-trap" tabindex="-1"><a class="header-anchor" href="#the-visibility-timeout-trap">The visibility timeout trap</a></h2>
<p>With Redis as the broker, a worker takes a task but Redis does not know whether it finished. Celery
handles this with a <strong>visibility timeout</strong>, one hour by default: if a task has not been acknowledged
within that time, it is given to another worker.</p>
<p>Two things follow:</p>
<ul>
<li><strong>A task that runs longer than an hour can run twice.</strong> If you have long reports or exports, raise the
timeout above your longest task with <code>broker_transport_options = {&quot;visibility_timeout&quot;: 7200}</code>.</li>
<li><strong>Tasks scheduled far in the future are redelivered.</strong> A task with a countdown or <code>eta</code> longer than the
visibility timeout can be handed out again before it is due. For anything scheduled hours ahead, use a
<a href="/docs/scheduled-tasks-cron/">scheduled task</a> or Celery beat instead of a long countdown.</li>
</ul>
<p>Either way, write tasks so that running twice is harmless: check whether the email was sent or the
invoice created before doing it again.</p>
<h2 id="settings-for-production" tabindex="-1"><a class="header-anchor" href="#settings-for-production">Settings for production</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setting</th>
<th>Value</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>task_ignore_result</code></td>
<td><code>True</code></td>
<td>Saves commands and memory unless you read results</td>
</tr>
<tr>
<td><code>task_acks_late</code></td>
<td><code>True</code></td>
<td>Acknowledge after the task finishes rather than when it starts</td>
</tr>
<tr>
<td><code>task_reject_on_worker_lost</code></td>
<td><code>True</code></td>
<td>With <code>acks_late</code>, requeue a task whose worker process was killed; without it, that task is acknowledged and lost. Watch for a task that always crashes its process, which then retries forever</td>
</tr>
<tr>
<td><code>worker_prefetch_multiplier</code></td>
<td><code>1</code></td>
<td>Each worker process reserves one task at a time (a worker with concurrency 4 reserves 4), so long tasks do not hold others hostage</td>
</tr>
<tr>
<td><code>broker_transport_options</code></td>
<td><code>{&quot;visibility_timeout&quot;: ...}</code></td>
<td>Longer than your slowest task</td>
</tr>
<tr>
<td><code>task_time_limit</code></td>
<td>e.g. <code>600</code></td>
<td>Kill a task that hangs instead of blocking a worker forever</td>
</tr>
<tr>
<td><code>broker_connection_retry_on_startup</code></td>
<td><code>True</code></td>
<td>Keep retrying if Redis is not reachable when the worker boots</td>
</tr>
</tbody>
</table></div>
<p>Even with both settings, if the whole worker machine or container dies, its unacknowledged tasks come
back only after the visibility timeout, up to an hour by default.</p>
<p>Run the worker as its own long-running process, not inside your web server. On Voroa that is a
<a href="/background-workers/">background worker</a>, which never sleeps; the steps are in
<a href="/docs/deploy-background-worker/">deploying a background worker</a>, and the Redis URL goes in an
<a href="/docs/environment-variables/">environment variable</a>.</p>
<h2 id="choosing-a-redis-for-celery" tabindex="-1"><a class="header-anchor" href="#choosing-a-redis-for-celery">Choosing a Redis for Celery</a></h2>
<ol>
<li><strong>Limited by memory, not commands.</strong> As measured above, idle workers send millions of commands a month.</li>
<li><strong>No eviction.</strong> If the store deletes keys when full, queued tasks can vanish. Use a policy that
refuses writes instead; see <a href="https://redis.io/docs/latest/develop/reference/eviction/">Redis eviction policies</a>.</li>
<li><strong>Data kept on disk.</strong> A store that empties on restart loses every queued task.</li>
<li><strong>The same region as the worker.</strong> Every task fetch is a round trip; a store in another country slows
every one.</li>
</ol>
<p>Celery should work the same on Redis and Valkey, since it uses core list and key commands; test with
your own tasks before you switch. our
<a href="/blog/valkey-vs-redis/">Valkey vs Redis comparison</a> covers where the two differ. On Voroa, a key value
store defaults to <strong>Keep everything</strong> when memory is full, keeps data on disk with a nightly snapshot,
and runs in Mumbai beside your workers. See the <a href="/docs/key-value/">Key Value guide</a>.</p>
<h2 id="run-celery-and-its-redis-in-one-place" tabindex="-1"><a class="header-anchor" href="#run-celery-and-its-redis-in-one-place">Run Celery and its Redis in one place</a></h2>
<p>Your Django or Flask app, the Celery worker and a Redis-compatible store, side by side in Mumbai, with a
free store size to start. See the sizes on the <a href="/managed-redis-india/">managed Redis page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Do I need a result backend for Celery?</strong>
No. Only if your app reads task return values. Without one, tasks still run; set
<code>task_ignore_result = True</code>.</p>
<p><strong>Why is my Redis memory growing with Celery?</strong>
Usually stored results, which are kept for a day by default. Ignore results you do not read, or lower
<code>result_expires</code>.</p>
<p><strong>Why did my Celery task run twice?</strong>
With Redis as the broker, a task not acknowledged within the visibility timeout (one hour by default)
is redelivered. Long tasks and long countdowns are the usual cause.</p>
<p><strong>Can I use the same Redis for the broker and results?</strong>
Yes, ideally on separate database numbers or key prefixes. On a small store, keep results off so they do
not compete with the queue for memory.</p>
<p><strong>Does Celery work on Windows?</strong>
For development, with <code>--pool=solo</code>. Run production workers on Linux.</p>
<p><strong>Is Redis or RabbitMQ better for Celery?</strong>
Redis is simpler to run and fine for most apps. RabbitMQ gives stronger delivery guarantees without a
visibility timeout and suits very high volumes or complex routing.</p>
<p><em>Measured 7 November 2026 with Celery 5.7.0 and Redis 8.10.3. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Cron Job Every 5 Minutes: Expressions, Traps and Limits</title>
      <link>https://getvoroa.com/blog/cron-every-5-minutes/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/cron-every-5-minutes/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[How to write a cron job every 5 minutes, plus every 10, 15 and 30, the step values that give uneven gaps, overlapping runs and each platform's minimum interval.]]></description>
      <content:encoded><![CDATA[<p>A cron job every 5 minutes is written <code>*/5 * * * *</code>. That one line answers most searches, and the
table below covers the other common intervals.</p>
<p>The rest of this guide covers what catches people out later. Some step values give uneven gaps, a slow
job can overlap the next run, and several hosting platforms will not run a job that often at all.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#every-n-minutes-expressions">Every-N-minutes expressions</a></li>
<li><a href="#what-5-actually-means">What */5 actually means</a></li>
<li><a href="#the-step-values-that-give-uneven-gaps">The step values that give uneven gaps</a></li>
<li><a href="#when-a-5-minute-job-takes-7-minutes">When a 5-minute job takes 7 minutes</a></li>
<li><a href="#can-cron-run-every-30-seconds">Can cron run every 30 seconds?</a></li>
<li><a href="#each-platforms-minimum-interval">Each platform’s minimum interval</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="every-n-minutes-expressions" tabindex="-1"><a class="header-anchor" href="#every-n-minutes-expressions">Every-N-minutes expressions</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Schedule</th>
<th>Expression</th>
<th>Runs per day</th>
</tr>
</thead>
<tbody>
<tr>
<td>Every minute</td>
<td><code>* * * * *</code></td>
<td>1,440</td>
</tr>
<tr>
<td>Every 2 minutes</td>
<td><code>*/2 * * * *</code></td>
<td>720</td>
</tr>
<tr>
<td>Every 5 minutes</td>
<td><code>*/5 * * * *</code></td>
<td>288</td>
</tr>
<tr>
<td>Every 10 minutes</td>
<td><code>*/10 * * * *</code></td>
<td>144</td>
</tr>
<tr>
<td>Every 15 minutes</td>
<td><code>*/15 * * * *</code></td>
<td>96</td>
</tr>
<tr>
<td>Every 30 minutes</td>
<td><code>*/30 * * * *</code> or <code>0,30 * * * *</code></td>
<td>48</td>
</tr>
<tr>
<td>Every hour</td>
<td><code>0 * * * *</code></td>
<td>24</td>
</tr>
<tr>
<td>Every 5 minutes, office hours only</td>
<td><code>*/5 9-18 * * 1-5</code></td>
<td>120 on weekdays</td>
</tr>
<tr>
<td>Every 5 minutes, offset by 2</td>
<td><code>2-59/5 * * * *</code></td>
<td>288</td>
</tr>
</tbody>
</table></div>
<p>The offset form is useful when many jobs share a server. Starting at minute 2 keeps your job away from
the busy top of the hour.</p>
<h2 id="what-5-actually-means" tabindex="-1"><a class="header-anchor" href="#what-5-actually-means">What */5 actually means</a></h2>
<p><code>*/5</code> in the minute field means “every minute divisible by 5”: 0, 5, 10 and so on up to 55. It is tied
to the clock, not to when you saved the job.</p>
<p>So a job added at 10:03 first runs at 10:05, not 10:08. And it always runs at the same minutes past the
hour, which makes logs easy to read.</p>
<h2 id="the-step-values-that-give-uneven-gaps" tabindex="-1"><a class="header-anchor" href="#the-step-values-that-give-uneven-gaps">The step values that give uneven gaps</a></h2>
<p>Steps restart at the top of every hour. That is fine for numbers that divide 60 evenly: 2, 3, 4, 5, 6,
10, 12, 15, 20 and 30. For any other step, the last gap of the hour comes out shorter.</p>
<p>We generated the real run times for a day with the <code>croniter</code> library, which follows standard cron
rules:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Expression</th>
<th>First runs</th>
<th>Gaps you actually get</th>
<th>Runs per day</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>*/5 * * * *</code></td>
<td>00:05, 00:10, 00:15</td>
<td>always 5 min</td>
<td>288</td>
</tr>
<tr>
<td><code>*/7 * * * *</code></td>
<td>00:49, 00:56, <strong>01:00</strong>, 01:07</td>
<td>7 min, then <strong>4 min</strong> at each hour</td>
<td>216</td>
</tr>
<tr>
<td><code>*/25 * * * *</code></td>
<td>00:25, 00:50, <strong>01:00</strong>, 01:25</td>
<td>25, 25, then <strong>10 min</strong></td>
<td>72</td>
</tr>
<tr>
<td><code>*/45 * * * *</code></td>
<td>00:45, <strong>01:00</strong>, 01:45, 02:00</td>
<td>alternating 45 and <strong>15 min</strong></td>
<td>48</td>
</tr>
<tr>
<td><code>0 */5 * * *</code> (hours)</td>
<td>05:00, 10:00, 15:00, 20:00, <strong>00:00</strong></td>
<td>5 h, then <strong>4 h</strong> at midnight</td>
<td>5</td>
</tr>
</tbody>
</table></div>
<p><img src="https://media.getvoroa.com/img/daf4a73c0056d582.webp" alt="Diagram: run times within one hour for */5, */7 and */45 - */5 runs evenly every 5 minutes, */7 leaves a 4-minute gap before the hour, */45 alternates 45 and 15 minutes" width="700" loading="eager" decoding="async"></p>
<p>If you need “every 7 minutes” exactly, cron cannot express it. Run every minute and let the script skip
until 7 minutes have passed since its last run, or use a scheduler with an interval trigger, such as
<a href="/blog/apscheduler/">APScheduler in Python</a>.</p>
<h2 id="when-a-5-minute-job-takes-7-minutes" tabindex="-1"><a class="header-anchor" href="#when-a-5-minute-job-takes-7-minutes">When a 5-minute job takes 7 minutes</a></h2>
<p>Cron starts each run on time, whether or not the last one has finished. A job that usually takes 2
minutes but once takes 7 will still be running when the next copy starts.</p>
<p>Two copies working on the same rows can send duplicate emails or corrupt a count. On a Linux server,
<code>flock</code> stops the second copy from starting:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">*/5 * * * * flock -n /tmp/sync.lock /usr/local/bin/sync.sh
</code></pre>
</div>
<p><code>flock -n</code> exits at once if the lock is held, so an overlapping run is skipped rather than queued. In
application code, the same idea is a lock key in Redis or your database. Our
<a href="/blog/cron-job-in-node-js/">Node.js cron tests</a> measured three copies of a slow job running at once
without one.</p>
<h2 id="can-cron-run-every-30-seconds" tabindex="-1"><a class="header-anchor" href="#can-cron-run-every-30-seconds">Can cron run every 30 seconds?</a></h2>
<p>Standard cron’s smallest unit is one minute. To run every 30 seconds, add a second entry that sleeps
first:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">* * * * * /usr/local/bin/task.sh
* * * * * sleep 30; /usr/local/bin/task.sh
</code></pre>
</div>
<p>If you need sub-minute timing regularly, a long-running process with an in-app scheduler or a queue is
usually cleaner than stacking cron lines. Some libraries, such as node-cron, accept an optional seconds
field.</p>
<h2 id="each-platforms-minimum-interval" tabindex="-1"><a class="header-anchor" href="#each-platforms-minimum-interval">Each platform’s minimum interval</a></h2>
<p>Your cron expression is only half the story. Many hosts set a floor on how often a job may run, and
some do not promise exact timing.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Platform</th>
<th>Shortest interval</th>
<th>Timing</th>
<th>Timezone</th>
</tr>
</thead>
<tbody>
<tr>
<td>Linux cron on your own server</td>
<td>1 minute</td>
<td>On the minute</td>
<td>Server’s timezone</td>
</tr>
<tr>
<td>GitHub Actions <code>schedule</code></td>
<td>5 minutes</td>
<td>Can be delayed at busy times, especially the top of the hour</td>
<td>UTC by default</td>
</tr>
<tr>
<td>Vercel Cron Jobs, Hobby</td>
<td>Once a day</td>
<td>Anywhere within the hour (up to 59 minutes late)</td>
<td>UTC</td>
</tr>
<tr>
<td>Vercel Cron Jobs, Pro</td>
<td>1 minute</td>
<td>Per minute</td>
<td>UTC</td>
</tr>
<tr>
<td>Heroku Scheduler</td>
<td>10 minutes</td>
<td>Expected but not guaranteed; can rarely skip or run twice</td>
<td>UTC</td>
</tr>
<tr>
<td>Voroa Cron Job or Scheduled Task</td>
<td>1 minute</td>
<td>Usually on time; can run late or overlap; skipped while the service is stopped</td>
<td>IST</td>
</tr>
</tbody>
</table></div>
<p>Sources: the <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows">GitHub Actions schedule docs</a>,
the <a href="https://vercel.com/docs/cron-jobs/usage-and-pricing">Vercel cron usage page</a> and the Heroku
Scheduler documentation, all checked on 7 November 2026. Vercel describes Cron Jobs as moving to a
newer Schedules feature.</p>
<p>Two of these matter for India. On a UTC platform, <code>*/5 9-18 * * 1-5</code> means 2:30 pm to 12:25 am IST, not
office hours. And on GitHub Actions, scheduled workflows in a public repository are turned off after 60
days with no activity.</p>
<h2 id="run-a-5-minute-job-on-voroa" tabindex="-1"><a class="header-anchor" href="#run-a-5-minute-job-on-voroa">Run a 5-minute job on Voroa</a></h2>
<p>Voroa runs <strong>Cron Jobs</strong> and <strong>Scheduled Tasks</strong> on schedules down to every minute, and the presets
include Every minute and Hourly. Schedules are in India Standard Time, so <code>*/5 9-18 * * 1-5</code> means 9 am
to 6:55 pm IST on weekdays.</p>
<p>Runs happen inside a service that stays running, so a slow run can overlap the next one. Use a lock as
shown above. Setup is in <a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a>, and long-running
processes are covered on the <a href="/background-workers/">background workers page</a>. Not sure which type your
code needs? See <a href="/docs/worker-or-cron-job/">worker or cron job</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>How do I run a cron job every 5 minutes?</strong>
Use <code>*/5 * * * *</code>. It runs at minutes 0, 5, 10 and so on, every hour, 288 times a day.</p>
<p><strong>Is <code>*/5</code> the same as “5 minutes after the last run”?</strong>
No. It runs at clock minutes divisible by 5. A job added at 10:03 first runs at 10:05.</p>
<p><strong>Why does my <code>*/7</code> job run 4 minutes apart sometimes?</strong>
Steps restart every hour. After minute 56, the next run is minute 0, only 4 minutes later.</p>
<p><strong>How do I stop a cron job from overlapping?</strong>
On Linux, wrap the command in <code>flock -n</code>. In code, take a lock in Redis or your database before doing
the work.</p>
<p><strong>Can GitHub Actions run a cron every minute?</strong>
No. The shortest interval is every 5 minutes, and runs can be delayed during busy periods.</p>
<p><strong>What timezone does cron use?</strong>
The server’s, which is usually UTC on cloud platforms. To run at 9 am IST on a UTC server, write the UTC
time: <code>30 3 * * *</code>. Or use a platform that schedules in IST.</p>
<p><em>Checked 7 November 2026. Found something out of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Cron Job in Node.js: node-cron Tested for Duplicate Runs</title>
      <link>https://getvoroa.com/blog/cron-job-in-node-js/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/cron-job-in-node-js/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>Node.js</category>
      <description><![CDATA[Set up a cron job in Node.js with node-cron, then see what we measured: duplicate runs across instances, overlapping jobs, missed runs and how to fix each.]]></description>
      <content:encoded><![CDATA[<p>A cron job in Node.js runs a function on a schedule: a nightly report, an hourly cleanup, a reminder
email every morning. The usual way is the <code>node-cron</code> package, and it takes five lines.</p>
<p>This guide shows those lines, then what we saw when we tested the parts tutorials skip. Two app
instances, a slow job and a restart each changed the result.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#set-up-node-cron-in-five-lines">Set up node-cron in five lines</a></li>
<li><a href="#reading-a-cron-expression">Reading a cron expression</a></li>
<li><a href="#test-1-two-instances-run-every-job-twice">Test 1: two instances run every job twice</a></li>
<li><a href="#test-2-a-slow-job-piles-up">Test 2: a slow job piles up</a></li>
<li><a href="#test-3-a-restart-skips-runs">Test 3: a restart skips runs</a></li>
<li><a href="#timezones-the-230-pm-surprise">Timezones: the 2:30 pm surprise</a></li>
<li><a href="#in-process-cron-or-a-separate-cron-job">In-process cron or a separate cron job?</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="set-up-node-cron-in-five-lines" tabindex="-1"><a class="header-anchor" href="#set-up-node-cron-in-five-lines">Set up node-cron in five lines</a></h2>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">npm install node-cron
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">import cron from 'node-cron';

cron.schedule('0 9 * * *', async () =&gt; {
  await sendDailyDigest();            // your code
}, { timezone: 'Asia/Kolkata' });
</code></pre>
</div>
<p>That runs <code>sendDailyDigest</code> at 9:00 am India time, every day, for as long as the process stays up. The
callback can be <code>async</code>. Errors inside it do not stop the schedule, so catch and log them yourself.</p>
<h2 id="reading-a-cron-expression" tabindex="-1"><a class="header-anchor" href="#reading-a-cron-expression">Reading a cron expression</a></h2>
<p>A standard expression has five fields. <code>node-cron</code> also accepts an optional sixth field for seconds at
the front.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Field</th>
<th>Allowed</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Minute</td>
<td>0-59</td>
<td><code>*/15</code> = every 15 minutes</td>
</tr>
<tr>
<td>Hour</td>
<td>0-23</td>
<td><code>9-17</code> = 9 am to 5 pm</td>
</tr>
<tr>
<td>Day of month</td>
<td>1-31</td>
<td><code>1</code> = the 1st</td>
</tr>
<tr>
<td>Month</td>
<td>1-12</td>
<td><code>*</code> = every month</td>
</tr>
<tr>
<td>Day of week</td>
<td>0-7 (0 and 7 are Sunday)</td>
<td><code>1-5</code> = Monday to Friday</td>
</tr>
</tbody>
</table></div>
<p>Some patterns you will use often:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Schedule</th>
<th>Expression</th>
</tr>
</thead>
<tbody>
<tr>
<td>Every 5 minutes</td>
<td><code>*/5 * * * *</code></td>
</tr>
<tr>
<td>Every hour, on the hour</td>
<td><code>0 * * * *</code></td>
</tr>
<tr>
<td>Every day at 2 am</td>
<td><code>0 2 * * *</code></td>
</tr>
<tr>
<td>Weekdays at 9 am</td>
<td><code>0 9 * * 1-5</code></td>
</tr>
<tr>
<td>First of the month at midnight</td>
<td><code>0 0 1 * *</code></td>
</tr>
</tbody>
</table></div>
<h2 id="test-1-two-instances-run-every-job-twice" tabindex="-1"><a class="header-anchor" href="#test-1-two-instances-run-every-job-twice">Test 1: two instances run every job twice</a></h2>
<p><img src="https://media.getvoroa.com/img/ebbcf3c5ea95b237.webp" alt="Diagram: with one instance each scheduled time runs once; with two instances each runs twice; with two instances and a Redis lock it runs once again, measured over 20 seconds as 20, 40 and 20 runs" width="700" loading="eager" decoding="async"></p>
<p><strong>Setup for all tests:</strong> node-cron 4.6.0 on Node.js 24, a schedule that fires every second so results
show up quickly, and Redis 8.10.3 to count runs. Measured 7 November 2026 on a laptop.</p>
<p>This is a common problem. You scale a web app to two instances for reliability. Each
instance loads the same code, so each one starts its own copy of the schedule.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setup (20 seconds)</th>
<th>Runs</th>
<th>Seconds with more than one run</th>
</tr>
</thead>
<tbody>
<tr>
<td>One instance</td>
<td>20</td>
<td>0</td>
</tr>
<tr>
<td>Two instances</td>
<td><strong>40</strong></td>
<td><strong>20 of 20</strong></td>
</tr>
<tr>
<td>Two instances with a Redis lock</td>
<td>20</td>
<td>0 (the other instance’s attempts were skipped by the lock)</td>
</tr>
</tbody>
</table></div>
<p>With two instances, every scheduled job ran twice. For a cleanup that is harmless. For a daily digest
it means every user gets two emails. For a billing job it means two charges.</p>
<p>The fix is to let only one instance do the work for each scheduled time. A short lock in Redis does it:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">cron.schedule('0 9 * * *', async () =&gt; {
  const slot = new Date().toISOString().slice(0, 16);           // one key per minute
  const won = await redis.set(`lock:digest:${slot}`, '1', 'NX', 'EX', 300);
  if (!won) return;                                              // another instance has it
  await sendDailyDigest();
});
</code></pre>
</div>
<p><a href="https://redis.io/docs/latest/commands/set/"><code>SET ... NX</code></a> succeeds for only one caller, so exactly one instance runs the job. In our test the lock
cut 40 runs back to 20.</p>
<h2 id="test-2-a-slow-job-piles-up" tabindex="-1"><a class="header-anchor" href="#test-2-a-slow-job-piles-up">Test 2: a slow job piles up</a></h2>
<p>Next, a job that takes 2.5 seconds, scheduled every second. In real life, think of a report that
usually takes 4 minutes, scheduled every 5, on a day it takes 7.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setup (15 seconds)</th>
<th>Runs started</th>
<th>Most running at the same time</th>
</tr>
</thead>
<tbody>
<tr>
<td>Default</td>
<td>15</td>
<td><strong>3</strong></td>
</tr>
<tr>
<td><code>noOverlap: true</code></td>
<td>5</td>
<td>1</td>
</tr>
</tbody>
</table></div>
<p>By default, node-cron starts the next run even if the last one has not finished. Three copies of the
job were running at once. If each one reads and writes the same rows, they get in each other’s way.</p>
<p>The <code>noOverlap</code> option skips a run while the previous one is still going:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">cron.schedule('*/5 * * * *', buildReport, { noOverlap: true });
</code></pre>
</div>
<p>That is usually what you want. The cost is that a skipped run is skipped, not delayed.</p>
<h2 id="test-3-a-restart-skips-runs" tabindex="-1"><a class="header-anchor" href="#test-3-a-restart-skips-runs">Test 3: a restart skips runs</a></h2>
<p>Finally, we stopped the only instance for 5 seconds, as a deploy or a crash would, and started it
again.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setup (20-second window)</th>
<th>Runs</th>
</tr>
</thead>
<tbody>
<tr>
<td>Instance down for about 5 seconds</td>
<td>14 (6 scheduled seconds missed)</td>
</tr>
</tbody>
</table></div>
<p>node-cron keeps its schedule in memory. While the process is down, nothing runs, and nothing catches up
when it comes back. A deploy at 1:59 am can silently skip your 2 am backup.</p>
<p>For a job that must run, record when it last succeeded, for example a row in your database. On start-up,
check that time and run the job if it is overdue.</p>
<p><strong>One good result:</strong> timing was accurate. Across the tests, runs fired a median of 7 to 12 ms after the
scheduled second, and the 99th percentile stayed between 15 and 35 ms.</p>
<h2 id="timezones-the-230-pm-surprise" tabindex="-1"><a class="header-anchor" href="#timezones-the-230-pm-surprise">Timezones: the 2:30 pm surprise</a></h2>
<p>node-cron uses the server’s timezone unless you pass one. Most cloud servers and containers run on UTC.
So <code>0 9 * * *</code> fires at 9:00 UTC, which is <strong>2:30 pm in India</strong>.</p>
<p>Always set <code>timezone: 'Asia/Kolkata'</code> if you mean Indian time. India has no daylight saving, so the time
never shifts during the year. Zones with daylight saving can skip or repeat an hour twice a year.</p>
<h2 id="in-process-cron-or-a-separate-cron-job" tabindex="-1"><a class="header-anchor" href="#in-process-cron-or-a-separate-cron-job">In-process cron or a separate cron job?</a></h2>
<p>Running cron inside your web app is fine for small, safe jobs. It gets risky as soon as you run more
than one instance or the job matters.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>node-cron inside the app</th>
<th>A separate cron job, one instance</th>
</tr>
</thead>
<tbody>
<tr>
<td>Duplicate runs when the app scales</td>
<td>Yes, unless you add a lock</td>
<td>No, it does not scale with the app</td>
</tr>
<tr>
<td>Runs while the web app is down</td>
<td>No</td>
<td>Yes, it is a separate process</td>
</tr>
<tr>
<td>Extra cost</td>
<td>None</td>
<td>Usually a small instance or free tier</td>
</tr>
<tr>
<td>Best for</td>
<td>Cache warm-ups, small cleanups</td>
<td>Reports, emails, billing, backups</td>
</tr>
</tbody>
</table></div>
<p>A separate cron job is its own process with one instance, so it does not care how many web instances
you have. Overlap and missed runs (Tests 2 and 3) can still happen there, so keep jobs safe to run twice.</p>
<p>For queued work that should run as soon as possible, a job queue fits better; see
our <a href="/blog/bullmq-redis-job-queue/">Redis job queue guide</a>.</p>
<h2 id="run-a-nodejs-cron-job-on-voroa" tabindex="-1"><a class="header-anchor" href="#run-a-nodejs-cron-job-on-voroa">Run a Node.js cron job on Voroa</a></h2>
<p>On Voroa you can create a <strong>Cron Job</strong> straight from your repository: give it a command such as
<code>node scripts/digest.js</code> and a schedule. Voroa builds the repository into a small service that stays
running and runs your command inside it on schedule. It is separate from your
<a href="/web-services/">web services</a>, so scaling the app never doubles a job.</p>
<p>Two things to plan for. Runs reuse the same running container rather than a fresh one each time, and a
slow run can overlap the next. And if the service is stopped, scheduled runs are skipped, not caught up
later. Keep jobs idempotent and record the last successful run, as in Test 3.</p>
<p>Schedules run in India Standard Time, so <code>0 9 * * *</code> means 9:00 am IST. Setup is in
<a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a>, and
<a href="/docs/worker-or-cron-job/">worker or cron job</a> helps you pick the right type. More on long-running
processes is on the <a href="/background-workers/">background workers page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>How do I run a cron job in Node.js?</strong>
Install <code>node-cron</code>, call <code>cron.schedule(expression, function)</code>, and keep the process running. Use the
<code>timezone</code> option for local time.</p>
<p><strong>Why does my Node.js cron job run twice?</strong>
Usually because the app runs on two instances, or the process started twice. In our test, two instances
ran every job twice. Use a lock or a separate cron job.</p>
<p><strong>Does node-cron run missed jobs after a restart?</strong>
No. It keeps the schedule in memory, so runs during downtime are skipped. Track the last successful run
yourself if a job must not be missed.</p>
<p><strong>How do I stop overlapping runs in node-cron?</strong>
Pass <code>{ noOverlap: true }</code>. A run is skipped while the previous one is still in progress.</p>
<p><strong>What timezone does node-cron use?</strong>
The server’s, usually UTC in the cloud. Pass <code>timezone: 'Asia/Kolkata'</code> for Indian time.</p>
<p><strong>node-cron or a cron job service?</strong>
node-cron for small in-app tasks. A separate cron job for jobs that must not run twice because the app
scaled, or must survive app deploys. Keep them idempotent either way.</p>
<p><em>Tested 7 November 2026 with node-cron 4.6.0 and Node.js 24. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>How to Write a Cron Job in Python: Steps, Code and Traps</title>
      <link>https://getvoroa.com/blog/cron-job-in-python/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/cron-job-in-python/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Python</category>
      <description><![CDATA[How to write a cron job in Python in 6 steps: the crontab line, the venv path, logs and the % trap, plus python-crontab and in-process schedulers, tested.]]></description>
      <content:encoded><![CDATA[<p>To write a cron job in Python, you write a normal script, then add one line to your crontab that runs
it with the full path to your Python interpreter. There are two other ways too: write the crontab from
Python code, or keep a scheduler running inside your program. Each one fails in its own way.</p>
<p>This guide starts with the six steps, then shows all three ways with working code, and the traps we hit
when testing them on Python 3.14: a script that imports fine in your terminal but not under cron, and a
scheduler that drifts.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#how-to-write-a-cron-job-in-python">How to write a cron job in Python</a></li>
<li><a href="#reading-the-five-time-fields">Reading the five time fields</a></li>
<li><a href="#which-way-should-you-pick">Which way should you pick?</a></li>
<li><a href="#way-1-a-crontab-line">Way 1: a crontab line</a></li>
<li><a href="#the-virtualenv-trap-measured">The virtualenv trap, measured</a></li>
<li><a href="#way-2-write-the-crontab-from-python">Way 2: write the crontab from Python</a></li>
<li><a href="#way-3-a-scheduler-inside-your-program">Way 3: a scheduler inside your program</a></li>
<li><a href="#the-schedule-library-drifts">The schedule library drifts</a></li>
<li><a href="#running-it-in-docker">Running it in Docker</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="how-to-write-a-cron-job-in-python" tabindex="-1"><a class="header-anchor" href="#how-to-write-a-cron-job-in-python">How to write a cron job in Python</a></h2>
<p>These steps work on any Linux server or Mac with cron. The example runs <code>digest.py</code> every day at 9 am,
server time.</p>
<ol>
<li>
<p><strong>Write the script and use absolute paths inside it.</strong> Cron starts your job in your home directory,
so a relative path such as <code>open(&quot;data.csv&quot;)</code> points to the wrong place.</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python"># /srv/app/digest.py
from datetime import datetime
from pathlib import Path

HERE = Path(__file__).resolve().parent
(HERE / &quot;digest-last-run.txt&quot;).write_text(f&quot;{datetime.now():%Y-%m-%d %H:%M:%S}\n&quot;)
print(&quot;digest done&quot;)
</code></pre>
</div>
</li>
<li>
<p><strong>Find the full path of the Python that has your packages.</strong> Inside a virtualenv it is
<code>.venv/bin/python</code> in your project. Print it once so you can copy it:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">cd /srv/app &amp;&amp; realpath .venv/bin/python
</code></pre>
</div>
</li>
<li>
<p><strong>Run the script with that path, exactly as cron will.</strong> If this fails, cron will fail too:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">/srv/app/.venv/bin/python /srv/app/digest.py
</code></pre>
</div>
</li>
<li>
<p><strong>Open your crontab</strong> with <code>crontab -e</code>. It opens in your editor; each line is one job.</p>
</li>
<li>
<p><strong>Add one line: the schedule, then the command, with output sent to a log.</strong></p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>0 9 * * * cd /srv/app &amp;&amp; /srv/app/.venv/bin/python digest.py &gt;&gt; /srv/app/digest.log 2&gt;&amp;1
</code></pre>
</div>
</li>
<li>
<p><strong>Save, then check.</strong> <code>crontab -l</code> lists the job. After 9 am, look in <code>digest.log</code> for
<code>digest done</code>. If it is empty, the first error line in that log tells you why.</p>
</li>
</ol>
<p>That is a complete Python cron job. The rest of this page explains each choice in that line, and the
other two ways to schedule Python.</p>
<h2 id="reading-the-five-time-fields" tabindex="-1"><a class="header-anchor" href="#reading-the-five-time-fields">Reading the five time fields</a></h2>
<p>A crontab line starts with five fields, in this order: minute, hour, day of month, month and day of week.
A <code>*</code> means “every”. Common schedules:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Schedule</th>
<th>Runs</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>0 9 * * *</code></td>
<td>Every day at 9:00</td>
</tr>
<tr>
<td><code>*/15 * * * *</code></td>
<td>Every 15 minutes (at :00, :15, :30, :45)</td>
</tr>
<tr>
<td><code>0 * * * *</code></td>
<td>At the start of every hour</td>
</tr>
<tr>
<td><code>30 2 * * 1-5</code></td>
<td>2:30 on weekdays</td>
</tr>
<tr>
<td><code>0 0 1 * *</code></td>
<td>Midnight on the 1st of every month</td>
</tr>
</tbody>
</table></div>
<p>Two details catch people out. A step only counts inside its own field, so <code>*/35</code> in the minute field runs
at :00 and :35, not every 35 minutes. And a <code>%</code> in the command is turned into a newline by cron, so a
command such as <code>date +%F</code> must be written <code>date +\%F</code>. Both rules are in the
<a href="https://man7.org/linux/man-pages/man5/crontab.5.html">crontab(5) manual</a>.</p>
<h2 id="which-way-should-you-pick" tabindex="-1"><a class="header-anchor" href="#which-way-should-you-pick">Which way should you pick?</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Your situation</th>
<th>Use</th>
</tr>
</thead>
<tbody>
<tr>
<td>A script that runs, finishes and exits, on a Linux server</td>
<td>Way 1: a crontab line</td>
</tr>
<tr>
<td>An installer or tool that must add cron entries for users</td>
<td>Way 2: python-crontab</td>
</tr>
<tr>
<td>A long-running app that should run small jobs on a timer</td>
<td>Way 3: an in-process scheduler</td>
</tr>
</tbody>
</table></div>
<p>If you are not sure, use Way 1. The operating system starts your script, so nothing has to stay running.</p>
<h2 id="way-1-a-crontab-line" tabindex="-1"><a class="header-anchor" href="#way-1-a-crontab-line">Way 1: a crontab line</a></h2>
<p>Run <code>crontab -e</code> and add one line. This runs <code>digest.py</code> every day at 9 am, server time:</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>0 9 * * * cd /srv/app &amp;&amp; /srv/app/.venv/bin/python digest.py &gt;&gt; /var/log/digest.log 2&gt;&amp;1
</code></pre>
</div>
<p>Three details in that line prevent most failures. It calls the <strong>virtualenv’s own Python</strong> by its full
path, it changes to the project folder first, and it sends output and errors to a log file. Make sure the
user that owns the crontab can write to that log path; <code>/var/log</code> often needs root, so a log inside your
project folder is the easy choice.</p>
<h2 id="the-virtualenv-trap-measured" tabindex="-1"><a class="header-anchor" href="#the-virtualenv-trap-measured">The virtualenv trap, measured</a></h2>
<p>Cron does not load your shell, so it does not see an activated virtualenv. We reproduced cron’s empty
environment with <code>env -i PATH=/usr/bin:/bin</code> and ran a script whose package was installed only in the
virtualenv:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Command run under a cron-like environment</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>python3 /home/app/job.py</code></td>
<td><code>ModuleNotFoundError: No module named 'schedule'</code></td>
</tr>
<tr>
<td><code>/home/app/.venv/bin/python /home/app/job.py</code></td>
<td>Ran</td>
</tr>
<tr>
<td><code>cd /tmp; /home/app/.venv/bin/python job.py</code></td>
<td><code>can't open file '/tmp/job.py'</code></td>
</tr>
</tbody>
</table></div>
<p>So call the virtualenv’s interpreter directly, not <code>python3</code> and not <code>source activate</code>. And use absolute
paths, or <code>cd</code> first, because cron starts your job in your home directory. Python’s
<a href="https://docs.python.org/3/library/venv.html">venv documentation</a> confirms that running the venv’s
interpreter directly is enough; activation is optional.</p>
<p>Environment variables such as <code>DATABASE_URL</code> are missing for the same reason. Load them inside the script,
for example from a <code>.env</code> file. Our <a href="/blog/cron-job-not-running/">cron troubleshooting checklist</a> covers
the other causes.</p>
<h2 id="way-2-write-the-crontab-from-python" tabindex="-1"><a class="header-anchor" href="#way-2-write-the-crontab-from-python">Way 2: write the crontab from Python</a></h2>
<p>The <code>python-crontab</code> package edits crontab files from code. It is useful when your program installs its
own schedule, for example a CLI tool’s <code>setup</code> command:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python">from crontab import CronTab

cron = CronTab(user=True)
job = cron.new(
    command=&quot;/srv/app/.venv/bin/python /srv/app/digest.py &gt;&gt; /var/log/digest.log 2&gt;&amp;1&quot;,
    comment=&quot;digest&quot;,
)
job.setall(&quot;30 3 * * *&quot;)
cron.write()
</code></pre>
</div>
<p>Tested with python-crontab 3.4.0, it renders this line, with a comment so you can find and remove the job
later:</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>30 3 * * * /srv/app/.venv/bin/python /srv/app/digest.py &gt;&gt; /var/log/digest.log 2&gt;&amp;1 # digest
</code></pre>
</div>
<p>Nothing is saved until <code>cron.write()</code> runs. It is still a normal cron job, so the virtualenv and timezone
rules above apply. Note <code>30 3</code>: on a UTC server that is 9 am in India, as our
<a href="/blog/cron-timezone-ist/">cron timezone guide</a> explains.</p>
<h2 id="way-3-a-scheduler-inside-your-program" tabindex="-1"><a class="header-anchor" href="#way-3-a-scheduler-inside-your-program">Way 3: a scheduler inside your program</a></h2>
<p>If your app already runs all the time, a library can call functions on a timer, with no crontab at all.
The two common choices:</p>
<ul>
<li><strong>schedule</strong>: tiny and readable, <code>schedule.every().day.at(&quot;09:00&quot;).do(job)</code>. No cron expressions, and
no persistence.</li>
<li><strong>APScheduler</strong>: cron expressions, timezones and optional job stores. Our
<a href="/blog/apscheduler/">APScheduler guide</a> covers its restart and duplicate-run traps.</li>
</ul>
<p>For Django or Flask apps that already use Celery, <a href="/blog/celery-beat/">Celery beat</a> is the usual fit.</p>
<p>Every in-process scheduler shares one rule: it runs once per copy of your program. Run the app on two
servers, or with four Gunicorn workers that each start the scheduler, and each job runs two or four times.</p>
<h2 id="the-schedule-library-drifts" tabindex="-1"><a class="header-anchor" href="#the-schedule-library-drifts">The schedule library drifts</a></h2>
<p>We scheduled <code>schedule.every(5).seconds.do(job)</code> with a job that takes 2 seconds, and ran it for 40
seconds with schedule 1.2.2.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Run</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
</tr>
</thead>
<tbody>
<tr>
<td>Started at (seconds)</td>
<td>5.0</td>
<td>12.0</td>
<td>19.1</td>
<td>26.1</td>
<td>33.1</td>
<td>40.1</td>
</tr>
</tbody>
</table></div>
<p>The gap was <strong>7 seconds</strong>, not 5: the library counts the next interval from when the job finished. In 40
seconds that is 6 runs instead of 8. For “every few minutes, roughly” that is fine. For “exactly on the
hour”, use a cron expression instead.</p>
<p><img src="https://media.getvoroa.com/img/150ae8d348c6f42d.webp" alt="Diagram: a 2-second job scheduled every 5 seconds with the schedule library; each run starts 7 seconds after the previous one, so after 40 seconds it has run 6 times instead of 8" width="700" loading="eager" decoding="async"></p>
<h2 id="running-it-in-docker" tabindex="-1"><a class="header-anchor" href="#running-it-in-docker">Running it in Docker</a></h2>
<p>A standard Python image has no cron daemon. Writing a crontab inside the container does nothing unless you
install cron and run it as the main process, which also hides your environment variables from the job.</p>
<p>The simpler options: run the script from the host’s cron with <code>docker run</code>, use an in-process scheduler in
a container that stays up, or use your platform’s scheduled job feature.</p>
<h2 id="python-cron-jobs-on-voroa" tabindex="-1"><a class="header-anchor" href="#python-cron-jobs-on-voroa">Python cron jobs on Voroa</a></h2>
<p>On Voroa, a <strong>Cron Job</strong> builds your repository and runs a command such as <code>python digest.py</code> on a
schedule, read in India Standard Time. Your environment variables are already set, so the virtualenv and
<code>.env</code> traps above do not apply.</p>
<p>The command runs inside a service that stays running, so a slow run can overlap the next, and runs are
skipped while the service is stopped. See <a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a> and the
<a href="/docs/deploy-python-django/">Django deploy guide</a>. Long-running consumers go on the
<a href="/background-workers/">background workers page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>How do I create a cron job for a Python script?</strong>
Run <code>crontab -e</code>, add a line with the schedule and the full path to your Python and script, for example
<code>0 9 * * * /full/path/to/.venv/bin/python /full/path/to/script.py &gt;&gt; /full/path/to/script.log 2&gt;&amp;1</code>, and
save. <code>crontab -l</code> confirms it is installed.</p>
<p><strong>Why does my Python script work in the terminal but not in cron?</strong>
Usually the wrong interpreter. Cron uses the system <code>python3</code>, which cannot see packages in your
virtualenv. Call the venv’s Python by its full path.</p>
<p><strong>Do I need to activate the virtualenv in cron?</strong>
No. Running <code>.venv/bin/python</code> directly uses the virtualenv’s packages.</p>
<p><strong>Do I need a shebang line or <code>chmod +x</code>?</strong>
Not if the crontab line starts with the interpreter, as in the examples here. You only need both if the
line calls the script on its own, such as <code>/srv/app/digest.py</code>.</p>
<p><strong>Can Python run cron jobs without crontab?</strong>
Yes, with an in-process library such as schedule or APScheduler, as long as your program keeps running.</p>
<p><strong>How do I see the output of a Python cron job?</strong>
Redirect it: <code>&gt;&gt; /srv/app/job.log 2&gt;&amp;1</code>. Without that, cron mails the output to the crontab’s owner, or it
is lost if no mail is set up.</p>
<p><em>Tested on Python 3.14 with schedule 1.2.2 and python-crontab 3.4.0 on 7 November 2026; steps and crontab
rules reviewed 14 November 2026. Found something out of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Cron Job Not Running? A 12-Point Checklist That Finds It</title>
      <link>https://getvoroa.com/blog/cron-job-not-running/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/cron-job-not-running/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Cron job not running? Work through 12 checks, from the daemon and the PATH to the % sign, timezones and overlapping runs, plus traps on hosted platforms.]]></description>
      <content:encoded><![CDATA[<p>A cron job not running is one of the most annoying problems in operations, because nothing tells you.
There is no error on screen, and the job simply does not happen.</p>
<p>The cause is almost always one of a short list. Work through the checks below in order. The first few
take seconds, and together they cover the vast majority of cases.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#first-did-it-run-at-all">First: did it run at all?</a></li>
<li><a href="#checks-1-4-is-cron-set-up">Checks 1-4: is cron set up?</a></li>
<li><a href="#checks-5-8-does-the-command-work-under-cron">Checks 5-8: does the command work under cron?</a></li>
<li><a href="#checks-9-12-is-it-running-at-the-wrong-time-or-twice">Checks 9-12: is it running at the wrong time, or twice?</a></li>
<li><a href="#extra-traps-on-hosted-platforms">Extra traps on hosted platforms</a></li>
<li><a href="#a-crontab-line-that-avoids-most-of-this">A crontab line that avoids most of this</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="first-did-it-run-at-all" tabindex="-1"><a class="header-anchor" href="#first-did-it-run-at-all">First: did it run at all?</a></h2>
<p>Before changing anything, find out whether cron started the job and it failed, or never started it.</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">grep CRON /var/log/syslog          # Debian and Ubuntu
journalctl -u cron --since today   # systemd (the unit is crond on RHEL-family systems)
</code></pre>
</div>
<p>A line with your command means cron started it, so the problem is inside the command (checks 5 to 8).
No line means cron never ran it (checks 1 to 4 and 9).</p>
<p><img src="https://media.getvoroa.com/img/fe5a768471950f82.webp" alt="Diagram: a decision tree for a cron job that is not running - check the cron log; if cron started the job, debug the command (PATH, environment, permissions, output); if it never started, check the daemon, the crontab, the syntax and the timezone" width="700" loading="eager" decoding="async"></p>
<h2 id="checks-1-4-is-cron-set-up" tabindex="-1"><a class="header-anchor" href="#checks-1-4-is-cron-set-up">Checks 1-4: is cron set up?</a></h2>
<p><strong>1. Is the cron daemon running?</strong> On Ubuntu, <code>systemctl status cron</code>. In a minimal Docker container
there is usually no cron daemon at all unless you start one.</p>
<p><strong>2. Is the job in the crontab you think?</strong> Run <code>crontab -l</code> as the same user. A job added with
<code>sudo crontab -e</code> lives in root’s crontab, not yours.</p>
<p><strong>3. Is the syntax right?</strong> Standard cron takes five time fields. A sixth field for seconds, copied
from a library such as node-cron, makes the line invalid. Files in <code>/etc/cron.d/</code> also need a user
name before the command.</p>
<p><strong>4. Does the file end with a newline?</strong> Cron can ignore a last line with no newline after it. Press
Enter after the final job.</p>
<h2 id="checks-5-8-does-the-command-work-under-cron" tabindex="-1"><a class="header-anchor" href="#checks-5-8-does-the-command-work-under-cron">Checks 5-8: does the command work under cron?</a></h2>
<p><strong>5. The PATH is short.</strong> Cron runs with a minimal PATH, often just <code>/usr/bin:/bin</code>. A command such as
<code>node</code>, <code>python3</code> or <code>php</code> installed elsewhere is not found. Use full paths (<code>which node</code> shows it), or
set <code>PATH=</code> at the top of the crontab.</p>
<p><strong>6. Your environment is not loaded.</strong> Cron does not read <code>.bashrc</code> or <code>.profile</code>. Variables such as
<code>DATABASE_URL</code> that you export in your shell are missing. Load them explicitly in the script.</p>
<p><strong>7. The <code>%</code> sign is special.</strong> In a crontab, <code>%</code> means “new line”. A command such as
<code>date +%Y-%m-%d</code> breaks unless you write <code>\%</code>. Moving the command into a script avoids it.</p>
<p><strong>8. Permissions and the working directory.</strong> The script must be executable (<code>chmod +x</code>), and cron
starts in your home directory. A script that opens <code>config.json</code> by relative path fails. <code>cd</code> to the
right folder first.</p>
<h2 id="checks-9-12-is-it-running-at-the-wrong-time-or-twice" tabindex="-1"><a class="header-anchor" href="#checks-9-12-is-it-running-at-the-wrong-time-or-twice">Checks 9-12: is it running at the wrong time, or twice?</a></h2>
<p><strong>9. The timezone.</strong> Cron uses the server’s timezone, usually UTC in the cloud. A job meant for 9 am in
India written as <code>0 9 * * *</code> runs at 2:30 pm. Write <code>30 3 * * *</code>, or change the server timezone.</p>
<p><strong>10. Day-of-month and day-of-week.</strong> If both are set, standard cron runs when either matches.
<code>0 9 1 * 1</code> runs on the 1st and every Monday, which looks like “running at random”.</p>
<p><strong>11. Overlapping runs.</strong> A slow run is still going when the next one starts. They clash, and one
seems to fail. Wrap the command in <code>flock -n /tmp/job.lock</code> so a second copy is skipped.</p>
<p><strong>12. It runs on two machines.</strong> The same crontab on two servers, or an in-app scheduler in two
instances, runs every job twice. We measured exactly that with
<a href="/blog/cron-job-in-node-js/">node-cron</a> and <a href="/blog/apscheduler/">APScheduler</a>.</p>
<h2 id="extra-traps-on-hosted-platforms" tabindex="-1"><a class="header-anchor" href="#extra-traps-on-hosted-platforms">Extra traps on hosted platforms</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Platform</th>
<th>What stops a job running</th>
</tr>
</thead>
<tbody>
<tr>
<td>GitHub Actions</td>
<td>Minimum 5-minute interval; delays at busy times; disabled after 60 days without activity in public repositories</td>
</tr>
<tr>
<td>Vercel</td>
<td>Only production deployments run crons; redirects (such as <code>trailingSlash</code>) end the run; Hobby allows one run a day on legacy Cron Jobs</td>
</tr>
<tr>
<td>Heroku Scheduler</td>
<td>A run is stopped at its interval; runs are best-effort</td>
</tr>
<tr>
<td>Celery beat</td>
<td>Two beat processes double tasks; a lost schedule file loses last-run times</td>
</tr>
<tr>
<td>Voroa</td>
<td>The parent service must be running; runs are skipped while it is stopped, including after a free workspace uses its monthly free hours</td>
</tr>
</tbody>
</table></div>
<p>Sources: the <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows">GitHub Actions schedule docs</a>
and each platform’s own documentation. More detail per platform is in our posts on
<a href="/blog/vercel-cron-jobs/">Vercel cron jobs</a> and <a href="/blog/heroku-scheduler/">Heroku Scheduler</a>.</p>
<h2 id="a-crontab-line-that-avoids-most-of-this" tabindex="-1"><a class="header-anchor" href="#a-crontab-line-that-avoids-most-of-this">A crontab line that avoids most of this</a></h2>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
CRON_TZ=Asia/Kolkata

30 9 * * * cd /srv/app &amp;&amp; flock -n /tmp/digest.lock ./bin/digest &gt;&gt; /var/log/digest.log 2&gt;&amp;1
</code></pre>
</div>
<p>It sets a shell and a PATH, runs from the right folder, takes a lock, and logs both output and errors.
<code>CRON_TZ</code> is honoured by cronie (Fedora, RHEL); Ubuntu’s default cron ignores it, so check yours.</p>
<h2 id="on-voroa" tabindex="-1"><a class="header-anchor" href="#on-voroa">On Voroa</a></h2>
<p>A Voroa <strong>Cron Job</strong> or <strong>Scheduled Task</strong> runs your command inside a service that stays running,
with your environment variables already set and schedules in India Standard Time. That removes checks
5, 6 and 9, though the container’s own clock may still read UTC if your job reads the date.</p>
<p>Voroa does not show per-run output for a scheduled command, so make the job record its result
somewhere you can check, such as a database row. See
<a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a> and
<a href="/docs/worker-or-cron-job/">worker or cron job</a>. Long-running processes are covered on the
<a href="/background-workers/">background workers page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Why is my cron job not running?</strong>
Most often: a missing PATH, a missing environment variable, a timezone difference, or the job is in a
different user’s crontab. Check the cron log first to see whether it started at all.</p>
<p><strong>How do I check if cron ran my job?</strong>
Look for your command in <code>/var/log/syslog</code> (Debian, Ubuntu) or <code>journalctl -u cron</code>, and send the job’s
own output to a log file.</p>
<p><strong>Why does my script work in the terminal but not in cron?</strong>
Cron has a short PATH, does not load your shell profile, and starts in your home directory. Use full
paths and load your variables in the script.</p>
<p><strong>Why does my cron job run at the wrong time?</strong>
The server is probably on UTC. For 9 am IST on a UTC server, write <code>30 3 * * *</code>.</p>
<p><strong>How do I stop a cron job running twice?</strong>
Use <code>flock -n</code> against overlap, and make sure only one machine or one scheduler instance runs the job.</p>
<p><strong>Does cron run if the server was off?</strong>
No. Standard cron does not catch up missed runs. systemd timers can, with <code>Persistent=true</code>.</p>
<p><em>Updated 7 November 2026. Found something out of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Cron Timezone: Running Cron Jobs in IST on a UTC Server</title>
      <link>https://getvoroa.com/blog/cron-timezone-ist/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/cron-timezone-ist/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Cron timezone for India: converting IST to UTC cron, the early-morning day shift, the first-of-month trap, and how each platform and library sets the zone.]]></description>
      <content:encoded><![CDATA[<p>Cron timezone problems look like random failures: a report arrives at 2:30 pm instead of 9 am, or a
Monday job fires on Sunday night. The cause is almost always the same. Your schedule is read in UTC, and
you wrote it in Indian time.</p>
<p>This guide shows how to convert correctly, the two traps that a simple “subtract 5:30” misses, and how
each platform and library lets you set the zone. The conversions below were generated with Python’s
<code>zoneinfo</code>, not worked out by hand.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#which-timezone-does-cron-use">Which timezone does cron use?</a></li>
<li><a href="#ist-to-utc-conversion-table">IST to UTC conversion table</a></li>
<li><a href="#trap-1-before-530-am-ist-the-day-changes">Trap 1: before 5:30 am IST, the day changes</a></li>
<li><a href="#trap-2-the-first-of-the-month">Trap 2: the first of the month</a></li>
<li><a href="#setting-the-timezone-platform-by-platform">Setting the timezone, platform by platform</a></li>
<li><a href="#setting-it-in-code">Setting it in code</a></li>
<li><a href="#the-schedule-and-the-clock-are-not-the-same">The schedule and the clock are not the same</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="which-timezone-does-cron-use" tabindex="-1"><a class="header-anchor" href="#which-timezone-does-cron-use">Which timezone does cron use?</a></h2>
<p>Classic Linux cron uses the server’s system timezone. Check it with:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">timedatectl          # or: date
</code></pre>
</div>
<p>Most cloud servers and containers are set to UTC. Most hosted schedulers also use UTC by default. India
Standard Time is UTC plus 5 hours 30 minutes, all year: India has no daylight saving, so the offset never
changes.</p>
<h2 id="ist-to-utc-conversion-table" tabindex="-1"><a class="header-anchor" href="#ist-to-utc-conversion-table">IST to UTC conversion table</a></h2>
<p>For a job you want on Monday in India:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>You want (IST, Monday)</th>
<th>UTC time</th>
<th>Cron line on a UTC server</th>
</tr>
</thead>
<tbody>
<tr>
<td>Midnight</td>
<td>Sunday 18:30</td>
<td><code>30 18 * * 0</code></td>
</tr>
<tr>
<td>2:00 am</td>
<td>Sunday 20:30</td>
<td><code>30 20 * * 0</code></td>
</tr>
<tr>
<td>5:29 am</td>
<td>Sunday 23:59</td>
<td><code>59 23 * * 0</code></td>
</tr>
<tr>
<td>5:30 am</td>
<td>Monday 00:00</td>
<td><code>0 0 * * 1</code></td>
</tr>
<tr>
<td>9:00 am</td>
<td>Monday 03:30</td>
<td><code>30 3 * * 1</code></td>
</tr>
<tr>
<td>9:30 am</td>
<td>Monday 04:00</td>
<td><code>0 4 * * 1</code></td>
</tr>
<tr>
<td>Noon</td>
<td>Monday 06:30</td>
<td><code>30 6 * * 1</code></td>
</tr>
<tr>
<td>6:00 pm</td>
<td>Monday 12:30</td>
<td><code>30 12 * * 1</code></td>
</tr>
<tr>
<td>11:30 pm</td>
<td>Monday 18:00</td>
<td><code>0 18 * * 1</code></td>
</tr>
</tbody>
</table></div>
<p>For daily jobs drop the last field: 9 am IST every day is <code>30 3 * * *</code>.</p>
<p>Ranges convert the same way. “Every hour from 9 am to 6 pm IST” is <code>30 3-12 * * *</code>. Because of the half
hour, it fires at 30 minutes past each UTC hour, which is on the hour in India.</p>
<h2 id="trap-1-before-530-am-ist-the-day-changes" tabindex="-1"><a class="header-anchor" href="#trap-1-before-530-am-ist-the-day-changes">Trap 1: before 5:30 am IST, the day changes</a></h2>
<p>Look at the top of the table. Anything earlier than 5:30 am in India happens on the <strong>previous day</strong> in
UTC.</p>
<p><img src="https://media.getvoroa.com/img/dd96d23b890db063.webp" alt="Diagram: a 24-hour IST day lined up against UTC; IST midnight to 5:29 am falls on the previous UTC day, so a Monday 2 am IST job must be written for Sunday in UTC" width="700" loading="eager" decoding="async"></p>
<p>So a “Monday 2 am” backup written as <code>30 20 * * 1</code> runs on <strong>Tuesday</strong> 2 am in India. The correct line is
<code>30 20 * * 0</code>, with Sunday as the day. Weekday ranges shift too: “2 am IST, Monday to Friday” becomes
<code>30 20 * * 0-4</code>.</p>
<p>This is the most common reason a weekly job seems to run on the wrong day.</p>
<h2 id="trap-2-the-first-of-the-month" tabindex="-1"><a class="header-anchor" href="#trap-2-the-first-of-the-month">Trap 2: the first of the month</a></h2>
<p>The same shift hits dates. “2 am IST on the 1st of every month” is 20:30 UTC on the <strong>last day of the
previous month</strong>.</p>
<p>Standard cron cannot say “last day of the month”, because months have 28 to 31 days. You have two
options: run the job at 5:30 am IST or later, so the date stays the same in UTC, or use a scheduler that
lets you set the timezone to <code>Asia/Kolkata</code>.</p>
<h2 id="setting-the-timezone-platform-by-platform" tabindex="-1"><a class="header-anchor" href="#setting-the-timezone-platform-by-platform">Setting the timezone, platform by platform</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Where it runs</th>
<th>Default zone</th>
<th>Can you change it?</th>
</tr>
</thead>
<tbody>
<tr>
<td>Your own Linux server</td>
<td>The system timezone</td>
<td>Change the system zone, or use <code>CRON_TZ</code> where your cron supports it (cronie does; Ubuntu’s default cron does not)</td>
</tr>
<tr>
<td>GitHub Actions <code>schedule</code></td>
<td>UTC</td>
<td>Its docs now describe setting an IANA timezone</td>
</tr>
<tr>
<td>Vercel Cron Jobs</td>
<td>UTC only</td>
<td>The newer Schedules feature adds timezones</td>
</tr>
<tr>
<td>Heroku Scheduler</td>
<td>UTC</td>
<td>No; adjust the time yourself</td>
</tr>
<tr>
<td>Render, Railway cron</td>
<td>UTC</td>
<td>Not documented as configurable</td>
</tr>
<tr>
<td>Voroa Cron Job or Scheduled Task</td>
<td>IST</td>
<td>It is IST already</td>
</tr>
</tbody>
</table></div>
<p>Sources: each platform’s documentation, checked on 7 November 2026, including the
<a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows">GitHub Actions events page</a>.
Platform details are in our posts on <a href="/blog/vercel-cron-jobs/">Vercel cron jobs</a> and
<a href="/blog/heroku-scheduler/">Heroku Scheduler</a>.</p>
<h2 id="setting-it-in-code" tabindex="-1"><a class="header-anchor" href="#setting-it-in-code">Setting it in code</a></h2>
<p>In-app schedulers take a timezone option. Use the IANA name <code>Asia/Kolkata</code>; avoid fixed offsets such as
<code>+05:30</code> where a name is accepted.</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// node-cron
cron.schedule('0 9 * * *', job, { timezone: 'Asia/Kolkata' });
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python"># APScheduler
BlockingScheduler(timezone=&quot;Asia/Kolkata&quot;)

# Celery beat
app.conf.timezone = &quot;Asia/Kolkata&quot;
</code></pre>
</div>
<p>Without the option, node-cron and APScheduler fall back to the server’s zone, usually UTC. Celery falls
back to UTC whatever the server’s zone is. Our tests of
<a href="/blog/cron-job-in-node-js/">node-cron</a>, <a href="/blog/apscheduler/">APScheduler</a> and
<a href="/blog/celery-beat/">Celery beat</a> cover the other things these schedulers get wrong.</p>
<h2 id="the-schedule-and-the-clock-are-not-the-same" tabindex="-1"><a class="header-anchor" href="#the-schedule-and-the-clock-are-not-the-same">The schedule and the clock are not the same</a></h2>
<p>Setting the schedule’s timezone decides <strong>when</strong> the job starts. It does not change the clock your code
reads inside the job.</p>
<p>A job scheduled for 9 am IST can still see a UTC time from <code>new Date().getHours()</code> or a naive <code>datetime.now()</code> if the
container runs on UTC. If your job names a file by date or picks “yesterday’s” records, set the zone in
the code too, or use timezone-aware functions with <code>Asia/Kolkata</code>.</p>
<h2 id="cron-jobs-in-ist-on-voroa" tabindex="-1"><a class="header-anchor" href="#cron-jobs-in-ist-on-voroa">Cron jobs in IST on Voroa</a></h2>
<p>Voroa’s <strong>Cron Jobs</strong> and <strong>Scheduled Tasks</strong> read schedules in India Standard Time, so <code>0 9 * * *</code> means
9:00 am IST and <code>0 2 * * 1</code> means Monday 2 am IST, with no conversion.</p>
<p>The container’s own clock may still be UTC, as above, so read the date carefully inside the job. Runs
are skipped while the service is stopped, and a slow run can overlap the next, so keep jobs safe to run
twice. See <a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a>, and the
<a href="/background-workers/">background workers page</a> for long-running processes.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>What timezone does cron use?</strong>
The server’s system timezone. On most cloud servers and hosted schedulers that is UTC.</p>
<p><strong>How do I convert IST to UTC for cron?</strong>
Subtract 5 hours 30 minutes. 9 am IST is <code>30 3 * * *</code>. For times before 5:30 am IST, also move the day
back by one.</p>
<p><strong>Why does my Monday cron job run on Tuesday?</strong>
It is set for early morning IST but written with Monday in UTC. Early IST hours are the previous UTC day,
so write Sunday.</p>
<p><strong>Can I set a timezone in a crontab?</strong>
With cronie, yes, using <code>CRON_TZ=Asia/Kolkata</code>. Ubuntu’s default cron ignores it, so change the system
timezone instead.</p>
<p><strong>Does India have daylight saving time?</strong>
No. IST is UTC plus 5:30 all year, so a converted schedule never needs adjusting.</p>
<p><strong>Which timezone name should I use for India?</strong>
<code>Asia/Kolkata</code>. It is the IANA name that libraries and platforms accept.</p>
<p><em>Conversions generated with Python zoneinfo on 7 November 2026. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>ElastiCache Pricing in India 2026: Mumbai Costs in Rupees</title>
      <link>https://getvoroa.com/blog/elasticache-pricing-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/elasticache-pricing-india/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[ElastiCache pricing for the Mumbai region in rupees: node sizes, serverless minimums, Valkey vs Redis OSS, replicas and GST, from AWS's own price list.]]></description>
      <content:encoded><![CDATA[<p>ElastiCache pricing looks simple on the AWS page and gets confusing the moment you try to work out a
monthly bill in rupees. This guide is for developers and founders in India pricing a managed Redis or
Valkey cache on AWS. Every number below comes from AWS’s own published price list for the Mumbai region
(<code>ap-south-1</code>), dated 28 October 2026, converted to rupees at a stated rate, with the parts that change
the bill most: which engine you pick, serverless minimums, replicas and GST.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#how-elasticache-charges">How ElastiCache charges</a></li>
<li><a href="#node-prices-in-mumbai-in-rupees">Node prices in Mumbai, in rupees</a></li>
<li><a href="#serverless-the-minimum-decides-small-bills">Serverless: the minimum decides small bills</a></li>
<li><a href="#valkey-or-redis-oss-the-same-cache-a-different-price">Valkey or Redis OSS: the same cache, a different price</a></li>
<li><a href="#what-a-real-setup-costs">What a real setup costs</a></li>
<li><a href="#the-costs-that-are-not-on-the-price-card">The costs that are not on the price card</a></li>
<li><a href="#is-there-a-free-tier">Is there a free tier?</a></li>
<li><a href="#alternatives-priced-in-rupees">Alternatives priced in rupees</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="how-elasticache-charges" tabindex="-1"><a class="header-anchor" href="#how-elasticache-charges">How ElastiCache charges</a></h2>
<p>There are two ways to buy it:</p>
<ol>
<li><strong>Node-based (self-designed) clusters.</strong> You pick an instance size and pay for every hour each node
runs, whether it is busy or idle. A replica for failover is another node at the same price.</li>
<li><strong>Serverless.</strong> No sizes to pick. You pay for the data you store, per GB-hour, plus the requests you
send, in ElastiCache Processing Units (ECPUs). Each cache has a minimum amount of data you are billed
for, even if you store less.</li>
</ol>
<p>Backup storage beyond the free allowance, and data sent between Availability Zones or out of AWS, are
billed separately.</p>
<p><strong>How we converted.</strong> AWS publishes Mumbai prices in US dollars. We multiplied by 730 hours for a month
and converted at Rs 93.92 per dollar, the European Central Bank reference rate for 6 November 2026.
If your account is billed by AWS India, your invoice is in rupees at AWS’s own rate and adds 18% GST,
so treat the rupee figures as close estimates before GST. The dollar prices are exact, from the
<a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache pricing page</a> and AWS’s price list file.</p>
<h2 id="node-prices-in-mumbai-in-rupees" tabindex="-1"><a class="header-anchor" href="#node-prices-in-mumbai-in-rupees">Node prices in Mumbai, in rupees</a></h2>
<p>On-demand, per node, Mumbai region, before GST.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Node</th>
<th>Memory</th>
<th>Valkey per hour</th>
<th>Valkey per month</th>
<th>Redis OSS per hour</th>
<th>Redis OSS per month</th>
</tr>
</thead>
<tbody>
<tr>
<td>cache.t4g.micro</td>
<td>0.5 GiB</td>
<td>$0.0160</td>
<td>about Rs 1,097</td>
<td>$0.0200</td>
<td>about Rs 1,371</td>
</tr>
<tr>
<td>cache.t4g.small</td>
<td>1.37 GiB</td>
<td>$0.0328</td>
<td>about Rs 2,249</td>
<td>$0.0410</td>
<td>about Rs 2,811</td>
</tr>
<tr>
<td>cache.t4g.medium</td>
<td>3.09 GiB</td>
<td>$0.0648</td>
<td>about Rs 4,443</td>
<td>$0.0810</td>
<td>about Rs 5,553</td>
</tr>
<tr>
<td>cache.m7g.large</td>
<td>6.38 GiB</td>
<td>$0.1312</td>
<td>about Rs 8,995</td>
<td>$0.1640</td>
<td>about Rs 11,244</td>
</tr>
<tr>
<td>cache.r7g.large</td>
<td>13.07 GiB</td>
<td>$0.1792</td>
<td>about Rs 12,286</td>
<td>$0.2240</td>
<td>about Rs 15,358</td>
</tr>
</tbody>
</table></div>
<p>Two things to know about these sizes. The memory figure is the whole node, and part of it is needed by
the engine itself and for background saves, so plan to fill no more than about three-quarters of it.
And the <code>t4g</code> sizes are burstable: they suit steady light traffic, not sustained heavy load.</p>
<p><img src="https://media.getvoroa.com/img/f2807343044aa26a.webp" alt="Bar chart: monthly cost in rupees in the Mumbai region for one node, Valkey compared with Redis OSS - t4g.micro about 1,097 vs 1,371, t4g.small 2,249 vs 2,811, t4g.medium 4,443 vs 5,553, m7g.large 8,995 vs 11,244, r7g.large 12,286 vs 15,358" width="700" loading="eager" decoding="async"></p>
<h2 id="serverless-the-minimum-decides-small-bills" tabindex="-1"><a class="header-anchor" href="#serverless-the-minimum-decides-small-bills">Serverless: the minimum decides small bills</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Mumbai, serverless</th>
<th>Valkey</th>
<th>Redis OSS</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data stored, per GB-hour</td>
<td>$0.054</td>
<td>$0.081</td>
</tr>
<tr>
<td>Requests, per million ECPUs</td>
<td>$0.0015</td>
<td>$0.0022</td>
</tr>
<tr>
<td>Minimum data billed per cache</td>
<td>100 MB</td>
<td>1 GB</td>
</tr>
<tr>
<td>Minimum per month, from data alone</td>
<td>about $3.94 (about Rs 370)</td>
<td>about $59.13 (about Rs 5,553)</td>
</tr>
</tbody>
</table></div>
<p>For a small app this table is the whole story. A Redis OSS serverless cache costs at least about
Rs 5,553 per month before GST even if it holds one key, because you pay for 1 GB regardless. The same
cache on Valkey starts at about Rs 370. Requests are cheap at small scale: 100 million ECPUs cost about
$0.15 on Valkey.</p>
<p>Above the minimum, serverless costs about $39.42 (about Rs 3,702) per GB stored per month on Valkey.
A cache that holds more than a few GB around the clock is usually cheaper on nodes; one that is small or
spiky is usually cheaper serverless.</p>
<h2 id="valkey-or-redis-oss-the-same-cache-a-different-price" tabindex="-1"><a class="header-anchor" href="#valkey-or-redis-oss-the-same-cache-a-different-price">Valkey or Redis OSS: the same cache, a different price</a></h2>
<p>On every line above, Valkey is cheaper: 20% less on nodes, 33% less on serverless storage and 32% less on requests,
and a minimum one-tenth the size. Both speak the same protocol, so your client library does not change.</p>
<p>The reasons to stay on Redis OSS are narrow: data files from Redis 7.4 or later that you need to load
directly, or features that exist only in newer Redis releases. We covered the differences, with a
benchmark, in <a href="/blog/valkey-vs-redis/">Valkey vs Redis</a>. For most caches on AWS today, Valkey is the
default choice on price alone.</p>
<h2 id="what-a-real-setup-costs" tabindex="-1"><a class="header-anchor" href="#what-a-real-setup-costs">What a real setup costs</a></h2>
<p>Three common shapes, Mumbai, Valkey, before GST:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setup</th>
<th>What you run</th>
<th>About per month</th>
</tr>
</thead>
<tbody>
<tr>
<td>Side project or staging</td>
<td>1 x cache.t4g.micro, no replica</td>
<td>Rs 1,097</td>
</tr>
<tr>
<td>Production cache with failover</td>
<td>1 primary + 1 replica, cache.t4g.small</td>
<td>Rs 4,498</td>
</tr>
<tr>
<td>Busy app, 1 primary + 1 replica</td>
<td>cache.m7g.large x 2</td>
<td>Rs 17,991</td>
</tr>
</tbody>
</table></div>
<p>Add 18% GST on an AWS India invoice. A replica doubles the node cost, and with Multi-AZ automatic failover
turned on it takes over when the primary fails; without one, a node failure means downtime and an empty cache.</p>
<h2 id="the-costs-that-are-not-on-the-price-card" tabindex="-1"><a class="header-anchor" href="#the-costs-that-are-not-on-the-price-card">The costs that are not on the price card</a></h2>
<ul>
<li><strong>Reaching it from outside AWS.</strong> ElastiCache is reached from inside your AWS network (VPC). If your
app runs somewhere else, you need extra networking, and its cost and latency, to connect at all.</li>
<li><strong>Data transfer.</strong> Traffic between your app and the cache in the same Availability Zone is free;
traffic across zones is billed per GB in each direction. A replica in another zone, or an app spread
across zones, adds to it.</li>
<li><strong>Backups.</strong> Snapshots beyond the free allowance cost $0.085 per GB-month in Mumbai.</li>
<li><strong>Your time.</strong> Sizing nodes, setting parameter groups, VPC security rules and upgrade windows are
yours to manage on the node-based option.</li>
</ul>
<h2 id="is-there-a-free-tier" tabindex="-1"><a class="header-anchor" href="#is-there-a-free-tier">Is there a free tier?</a></h2>
<p>New AWS accounts created since 15 July 2025 get credits rather than a permanent free cache: $100 at
sign-up and up to $100 more, which AWS says can be used towards ElastiCache, for up to 6 months, with a card required. After that
you pay the prices above. For a free cache with no end date, our
<a href="/blog/free-redis-hosting/">free Redis hosting comparison</a> lists eight options and the catch in each.</p>
<h2 id="alternatives-priced-in-rupees" tabindex="-1"><a class="header-anchor" href="#alternatives-priced-in-rupees">Alternatives priced in rupees</a></h2>
<p>If your app does not run on AWS, or you want a bill in rupees with no conversion, a managed store
outside AWS can be simpler. For comparison, Voroa’s key value sizes, set in rupees and billed with a GST
invoice:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Voroa key value size</th>
<th>Memory</th>
<th>CPU (shared)</th>
<th>Price before GST</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cache Nano (free)</td>
<td>128 MB</td>
<td>0.05</td>
<td>Rs 0</td>
</tr>
<tr>
<td>Cache Nano Plus</td>
<td>128 MB</td>
<td>0.1</td>
<td>Rs 249 per month</td>
</tr>
<tr>
<td>Cache Starter</td>
<td>256 MB</td>
<td>0.1</td>
<td>Rs 425 per month</td>
</tr>
<tr>
<td>Cache Standard</td>
<td>1 GB</td>
<td>0.25</td>
<td>Rs 1,349 per month</td>
</tr>
<tr>
<td>Cache Standard Plus</td>
<td>3 GB</td>
<td>0.5</td>
<td>Rs 3,499 per month</td>
</tr>
<tr>
<td>Cache Pro</td>
<td>5 GB</td>
<td>0.5</td>
<td>Rs 5,699 per month</td>
</tr>
<tr>
<td>Cache Pro Plus</td>
<td>10 GB</td>
<td>2</td>
<td>Rs 10,499 per month</td>
</tr>
</tbody>
</table></div>
<p>The same caution as for ElastiCache applies, more strongly: the memory figure is the whole instance,
and Voroa can hold back up to 40% of it for the nightly save, so a 1 GB store may hold about 600 MB of
data. Compare usable data, not the label, when you weigh the two.</p>
<p>These are single instances with data written to disk and a nightly snapshot, not a primary-plus-replica
cluster, so compare them with a single ElastiCache node rather than a failover pair. They run in Mumbai
next to the <a href="/web-services/">web services</a> you deploy, reached over a private network. The full details
are on the <a href="/managed-redis-india/">managed Redis page</a> and in the <a href="/docs/key-value/">Key Value guide</a>.
Our <a href="/blog/bullmq-redis-job-queue/">Redis job queue guide</a> explains why a queue needs a store that does
not evict keys, which matters whichever host you choose.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>How much does ElastiCache cost per month in India?</strong>
The smallest Mumbai node, cache.t4g.micro on Valkey, is about Rs 1,097 per month before GST. A serverless
Valkey cache starts at about Rs 370 per month. A production pair with failover on cache.t4g.small is about
Rs 4,498.</p>
<p><strong>Is ElastiCache Valkey cheaper than Redis?</strong>
Yes. In Mumbai, Valkey nodes cost 20% less, serverless storage 33% less and requests 32% less, and the serverless
minimum is 100 MB instead of 1 GB.</p>
<p><strong>Does ElastiCache charge GST in India?</strong>
Accounts billed by AWS India are invoiced in rupees with 18% GST added.</p>
<p><strong>Is ElastiCache serverless cheaper than nodes?</strong>
For small or spiky caches, usually yes, especially on Valkey. For a cache holding several GB all day,
nodes are usually cheaper.</p>
<p><strong>Can I connect to ElastiCache from outside AWS?</strong>
Not directly. It is reached from inside your AWS network, so an app hosted elsewhere needs extra
networking to connect.</p>
<p><strong>Is there an ElastiCache free tier in 2026?</strong>
New accounts get up to $200 in AWS credits for up to 6 months instead of a permanent free tier.</p>
<p><em>Prices from AWS’s Mumbai price list published 28 October 2026; conversion at Rs 93.92 per US dollar
(ECB reference rate, 6 November 2026). Prices change; check the AWS page before you commit. Found
something out of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Free Redis Hosting 2026: 8 Options Compared from India</title>
      <link>https://getvoroa.com/blog/free-redis-hosting/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/free-redis-hosting/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Free tier</category>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Free Redis hosting compared: Upstash, Redis Cloud, Aiven Valkey, Render and 4 more, on memory, limits, deletion rules and latency we measured from India.]]></description>
      <content:encoded><![CDATA[<p>Every free Redis tier is small, and that is not the catch. The catch is what happens when you are not
looking: a database deleted after 14 quiet days, a cache that comes back empty after a restart, a
monthly command allowance that one queue worker uses up in a week, or a store in Singapore that turns
your cache into the slowest part of the request.</p>
<p>This page compares free Redis hosting the way you would actually use it: as a cache, a session store or
a job queue behind an app that has to keep working. We run a Redis-compatible store in Mumbai, so we did
not only read pricing pages. Every limit below was checked against the provider’s own documentation on
7 November 2026, and every latency number was measured by us on the same day, from a home broadband
connection in India and from a server in Mumbai.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#which-free-tier-fits-which-job">Which free tier fits which job</a></li>
<li><a href="#free-redis-hosting-compared-8-options">Free Redis hosting compared: 8 options</a></li>
<li><a href="#latency-from-india-measured">Latency from India, measured</a></li>
<li><a href="#why-a-far-away-cache-is-worse-than-no-cache">Why a far-away cache is worse than no cache</a></li>
<li><a href="#four-ways-a-free-key-value-store-lets-you-down">Four ways a free key value store lets you down</a></li>
<li><a href="#what-500000-commands-a-month-really-buys">What 500,000 commands a month really buys</a></li>
<li><a href="#is-redis-free-the-licence-change-and-valkey">Is Redis free? The licence change and Valkey</a></li>
<li><a href="#create-a-free-key-value-store-in-mumbai-on-voroa">Create a free key value store in Mumbai on Voroa</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="which-free-tier-fits-which-job" tabindex="-1"><a class="header-anchor" href="#which-free-tier-fits-which-job">Which free tier fits which job</a></h2>
<ul>
<li><strong>Learning Redis commands, or a weekend project:</strong> Redis Cloud. It is the official service, the
console is good, and 30 MB is plenty to learn on. Use it at least once every 14 days or it is deleted.</li>
<li><strong>A serverless app (Vercel, Netlify, edge functions):</strong> Upstash. It answers over HTTP as well as the
Redis protocol, so a function does not have to hold a connection open. Watch the 500,000 commands a
month.</li>
<li><strong>A cache you can afford to lose:</strong> Render’s free Key Value, if your app is also on Render. It keeps
nothing on disk, so every restart empties it.</li>
<li><strong>Sessions or a queue that must survive a restart:</strong> pick a tier that writes to disk. Upstash, Aiven,
Layerbase and Voroa do; Render’s free tier does not.</li>
<li><strong>Your app runs in India:</strong> put the store in Mumbai, next to the app. From a server in Mumbai we
measured about 9 ms to Mumbai and about 86 ms to Singapore, per round trip.</li>
</ul>
<h2 id="free-redis-hosting-compared-8-options" tabindex="-1"><a class="header-anchor" href="#free-redis-hosting-compared-8-options">Free Redis hosting compared: 8 options</a></h2>
<p>Every option we could verify, with limits as published by each provider on 7 November 2026. Sources:
Upstash pricing and FAQ, <a href="https://redis.io/docs/latest/operate/rc/subscriptions/view-essentials-subscription/essentials-plan-details/">Redis Cloud Essentials plans</a>,
the Redis Cloud inactivity policy, Aiven’s free plan page, <a href="https://render.com/docs/free">Render free instances</a>,
Railway’s trial terms, Layerbase’s plan page, the AWS Free Tier page, and
<a href="/docs/free-instances/">Voroa free instances</a>.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Provider</th>
<th>Free memory</th>
<th>The limit that bites</th>
<th>Sleeps or pauses?</th>
<th>Deleted after?</th>
<th>Kept on disk?</th>
<th>Connections</th>
<th>Nearest region to India</th>
</tr>
</thead>
<tbody>
<tr>
<td>Upstash</td>
<td>250 MB</td>
<td>500,000 commands and 10 GB bandwidth a month</td>
<td>No</td>
<td>Archived after 30+ days idle, restorable</td>
<td>Yes</td>
<td>Not published</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Redis Cloud</td>
<td>30 MB</td>
<td>100 ops/sec, 5 GB bandwidth a month</td>
<td>No</td>
<td>Deleted after 14 days with no commands</td>
<td>No, and no backups</td>
<td>30</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Aiven for Valkey</td>
<td>1 GB RAM, 1 CPU</td>
<td>No region choice, no pooling</td>
<td>Powered off when not used, with notice</td>
<td>No time limit</td>
<td>Backups included</td>
<td>Not published</td>
<td>Not selectable</td>
</tr>
<tr>
<td>Render Key Value</td>
<td>25 MB</td>
<td>Data lost on every restart</td>
<td>No</td>
<td>No</td>
<td>No</td>
<td>50</td>
<td>Singapore</td>
</tr>
<tr>
<td>Railway</td>
<td>Uses credit</td>
<td>$5 trial credit for 30 days, then $1 of credit a month</td>
<td>Stops when credit runs out</td>
<td>Trial volumes deleted 30 days after credit ends</td>
<td>Volume</td>
<td>Not published</td>
<td>Singapore</td>
</tr>
<tr>
<td>Layerbase</td>
<td>Part of 5 GB across 2 databases</td>
<td>Hibernates after 15 min idle</td>
<td>Yes, wakes in about 1-5 s</td>
<td>Archived after 14 days hibernated, reclaimed after 90 days archived</td>
<td>Snapshots</td>
<td>20</td>
<td>Not published</td>
</tr>
<tr>
<td>AWS ElastiCache</td>
<td>Credit-based</td>
<td>$100 credit (+$100), up to 6 months</td>
<td>No</td>
<td>Free plan ends with the credit or after 6 months</td>
<td>In memory, optional snapshots</td>
<td>Instance limit</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Voroa</td>
<td>128 MB</td>
<td>Shares free hours with your other free services</td>
<td>No (today)</td>
<td>No inactivity deletion today</td>
<td>Yes, plus nightly snapshot</td>
<td>50</td>
<td>Mumbai</td>
</tr>
</tbody>
</table></div>
<p>Three notes on the table. Aiven, Render and Voroa run Valkey or a Redis-compatible engine rather than
Redis Ltd’s own build; your client library does not notice the difference for everyday commands. AWS
needs a payment card at sign-up and its free period ends. Upstash also hands out an unclaimed
database through a single HTTP request with no sign-up, but that one is deleted after 3 days unless you
claim it into an account.</p>
<h2 id="latency-from-india-measured" tabindex="-1"><a class="header-anchor" href="#latency-from-india-measured">Latency from India, measured</a></h2>
<p>We timed TCP connections to each region, 15 attempts each, and report the median. A TCP connect is one
network round trip, which is the floor for every command you send. For Render we timed its public
key value endpoints. Upstash, Redis Cloud and ElastiCache all run their Mumbai and Singapore databases
inside AWS, so for those we timed the AWS regional endpoints as the baseline; we assume a database in
the same region adds little to it, but we did not measure each provider’s own endpoint. For Voroa we
timed the public address of our Mumbai database host on port 443.</p>
<p><strong>From a home broadband connection in India</strong></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Region</th>
<th>Median round trip</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS Mumbai (Upstash, Redis Cloud, ElastiCache)</td>
<td>29 ms</td>
</tr>
<tr>
<td>Voroa, Mumbai</td>
<td>30 ms</td>
</tr>
<tr>
<td>AWS Singapore</td>
<td>71 ms</td>
</tr>
<tr>
<td>Render, Singapore</td>
<td>73 ms</td>
</tr>
<tr>
<td>AWS Tokyo</td>
<td>144 ms</td>
</tr>
<tr>
<td>AWS Frankfurt</td>
<td>155 ms</td>
</tr>
<tr>
<td>Render, Frankfurt</td>
<td>161 ms</td>
</tr>
<tr>
<td>AWS Ireland</td>
<td>166 ms</td>
</tr>
<tr>
<td>Render, Oregon</td>
<td>234 ms</td>
</tr>
<tr>
<td>AWS US East</td>
<td>306 ms</td>
</tr>
</tbody>
</table></div>
<p><strong>From a server in Mumbai</strong> (where your app runs, if you host it in India)</p>
<p><img src="https://media.getvoroa.com/img/26571cf668c60d99.webp" alt="Bar chart: median round trip from a server in Mumbai to each region - AWS Mumbai 9 ms, AWS Singapore 86 ms, Render Singapore 91 ms, AWS Tokyo 133 ms, AWS Frankfurt 151 ms, AWS Ireland 191 ms, AWS US East 210 ms, Render Oregon 360 ms" width="700" loading="eager" decoding="async"></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Region</th>
<th>Median round trip</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS Mumbai</td>
<td>9 ms</td>
</tr>
<tr>
<td>AWS Singapore</td>
<td>86 ms</td>
</tr>
<tr>
<td>Render, Singapore</td>
<td>91 ms</td>
</tr>
<tr>
<td>AWS Tokyo</td>
<td>133 ms</td>
</tr>
<tr>
<td>AWS Frankfurt</td>
<td>151 ms</td>
</tr>
<tr>
<td>Render, Frankfurt</td>
<td>153 ms</td>
</tr>
<tr>
<td>AWS Ireland</td>
<td>191 ms</td>
</tr>
<tr>
<td>AWS US East</td>
<td>210 ms</td>
</tr>
<tr>
<td>Render, Oregon</td>
<td>360 ms</td>
</tr>
</tbody>
</table></div>
<p>An app on Voroa does not go out to the internet to reach its key value store at all. It connects over
the private network inside the Mumbai region, using the internal address on the database page.</p>
<h2 id="why-a-far-away-cache-is-worse-than-no-cache" tabindex="-1"><a class="header-anchor" href="#why-a-far-away-cache-is-worse-than-no-cache">Why a far-away cache is worse than no cache</a></h2>
<p>You add Redis to make a request faster. Whether it does depends almost entirely on distance.</p>
<p>A cache read is one round trip. A query against a Postgres database next to your app is also roughly
one round trip plus a few milliseconds of work. If the database is in Mumbai and the cache is in
Singapore, the “fast” path is now 86 ms and the “slow” path is about 10 ms. The cache made the page
slower, and every cache miss pays for both trips.</p>
<p>It adds up because a request rarely sends one command. A typical page checks the session, reads a rate
limit counter, increments it and reads one cached fragment. Four commands sent one after another cost
about 36 ms with the store in Mumbai and about 344 ms with it in Singapore, before your code has done
anything.</p>
<p>Two habits help wherever the store is:</p>
<ul>
<li><strong>Pipeline.</strong> Send several commands in one round trip. Most clients support it; in <code>redis-py</code> it is
<code>r.pipeline()</code>, in <code>ioredis</code> it is <code>redis.pipeline()</code>.</li>
<li><strong>Keep connections open.</strong> A new TLS connection costs several round trips before the first command.
A long-running server opens its connections once. A serverless function that connects on every call
should use an HTTP-based client, which is exactly what Upstash’s REST API is for.</li>
</ul>
<p>The real rule is simpler than either habit: put the cache in the same region as the app.</p>
<h2 id="four-ways-a-free-key-value-store-lets-you-down" tabindex="-1"><a class="header-anchor" href="#four-ways-a-free-key-value-store-lets-you-down">Four ways a free key value store lets you down</a></h2>
<p>Free tiers fail in one of four ways. Knowing which one applies tells you more than the memory figure.</p>
<p><img src="https://media.getvoroa.com/img/0f9960e83cd2e953.webp" alt="Diagram: four ways a free key value store lets you down - it forgets (Render), it sleeps (Layerbase), it hits a wall on memory, commands, operations per second or connections (Upstash, Redis Cloud), it is deleted, archived or powered off after a quiet spell (Redis Cloud, Upstash, Layerbase, Aiven)" width="700" loading="lazy" decoding="async"></p>
<p><strong>1. It forgets.</strong> Render’s free Key Value keeps everything in memory only. Render can restart it for
maintenance at any time, and when it does, every key is gone. Upgrading to a paid plan also loses the
data. That is fine for a cache your app can rebuild, and wrong for sessions: every restart logs out
every user.</p>
<p><strong>2. It sleeps.</strong> Layerbase hibernates a free database after 15 minutes without connections. The next
connection wakes it in roughly 1 to 5 seconds. Nothing is lost; the first request after a quiet spell
waits.</p>
<p><strong>3. It hits a wall.</strong> Each tier has a hard number that changes behaviour without warning your users:</p>
<ul>
<li><strong>Memory.</strong> When a store is full it either refuses writes with
<code>OOM command not allowed when used memory &gt; 'maxmemory'</code>, or evicts keys to make room, depending on
its eviction policy. Check which one you have. Evicting is right for a cache and wrong for a queue.</li>
<li><strong>Commands.</strong> Upstash’s free tier allows 500,000 commands a month (the maths is below).</li>
<li><strong>Throughput.</strong> Redis Cloud’s free database is capped at 100 operations per second.</li>
<li><strong>Connections.</strong> Redis Cloud allows 30, Layerbase 20. A web server with a pool of 10, a worker and a
scheduler can reach that sooner than you expect, and the next client gets
<code>ERR max number of clients reached</code>.</li>
</ul>
<p><strong>4. It goes when idle.</strong> Redis Cloud deletes a free database after 14 days in which
no command reaches it. Logging in to the console does not count. There is no backup, so the data is
gone. Upstash archives a free database after at least 30 days of inactivity and lets you restore it.
Layerbase archives a database after 14 days hibernated and reclaims it after 90 days archived. Aiven
does not delete a free service, but powers it off when it is not being used, after a notice by email.
If your project is quiet for a fortnight, these are the tiers that bite.</p>
<h2 id="what-500000-commands-a-month-really-buys" tabindex="-1"><a class="header-anchor" href="#what-500000-commands-a-month-really-buys">What 500,000 commands a month really buys</a></h2>
<p>Half a million sounds like a lot. Spread over a month it is about 16,700 commands a day, or 11 a
minute.</p>
<p>What uses commands is often not your users:</p>
<ul>
<li>A queue worker that polls once a second sends 2.6 million commands a month on its own, five times
the free allowance.</li>
<li>A health check that pings the store every 10 seconds uses about 260,000 a month.</li>
<li>A page that sends four commands per view runs out at around 125,000 page views a month.</li>
</ul>
<p>If a background worker is part of your design, a tier that meters commands is the wrong fit, however
much memory it has. Pick one that limits memory and connections instead, and pair the store with a
<a href="/background-workers/">background worker in the same region</a> so the polling never leaves the data
centre.</p>
<h2 id="is-redis-free-the-licence-change-and-valkey" tabindex="-1"><a class="header-anchor" href="#is-redis-free-the-licence-change-and-valkey">Is Redis free? The licence change and Valkey</a></h2>
<p><strong>Is Redis free?</strong> Yes, to use. Redis 8 and later is offered under a choice of three licences,
one of which is the AGPLv3, an open source licence. You can run it, build a product on it and charge for
that product without paying Redis Ltd. What changed is the licence for companies that sell Redis itself
as a hosted service.</p>
<p>The history, from the <a href="https://redis.io/legal/licenses/">official Redis licence page</a>:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Version</th>
<th>Licence</th>
</tr>
</thead>
<tbody>
<tr>
<td>Redis 7.2 and earlier</td>
<td>BSD-3-Clause (open source)</td>
</tr>
<tr>
<td>Redis 7.4 (March 2024)</td>
<td>RSALv2 or SSPLv1 (source-available, not open source)</td>
</tr>
<tr>
<td>Redis 8 and later</td>
<td>RSALv2, SSPLv1 or AGPLv3 (your choice)</td>
</tr>
</tbody>
</table></div>
<p><strong>Is Redis free for commercial use?</strong> Using Redis inside your own application, commercial or not, has
been free under every one of those licences. The restrictions in RSALv2 and SSPLv1 are aimed at
offering Redis to others as a managed service.</p>
<p><strong>What is Valkey?</strong> When Redis moved away from BSD in 2024, a group of contributors forked the last BSD
version and started Valkey under the Linux Foundation. Valkey stays BSD-licensed and speaks the same
protocol, so the same client libraries work against it. Many hosts, including several on this list,
now run Valkey under a “Redis-compatible” label.</p>
<p>For your app, the practical answer is the same as for Postgres: the software costs nothing, the machine
it runs on does. A free tier is a provider paying for a small machine on your behalf, and the catches
above are how they keep that cost bounded.</p>
<h2 id="create-a-free-key-value-store-in-mumbai-on-voroa" tabindex="-1"><a class="header-anchor" href="#create-a-free-key-value-store-in-mumbai-on-voroa">Create a free key value store in Mumbai on Voroa</a></h2>
<p>Voroa’s free plan includes one managed key value store per account: 128 MB of RAM, up to 50
connections, in Mumbai, with no card. It writes your data to disk and takes a nightly snapshot, so a
restart does not start from empty.</p>
<p>The trade-offs are real. 128 MB is small, so it suits sessions, rate limits and a modest cache rather
than a large dataset. Free services and free databases in a workspace share 750 running hours a month;
a free store on its own uses about 744, but a free store plus a free web service running all month use
them up in about 16 days, after which free services stop until the 1st. And free database capacity is
shared across the platform, so if it is full, a new free store is refused with a clear message instead
of being queued. The <a href="/docs/free-instances/#free-managed-databases">free tier rules</a> list every limit.</p>
<p><strong>Step 1.</strong> In the dashboard select <strong>New</strong>, then <strong>Key Value</strong>.</p>
<p><strong>Step 2.</strong> Name it and pick the free size. Under <strong>When memory is full</strong>, keep <strong>Keep everything</strong> if
every key matters, or choose a drop option if the store is only a cache.</p>
<p><strong>Step 3.</strong> Copy the connection URL shown at create time. It is shown once.</p>
<p><strong>Step 4.</strong> Add it to your service as an <a href="/docs/environment-variables/">environment variable</a>, for
example <code>REDIS_URL</code>.</p>
<p><strong>Step 5.</strong> Point your existing client at it. No other code change is needed:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// Node.js, with the redis package
import { createClient } from 'redis';
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
await client.set('greeting', 'namaste', { EX: 60 });
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python"># Python, with redis-py
import os, redis
r = redis.from_url(os.environ[&quot;REDIS_URL&quot;])
r.set(&quot;greeting&quot;, &quot;namaste&quot;, ex=60)
</code></pre>
</div>
<p>To reach it from your laptop or an app hosted elsewhere, turn on external access on the database page
and you get a TLS (<code>rediss://</code>) endpoint. What each setting does, including the read-only <strong>Data</strong> tab
for browsing keys, is in the <a href="/docs/key-value/">Key Value guide</a>. Moving data in from another host is
covered in <a href="/docs/import-existing-database/">importing an existing database</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Is there a completely free Redis server online?</strong>
Yes. Upstash, Redis Cloud, Aiven, Render, Layerbase and Voroa all offer a free tier with no end date.
Each has a catch: a command allowance, deletion when idle, a power-off, no persistence, hibernation or a
small memory size.</p>
<p><strong>Which free Redis cloud tier does not delete my data when I am away?</strong>
Render and Voroa have no inactivity deletion today, and Aiven only powers a quiet service off. Upstash
archives after 30 days and lets you restore. Layerbase archives after 14 days hibernated and reclaims
the database after 90 days archived. Redis Cloud deletes a free database after 14 days without
commands, with no backup.</p>
<p><strong>Can I get free Redis hosted in India?</strong>
Upstash, Redis Cloud and Voroa let you put a free database in Mumbai. AWS Mumbai is free only during its
credit-based trial. Render and Railway are closest in Singapore, and Aiven does not let you pick a region
on its free plan.</p>
<p><strong>Which free tier keeps data after a restart?</strong>
Upstash, Aiven, Layerbase and Voroa write to disk. Render’s free Key Value does not. Redis Cloud’s free
database has no backups, so a deleted free database cannot be recovered.</p>
<p><strong>Is the Redis free tier enough for sessions in production?</strong>
For a small app, often yes, if the store keeps data on disk and sits next to your app. Size it for your
active sessions, not your total users, and give sessions an expiry so old ones clear themselves.</p>
<p><strong>What should I use instead of Redis for permanent data?</strong>
A real database. Redis keeps data in memory, and even with disk persistence the last moments before a
crash can be lost. Keep what you cannot rebuild in Postgres; our
<a href="/blog/free-postgres-database/">free Postgres comparison</a> covers the same ground for databases.</p>
<h2 id="get-a-free-key-value-store-in-mumbai" tabindex="-1"><a class="header-anchor" href="#get-a-free-key-value-store-in-mumbai">Get a free key value store in Mumbai</a></h2>
<p>One free store per account, 128 MB, no card, kept on disk, in Mumbai. See every size
and what it costs on the <a href="/managed-redis-india/">managed Redis page</a>.</p>
<p><em>Last updated: 7 November 2026. We re-check every limit and re-run every measurement monthly. If a
provider has changed something we missed, write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a> and we will correct it.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Heroku Scheduler: Limits, Costs and Alternatives in 2026</title>
      <link>https://getvoroa.com/blog/heroku-scheduler/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/heroku-scheduler/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[How Heroku Scheduler works, its 10-minute minimum, UTC times and best-effort runs, what it costs in dyno hours, and how the alternatives handle the same jobs.]]></description>
      <content:encoded><![CDATA[<p>Heroku Scheduler is Heroku’s free add-on for running a command on a timetable: every 10 minutes, every
hour or once a day. It is the quickest way to get a cron job on Heroku, and for many apps it is enough.</p>
<p>This guide explains how it works, the limits that matter, and how the common alternatives compare.
Every platform fact below comes from that platform’s own documentation, checked on 7 November 2026.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#how-the-add-on-works">How the add-on works</a></li>
<li><a href="#the-five-limits-that-matter">The five limits that matter</a></li>
<li><a href="#what-it-costs">What it costs</a></li>
<li><a href="#the-clock-process-herokus-own-alternative">The clock process: Heroku’s own alternative</a></li>
<li><a href="#how-other-platforms-schedule-jobs">How other platforms schedule jobs</a></li>
<li><a href="#moving-a-scheduled-job-off-heroku">Moving a scheduled job off Heroku</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="how-the-add-on-works" tabindex="-1"><a class="header-anchor" href="#how-the-add-on-works">How the add-on works</a></h2>
<p>You add it from the Resources tab or with one command:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">heroku addons:create scheduler:standard
</code></pre>
</div>
<p>Then, in its dashboard, you enter a command such as <code>rake send_digest</code> or <code>python manage.py cleanup</code>,
pick a frequency and a dyno size, and set the next run time.</p>
<p>At each run, Heroku starts a <strong>one-off dyno</strong>, runs your command in it, and shuts it down. It is the same
as typing <code>heroku run your-command</code>, so if a command works with <code>heroku run</code>, it will work here.</p>
<h2 id="the-five-limits-that-matter" tabindex="-1"><a class="header-anchor" href="#the-five-limits-that-matter">The five limits that matter</a></h2>
<p><strong>1. Only three frequencies.</strong> Every 10 minutes, every hour, or every day. There is no cron expression,
so “weekdays at 9 am” or “every 5 minutes” cannot be set directly.</p>
<p><strong>2. Times are in UTC.</strong> A daily job set for 03:30 runs at 9:00 am in India. Add the offset yourself
when you pick the time.</p>
<p><strong>3. Runs are best-effort.</strong> Heroku says job execution is “expected but not guaranteed”. In very rare
cases a job may be skipped, and in very rare cases it may run twice. Heroku recommends a clock process
for critical production jobs.</p>
<p><strong>4. Each run is a fresh dyno.</strong> Nothing you write to disk survives between runs, and start-up time is
part of every run. That is good for isolation and bad for very frequent, very short jobs.</p>
<p><strong>5. A run is killed at its interval.</strong> Heroku says a scheduler dyno will not run longer than its
scheduling interval: a 10-minute job is stopped after about 10 minutes, an hourly job after about an
hour. Because of timing jitter, two runs of the same job may briefly overlap.</p>
<h2 id="what-it-costs" tabindex="-1"><a class="header-anchor" href="#what-it-costs">What it costs</a></h2>
<p>The add-on itself is free. The one-off dynos it starts are billed like any other dyno usage, and they
appear on your invoice as a “scheduler” dyno type.</p>
<p>So the cost depends on how long each run lasts and how often it runs. A two-minute job every 10 minutes
is about 288 minutes of dyno time a day, about 144 hours a month. A two-minute daily job is about one hour
a month.</p>
<h2 id="the-clock-process-herokus-own-alternative" tabindex="-1"><a class="header-anchor" href="#the-clock-process-herokus-own-alternative">The clock process: Heroku’s own alternative</a></h2>
<p>For jobs that must run, Heroku points to a <strong>custom clock process</strong>: a small process in your <code>Procfile</code>
that runs all the time and decides when jobs are due.</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>web:    node web.js
worker: node worker.js
clock:  node clock.js
</code></pre>
</div>
<p>It gives you full cron expressions and your own timezone. The trade-offs, from Heroku’s guidance:</p>
<ul>
<li>It runs 24/7, so you pay for a dyno all month.</li>
<li><strong>Run exactly one clock dyno.</strong> Scaling it to two schedules every job twice.</li>
<li>Dynos restart at least once a day, so the clock needs start-up logic to catch runs it missed.</li>
</ul>
<p>That duplicate-run warning is real. We measured it for in-process schedulers: two processes ran every
job twice, in both <a href="/blog/cron-job-in-node-js/">Node.js</a> and <a href="/blog/apscheduler/">Python</a>.</p>
<h2 id="how-other-platforms-schedule-jobs" tabindex="-1"><a class="header-anchor" href="#how-other-platforms-schedule-jobs">How other platforms schedule jobs</a></h2>
<p><img src="https://media.getvoroa.com/img/0702c2e38b950b9b.webp" alt="Diagram: what happens when a scheduled run is still going at the next run time - delayed until it finishes on Render, skipped on Railway, and started anyway (overlap) on plain cron and Voroa unless you take a lock; Heroku's scheduler instead stops a run at its interval" width="700" loading="eager" decoding="async"></p>
<p>Each platform handles the three hard questions differently: how often, what happens if a run is still
going, and which timezone.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Platform</th>
<th>Shortest interval</th>
<th>If the last run is still going</th>
<th>Timezone</th>
<th>Billing</th>
</tr>
</thead>
<tbody>
<tr>
<td>Heroku Scheduler</td>
<td>10 minutes</td>
<td>Killed at the interval; brief overlap possible</td>
<td>UTC</td>
<td>One-off dyno time</td>
</tr>
<tr>
<td>Heroku clock process</td>
<td>Any cron</td>
<td>Your code decides</td>
<td>Any</td>
<td>A dyno running 24/7</td>
</tr>
<tr>
<td>Render Cron Jobs</td>
<td>Cron expression</td>
<td>Next run is delayed until it finishes</td>
<td>UTC</td>
<td>Per second of run time, $1 minimum a month</td>
</tr>
<tr>
<td>Railway cron</td>
<td>5 minutes</td>
<td>Next run is skipped</td>
<td>UTC</td>
<td>Service usage</td>
</tr>
<tr>
<td>GitHub Actions <code>schedule</code></td>
<td>5 minutes, can be delayed</td>
<td>Separate workflow run</td>
<td>UTC by default</td>
<td>Actions minutes</td>
</tr>
<tr>
<td>Voroa Cron Job</td>
<td>1 minute</td>
<td>Can overlap; use a lock. Skipped while the service is stopped</td>
<td>IST</td>
<td>The size you pick for the service, billed monthly</td>
</tr>
</tbody>
</table></div>
<p>Sources: Heroku Scheduler and clock process docs, the <a href="https://render.com/docs/cronjobs">Render cron job docs</a>,
the <a href="https://docs.railway.com/reference/cron-jobs">Railway cron docs</a> and the GitHub Actions schedule
docs, all checked on 7 November 2026.</p>
<p>Three things stand out. Railway requires your process to exit after each run. Render guarantees only
one run at a time by delaying the next. And every hosted scheduler here except Voroa schedules in
UTC by default, which matters if your users are in India.</p>
<h2 id="moving-a-scheduled-job-off-heroku" tabindex="-1"><a class="header-anchor" href="#moving-a-scheduled-job-off-heroku">Moving a scheduled job off Heroku</a></h2>
<ol>
<li><strong>List your jobs.</strong> Copy each command, its frequency and its next run time from the Scheduler
dashboard.</li>
<li><strong>Convert to cron.</strong> “Every 10 minutes” is <code>*/10 * * * *</code>, “every hour at :30” is <code>30 * * * *</code>,
“daily at 03:30 UTC” is <code>30 3 * * *</code>. Our guide to
<a href="/blog/cron-every-5-minutes/">cron every 5 minutes</a> has more patterns.</li>
<li><strong>Fix the timezone.</strong> If the new platform uses IST, a daily 03:30 UTC job becomes <code>0 9 * * *</code>.</li>
<li><strong>Make jobs safe to run twice.</strong> Every platform above can, in some case, run late, overlap or repeat.</li>
<li><strong>Check the effect, not just the log.</strong> After the first run, confirm the rows were written or the
email went out.</li>
</ol>
<p>On Voroa, create a <strong>Cron Job</strong> from your repository with the same command and the converted schedule,
and add your environment variables. Runs are skipped while the service is stopped, including after a
free workspace uses its monthly free hours, until the 1st.</p>
<p>Steps are in
<a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a>, and
<a href="/docs/worker-or-cron-job/">worker or cron job</a> helps if your job is really a long-running process. More
on running workers is on the <a href="/background-workers/">background workers page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Is Heroku Scheduler free?</strong>
The add-on is free, but each run uses a one-off dyno, and that dyno time is billed like other dyno
usage.</p>
<p><strong>What is the minimum interval for Heroku Scheduler?</strong>
Every 10 minutes. The other choices are hourly and daily. It does not accept cron expressions.</p>
<p><strong>What timezone does Heroku Scheduler use?</strong>
UTC. Subtract 5 hours 30 minutes from the Indian time you want.</p>
<p><strong>Is Heroku Scheduler reliable?</strong>
Heroku describes runs as expected but not guaranteed: very rarely a job is skipped or runs twice. For
critical jobs it recommends a clock process.</p>
<p><strong>How do I run a Heroku job every 5 minutes?</strong>
Not with Scheduler. Use a clock process with a cron library, or a platform that accepts cron
expressions.</p>
<p><strong>What is the best Heroku Scheduler alternative?</strong>
It depends on your stack. Pick one that accepts full cron expressions, states what happens to
overlapping runs, and schedules in the timezone your users live in.</p>
<p><em>Checked 7 November 2026 against each platform’s documentation. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Redis Pub Sub Explained: How It Works and Where It Breaks</title>
      <link>https://getvoroa.com/blog/redis-pub-sub/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/redis-pub-sub/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>Node.js</category>
      <description><![CDATA[Redis pub sub explained with tests we ran: what happens to messages when a subscriber is offline, fan-out latency, slow subscribers, and when to use Streams.]]></description>
      <content:encoded><![CDATA[<p>Redis pub sub lets one part of your system send a message to every other part that is listening,
through a named channel, in under a millisecond when both sides are close to the server. It is the simplest way to broadcast events
between processes, and it is also the feature people most often use for the wrong job. This guide is
for developers wiring up real-time updates, cache invalidation or chat. It shows the commands, and
then what we saw when we tested the behaviour that tutorials skip: a subscriber that goes offline, a
hundred subscribers at once, and a subscriber that cannot keep up.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#what-pubsub-is-in-one-paragraph">What Pub/Sub is, in one paragraph</a></li>
<li><a href="#the-commands-with-working-code">The commands, with working code</a></li>
<li><a href="#test-1-a-subscriber-goes-offline">Test 1: a subscriber goes offline</a></li>
<li><a href="#test-2-one-message-a-hundred-listeners">Test 2: one message, a hundred listeners</a></li>
<li><a href="#test-3-the-subscriber-that-cannot-keep-up">Test 3: the subscriber that cannot keep up</a></li>
<li><a href="#pubsub-streams-or-a-job-queue">Pub/Sub, Streams or a job queue?</a></li>
<li><a href="#what-it-is-good-at">What it is good at</a></li>
<li><a href="#running-it-on-a-managed-store">Running it on a managed store</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="what-pubsub-is-in-one-paragraph" tabindex="-1"><a class="header-anchor" href="#what-pubsub-is-in-one-paragraph">What Pub/Sub is, in one paragraph</a></h2>
<p>A publisher sends a message to a channel with <code>PUBLISH</code>. Every client subscribed to that channel at
that moment receives it. Redis does not store the message anywhere: if nobody is listening, it is
gone, and <code>PUBLISH</code> tells you how many clients received it. The <a href="https://redis.io/docs/latest/develop/pubsub/">official documentation</a> calls this
<strong>at-most-once delivery</strong>: a message arrives once, or not at all.</p>
<p><img src="https://media.getvoroa.com/img/48f2fd544e484fd9.webp" alt="Diagram: a publisher sends one message to a channel; three connected subscribers receive it; a disconnected subscriber receives nothing and the message is not stored" width="700" loading="eager" decoding="async"></p>
<h2 id="the-commands-with-working-code" tabindex="-1"><a class="header-anchor" href="#the-commands-with-working-code">The commands, with working code</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Command</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SUBSCRIBE orders</code></td>
<td>Listen to one or more channels</td>
</tr>
<tr>
<td><code>PSUBSCRIBE orders.*</code></td>
<td>Listen to every channel matching a pattern</td>
</tr>
<tr>
<td><code>PUBLISH orders &quot;...&quot;</code></td>
<td>Send a message; returns the number of receivers</td>
</tr>
<tr>
<td><code>UNSUBSCRIBE</code> / <code>PUNSUBSCRIBE</code></td>
<td>Stop listening</td>
</tr>
<tr>
<td><code>SSUBSCRIBE</code> / <code>SPUBLISH</code></td>
<td>Sharded Pub/Sub for clusters (Redis 7 and later)</td>
</tr>
<tr>
<td><code>PUBSUB CHANNELS</code> / <code>PUBSUB NUMSUB</code></td>
<td>See which channels have subscribers</td>
</tr>
</tbody>
</table></div>
<p>Over the RESP2 protocol, which most clients use by default, a connection that has subscribed can only
run subscription commands, so a client needs a separate connection for its normal reads and writes.</p>
<p><strong>Node.js</strong> (with <code>ioredis</code>):</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">import Redis from 'ioredis';

const sub = new Redis(process.env.REDIS_URL);   // subscribe-only connection
const pub = new Redis(process.env.REDIS_URL);   // normal connection

await sub.subscribe('orders');
sub.on('message', (channel, message) =&gt; {
  const order = JSON.parse(message);
  console.log('new order', order.id);
});

const receivers = await pub.publish('orders', JSON.stringify({ id: 42 }));
console.log('delivered to', receivers, 'subscribers');
</code></pre>
</div>
<p><strong>Python</strong> (with <code>redis-py</code>):</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">python</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-python">import os, json, redis

r = redis.from_url(os.environ[&quot;REDIS_URL&quot;])
p = r.pubsub()
p.subscribe(&quot;orders&quot;)

r.publish(&quot;orders&quot;, json.dumps({&quot;id&quot;: 42}))

for msg in p.listen():
    if msg[&quot;type&quot;] == &quot;message&quot;:
        print(&quot;new order&quot;, json.loads(msg[&quot;data&quot;])[&quot;id&quot;])
</code></pre>
</div>
<h2 id="test-1-a-subscriber-goes-offline" tabindex="-1"><a class="header-anchor" href="#test-1-a-subscriber-goes-offline">Test 1: a subscriber goes offline</a></h2>
<p><strong>Setup for all three tests:</strong> Redis 8.10.3 in a Linux virtual machine (WSL2) on a laptop, with a
Node.js 24 client using <code>ioredis</code> on the same laptop. Default configuration. Measured 7 November 2026.</p>
<p>We connected a subscriber and published 100 messages: it received all 100. Then we disconnected it,
published 100 more, and reconnected.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Step</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Subscriber online, 100 messages published</td>
<td>100 received</td>
</tr>
<tr>
<td>Subscriber offline, 100 messages published</td>
<td><code>PUBLISH</code> returned 0 receivers, 100 times</td>
</tr>
<tr>
<td>Subscriber reconnects</td>
<td>0 of those 100 messages received</td>
</tr>
</tbody>
</table></div>
<p>This is the single most important fact about Pub/Sub. A deploy, a network blip, or a client
reconnecting after a Redis restart all create a gap, and every message published in that gap is lost
for good. If a missed message means a missed payment, a missed email or an order nobody sees, Pub/Sub
is the wrong tool. Use a <a href="/blog/bullmq-redis-job-queue/">Redis job queue</a> or Streams instead.</p>
<p>The return value of <code>PUBLISH</code> is your only signal. If it is 0 when you expected listeners, log it.</p>
<h2 id="test-2-one-message-a-hundred-listeners" tabindex="-1"><a class="header-anchor" href="#test-2-one-message-a-hundred-listeners">Test 2: one message, a hundred listeners</a></h2>
<p>We published 1,000 messages to 1, 10 and 100 subscribers, each message carrying the time it was sent,
and measured how long each one took to arrive.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Subscribers</th>
<th>Messages delivered</th>
<th>Median delivery time</th>
<th>99th percentile</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>1,000 of 1,000</td>
<td>0.38 ms</td>
<td>0.81 ms</td>
</tr>
<tr>
<td>10</td>
<td>10,000 of 10,000</td>
<td>0.76 ms</td>
<td>7.06 ms</td>
</tr>
<tr>
<td>100</td>
<td>100,000 of 100,000</td>
<td>7.09 ms</td>
<td>25.51 ms</td>
</tr>
</tbody>
</table></div>
<p>Every message reached every subscriber. Delivery time grew with the number of listeners, but read this
table carefully: all our subscribers ran inside one Node.js process, so the higher numbers include our
own client working through 100 connections one by one. We expect, but did not measure, that subscribers spread across separate
processes or servers would each see something closer to the single-subscriber figure, plus the
one-way network time from Redis.</p>
<p>Raw throughput is high. Publishing 100,000 small messages in a pipeline to one subscriber, all of
them arrived in 664 ms, about 150,000 messages a second.</p>
<h2 id="test-3-the-subscriber-that-cannot-keep-up" tabindex="-1"><a class="header-anchor" href="#test-3-the-subscriber-that-cannot-keep-up">Test 3: the subscriber that cannot keep up</a></h2>
<p>This one is rarely tested, and it causes real outages. Redis buffers outgoing messages for each
subscriber. If a subscriber reads more slowly than messages arrive, its buffer grows. Redis protects
itself with a limit, and the default for Pub/Sub clients is <code>pubsub 32mb 8mb 60</code>: disconnect the
client if its buffer passes 32 MB, or stays above 8 MB for 60 seconds. We confirmed both Redis 8.10.3
and Valkey 9.1.3 ship with that default.</p>
<p>We subscribed with a client that stopped reading its socket, then published 1 KB messages as fast as
we could.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What happened</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Messages published before Redis disconnected the subscriber</td>
<td>34,486 (about 34 MB)</td>
</tr>
<tr>
<td>Time to disconnection</td>
<td>about 10 seconds</td>
</tr>
<tr>
<td><code>client_output_buffer_limit_disconnections</code> in <code>INFO stats</code></td>
<td>1</td>
</tr>
</tbody>
</table></div>
<p>Redis closed the subscriber’s connection. A normal client such as <code>ioredis</code> sees that close,
reconnects and subscribes again, so from the outside it looks healthy. But everything that was sitting
in the 32 MB buffer, and everything published while it reconnected, is lost, exactly as in Test 1. In production this looks like “live
updates stop working under load”, and the cause is a subscriber doing slow work, such as a database
write, inside its message handler.</p>
<p>Two habits prevent it:</p>
<ul>
<li><strong>Do almost nothing in the handler.</strong> Push the message onto an in-process queue or a job queue and
return. Never wait on a database or an API inside <code>on('message')</code>.</li>
<li><strong>Watch the counter.</strong> <code>client_output_buffer_limit_disconnections</code> going up means a subscriber is
being cut off. See the <a href="https://redis.io/docs/latest/develop/reference/clients/">client output buffer settings</a>
in the Redis documentation.</li>
</ul>
<h2 id="pubsub-streams-or-a-job-queue" tabindex="-1"><a class="header-anchor" href="#pubsub-streams-or-a-job-queue">Pub/Sub, Streams or a job queue?</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Pub/Sub</th>
<th>Streams</th>
<th>Job queue (built on lists or streams)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Message stored?</td>
<td>No</td>
<td>Yes, until trimmed</td>
<td>Yes, until processed</td>
</tr>
<tr>
<td>Offline consumer catches up?</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Every listener gets every message?</td>
<td>Yes</td>
<td>Yes, or split across a consumer group</td>
<td>No, each job goes to one worker</td>
</tr>
<tr>
<td>Acknowledgement and retry</td>
<td>No</td>
<td>Yes (consumer groups)</td>
<td>Yes</td>
</tr>
<tr>
<td>Typical use</td>
<td>Live updates, cache invalidation</td>
<td>Event logs, activity feeds, audit trails</td>
<td>Emails, PDFs, image processing</td>
</tr>
</tbody>
</table></div>
<p>The rule of thumb: Pub/Sub for things that only matter right now, Streams for events you might need to
replay, a job queue for work that must happen even if it occasionally runs twice.</p>
<h2 id="what-it-is-good-at" tabindex="-1"><a class="header-anchor" href="#what-it-is-good-at">What it is good at</a></h2>
<ul>
<li><strong>WebSocket fan-out across servers.</strong> When you run two or more instances of a chat or live-dashboard
app, a message received by one instance has to reach users connected to the others. <a href="http://Socket.IO">Socket.IO</a>’s
Redis adapter does exactly this with Pub/Sub.</li>
<li><strong>Cache invalidation.</strong> When one service updates a record, it publishes the key, and every instance
drops its in-memory copy. A missed message only means one stale cache entry until its expiry.</li>
<li><strong>Live progress.</strong> A worker publishes “50% done” and the web server pushes it to the browser. If one
update is lost, the next one replaces it.</li>
<li><strong>Keyspace notifications.</strong> Redis can publish an event when a key expires or changes, which is
useful for light-weight triggers. They are off by default (<code>notify-keyspace-events</code>), a managed host
may not let you turn them on, and they are fire-and-forget too, so do not build billing on them.</li>
</ul>
<p>In every one of these, losing an occasional message is harmless. That is the test to apply.</p>
<h2 id="running-it-on-a-managed-store" tabindex="-1"><a class="header-anchor" href="#running-it-on-a-managed-store">Running it on a managed store</a></h2>
<p><strong>Count your connections.</strong> Every subscriber holds a connection open for as long as it listens, and
it needs a second one for normal commands. Ten app instances with one subscriber and one normal
connection each use 20 connections before any job queue or admin tool. Check your plan’s connection
limit; Voroa’s free key value size allows 50, and paid sizes from the Rs 425 one upward allow 250 or more.</p>
<p><strong>Keep subscribers close.</strong> Pub/Sub delivery is one network hop from Redis to each subscriber. From a
server in Mumbai we measured about 9 ms to a Mumbai region and about 86 ms to Singapore, per round
trip; the numbers are in our <a href="/blog/free-redis-hosting/">free Redis hosting comparison</a>. Put the store
in the same region as the app.</p>
<p><strong>Expect reconnects.</strong> A managed store restarts for maintenance and upgrades. <code>ioredis</code>
resubscribes automatically after a reconnect by default; check how your own client behaves. Either way,
messages sent during the gap are gone, as Test 1 showed.</p>
<p><strong>Engine choice does not matter here.</strong> Pub/Sub is part of the core both engines share, and we
confirmed Valkey 9.1.3 ships the same buffer default; our tests themselves ran on Redis. See <a href="/blog/valkey-vs-redis/">Valkey vs Redis</a> for where they do differ.</p>
<p>On Voroa, your <a href="/web-services/">web services</a> and background workers reach a key value store over
the private network in Mumbai, using the connection URL you add as an
<a href="/docs/environment-variables/">environment variable</a>. Setup details are in the
<a href="/docs/key-value/">Key Value guide</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Does Redis store Pub/Sub messages?</strong>
No. A message goes only to clients subscribed at that moment. If none are, it is discarded and
<code>PUBLISH</code> returns 0.</p>
<p><strong>Is Redis Pub/Sub reliable?</strong>
It is fast and simple, but delivery is at-most-once. In our tests, messages sent while a subscriber was
offline were never delivered, and a subscriber that fell 32 MB behind was disconnected.</p>
<p><strong>What is the difference between Redis Pub/Sub and Streams?</strong>
Streams store messages, so a consumer can read what it missed, acknowledge messages and retry. Pub/Sub
stores nothing.</p>
<p><strong>How many subscribers can Redis Pub/Sub handle?</strong>
Many; the practical limit is connections and network. In our test, 100 subscribers each received all
1,000 messages. Plan around your store’s connection limit.</p>
<p><strong>Why does my subscriber stop receiving messages?</strong>
Usually a reconnect gap or a slow handler that pushed its buffer past the limit. Check
<code>client_output_buffer_limit_disconnections</code> in <code>INFO stats</code>.</p>
<p><strong>Can I use Pub/Sub as a job queue?</strong>
No. A job published while no worker is listening is lost, and two workers would both run it. Use a
queue library or Streams with a consumer group.</p>
<h2 id="add-a-key-value-store-for-live-updates" tabindex="-1"><a class="header-anchor" href="#add-a-key-value-store-for-live-updates">Add a key value store for live updates</a></h2>
<p>A Redis-compatible store in Mumbai, next to the services you deploy, with a free size to start. See
the sizes on the <a href="/managed-redis-india/">managed Redis page</a>.</p>
<p><em>Tested 7 November 2026 on Redis 8.10.3 in WSL2 on a laptop; Valkey 9.1.3’s buffer default confirmed. Found something out
of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Redis Streams: Consumer Groups, Retries and Memory, Tested</title>
      <link>https://getvoroa.com/blog/redis-streams/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/redis-streams/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>Node.js</category>
      <description><![CDATA[A practical guide to Redis Streams: XADD, consumer groups, acknowledgements and crash recovery, with tests we ran on memory per entry, trimming and read speed.]]></description>
      <content:encoded><![CDATA[<p>Redis Streams are an append-only log inside Redis: producers add entries, and consumers read them at
their own pace, pick up where they left off after a restart, and acknowledge what they have processed.
They fill the gap between Pub/Sub, which forgets every message, and a full message broker. This guide
is for developers who need events that survive a crash: order events, activity feeds, audit trails,
webhooks to process. It covers the commands you actually use, and the numbers we measured on memory,
trimming, crash recovery and read speed.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#what-a-stream-is">What a stream is</a></li>
<li><a href="#the-six-commands-you-need">The six commands you need</a></li>
<li><a href="#consumer-groups-sharing-the-work">Consumer groups: sharing the work</a></li>
<li><a href="#tested-an-offline-consumer-catches-up">Tested: an offline consumer catches up</a></li>
<li><a href="#tested-a-consumer-crashes-mid-job">Tested: a consumer crashes mid-job</a></li>
<li><a href="#tested-memory-per-entry-and-trimming">Tested: memory per entry and trimming</a></li>
<li><a href="#tested-read-one-at-a-time-or-in-batches">Tested: read one at a time, or in batches?</a></li>
<li><a href="#streams-pubsub-lists-or-kafka">Streams, Pub/Sub, lists or Kafka?</a></li>
<li><a href="#a-worker-loop-you-can-copy">A worker loop you can copy</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="what-a-stream-is" tabindex="-1"><a class="header-anchor" href="#what-a-stream-is">What a stream is</a></h2>
<p>A stream is a key holding an ordered list of entries. Each entry has an ID, made of a millisecond
timestamp and a sequence number such as <code>1762500000000-0</code>, and a small set of field-value pairs. New
entries go on the end. Entries stay until you trim them, so any consumer can read from any point.</p>
<p><img src="https://media.getvoroa.com/img/1b9c5f739b23034b.webp" alt="Diagram: producers append entries to a stream; consumer group billing has two consumers sharing the entries; consumer group analytics reads every entry independently; unacknowledged entries sit in a pending list" width="700" loading="eager" decoding="async"></p>
<h2 id="the-six-commands-you-need" tabindex="-1"><a class="header-anchor" href="#the-six-commands-you-need">The six commands you need</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Command</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>XADD orders * id 42 total 499</code></td>
<td>Append an entry; <code>*</code> lets Redis choose the ID</td>
</tr>
<tr>
<td><code>XREADGROUP GROUP billing c1 COUNT 100 BLOCK 5000 STREAMS orders &gt;</code></td>
<td>Read new entries as consumer <code>c1</code> of group <code>billing</code>, waiting up to 5 s</td>
</tr>
<tr>
<td><code>XACK orders billing &lt;id&gt;</code></td>
<td>Mark an entry as processed</td>
</tr>
<tr>
<td><code>XPENDING orders billing</code></td>
<td>List entries read but not yet acknowledged</td>
</tr>
<tr>
<td><code>XAUTOCLAIM orders billing c2 60000 0</code></td>
<td>Take over entries another consumer left pending for over 60 s</td>
</tr>
<tr>
<td><code>XADD orders MAXLEN ~ 100000 * ...</code></td>
<td>Append and trim the stream to about 100,000 entries</td>
</tr>
</tbody>
</table></div>
<p><code>XGROUP CREATE orders billing 0 MKSTREAM</code> creates the group reading from the oldest entry (and the
stream, if it does not exist; use <code>$</code> instead of <code>0</code> to start from new entries only), and
<code>XRANGE</code> reads a range of history without a group. The full list is in the
<a href="https://redis.io/docs/latest/develop/data-types/streams/">Redis Streams documentation</a>.</p>
<h2 id="consumer-groups-sharing-the-work" tabindex="-1"><a class="header-anchor" href="#consumer-groups-sharing-the-work">Consumer groups: sharing the work</a></h2>
<p>A consumer group is a named reader of the stream with its own position. Two things follow:</p>
<ul>
<li><strong>Inside one group, each entry goes to one consumer.</strong> Run three <code>billing</code> workers and they split the
entries between them, like a job queue.</li>
<li><strong>Each group sees every entry.</strong> A <code>billing</code> group and an <code>analytics</code> group both receive every order,
independently, like Pub/Sub that remembers.</li>
</ul>
<p>When a consumer reads an entry, it moves to that group’s <strong>pending list</strong> until the consumer
acknowledges it with <code>XACK</code>. That pending list is what makes crash recovery possible.</p>
<h2 id="tested-an-offline-consumer-catches-up" tabindex="-1"><a class="header-anchor" href="#tested-an-offline-consumer-catches-up">Tested: an offline consumer catches up</a></h2>
<p><strong>Setup for all the tests:</strong> Redis 8.10.3 in a Linux virtual machine (WSL2) on a laptop, with a Node.js
24 client using <code>ioredis</code> on the same laptop. Default configuration. Measured 7 November 2026.</p>
<p>We created a group, added 100 entries while no consumer was running, then started a consumer.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Step</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>100 entries added, no consumer connected</td>
<td>stored in the stream</td>
</tr>
<tr>
<td>Consumer starts and reads with <code>&gt;</code></td>
<td>100 of 100 received</td>
</tr>
</tbody>
</table></div>
<p>Compare that with Pub/Sub, where the same test delivered 0 of 100 (see
<a href="/blog/redis-pub-sub/">Redis Pub/Sub, tested</a>). This is the main reason to choose Streams: a deploy or
a crash does not lose events.</p>
<h2 id="tested-a-consumer-crashes-mid-job" tabindex="-1"><a class="header-anchor" href="#tested-a-consumer-crashes-mid-job">Tested: a consumer crashes mid-job</a></h2>
<p>Consumer <code>c1</code> read 10 entries and then “crashed” without acknowledging them.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Step</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>XPENDING</code> straight after</td>
<td>10 pending, owned by <code>c1</code></td>
</tr>
<tr>
<td>After 1 second idle, <code>XAUTOCLAIM</code> by consumer <code>c2</code></td>
<td><code>c2</code> took all 10</td>
</tr>
<tr>
<td><code>XPENDING</code> after the claim</td>
<td>10 pending, owned by <code>c2</code></td>
</tr>
<tr>
<td><code>c2</code> processes and acknowledges</td>
<td>0 pending</td>
</tr>
</tbody>
</table></div>
<p>Nothing was lost and nothing needed a manual fix. In production you would set the idle time to longer
than your slowest job, for example 60 seconds, and run <code>XAUTOCLAIM</code> from every consumer on a timer.</p>
<p>Two things to plan for:</p>
<ul>
<li><strong>An entry can be processed twice.</strong> If <code>c1</code> finished the work but crashed before <code>XACK</code>, <code>c2</code>
repeats it. Make handlers idempotent: check whether the order was already charged before charging it.</li>
<li><strong>Poison entries.</strong> An entry that crashes every consumer is claimed again and again. The extended form
<code>XPENDING orders billing - + 100</code> shows a delivery count per entry; after, say, five attempts, copy it to a dead-letter stream and
acknowledge it.</li>
</ul>
<h2 id="tested-memory-per-entry-and-trimming" tabindex="-1"><a class="header-anchor" href="#tested-memory-per-entry-and-trimming">Tested: memory per entry and trimming</a></h2>
<p>We added 100,000 entries with two short fields (<code>user</code> and <code>action</code>), then compared that with a plain
list holding the same data as strings.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What we stored</th>
<th>Memory per entry</th>
<th>Total for 100,000</th>
</tr>
</thead>
<tbody>
<tr>
<td>Stream entries, 2 small fields</td>
<td>20.8 bytes</td>
<td>2.0 MB</td>
</tr>
<tr>
<td>List items, same data</td>
<td>24.2 bytes</td>
<td>2.3 MB</td>
</tr>
<tr>
<td>Stream with <code>MAXLEN ~ 10000</code>, after 100,000 adds</td>
<td>n/a</td>
<td>0.20 MB, 10,000 entries kept</td>
</tr>
</tbody>
</table></div>
<p>Streams are compact: entries that share the same field names are packed together, so the field names
are not stored again for every entry. But <strong>a stream never shrinks on its own</strong>. <code>XACK</code> does not delete
anything; acknowledged entries stay until you trim. An event stream with no trimming will eventually
fill your store.</p>
<p>Trim on every write with <code>MAXLEN ~ N</code> (keep about N entries) or <code>MINID ~ &lt;id&gt;</code> (keep entries newer than
a time). The <code>~</code> lets Redis trim in efficient chunks rather than to the exact number, which is much
cheaper. In our test it kept 10,000 entries in 0.20 MB while 100,000 went through; usually <code>~</code> leaves slightly
more than N, because it trims whole internal blocks.</p>
<p>At about 21 bytes per small entry, a 128 MB store holds a few million entries before anything else.
Larger entries cost more; measure your own with <code>MEMORY USAGE</code>.</p>
<h2 id="tested-read-one-at-a-time-or-in-batches" tabindex="-1"><a class="header-anchor" href="#tested-read-one-at-a-time-or-in-batches">Tested: read one at a time, or in batches?</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>How we read</th>
<th>Entries per second</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>XADD</code>, pipelined in batches of 10,000</td>
<td>222,717 written</td>
</tr>
<tr>
<td><code>XREADGROUP COUNT 500</code>, then one <code>XACK</code> for the batch</td>
<td>189,573 consumed</td>
</tr>
<tr>
<td><code>XREADGROUP COUNT 1</code>, then <code>XACK</code>, one at a time</td>
<td>1,785 consumed</td>
</tr>
</tbody>
</table></div>
<p>Reading in batches was <strong>about 100 times faster</strong> than reading one entry at a time, with Redis on the
same laptop. Each single read costs a full round trip, so over a real network the gap only widens: at
9 ms per round trip (Mumbai to Mumbai, as measured in our
<a href="/blog/free-redis-hosting/">free Redis hosting comparison</a>) a one-at-a-time consumer would manage
roughly 50 entries a second. Use <code>COUNT</code> of 50 to 500 and acknowledge the whole batch in one <code>XACK</code>.</p>
<h2 id="streams-pubsub-lists-or-kafka" tabindex="-1"><a class="header-anchor" href="#streams-pubsub-lists-or-kafka">Streams, Pub/Sub, lists or Kafka?</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Pub/Sub</th>
<th>Lists (<code>LPUSH</code> / <code>BRPOP</code>)</th>
<th>Streams</th>
<th>Kafka</th>
</tr>
</thead>
<tbody>
<tr>
<td>Messages stored</td>
<td>No</td>
<td>Until popped</td>
<td>Until trimmed</td>
<td>By retention</td>
</tr>
<tr>
<td>Several independent readers</td>
<td>Yes, live only</td>
<td>No</td>
<td>Yes, consumer groups</td>
<td>Yes</td>
</tr>
<tr>
<td>Acknowledgement and recovery</td>
<td>No</td>
<td>Manual</td>
<td>Built in</td>
<td>Built in</td>
</tr>
<tr>
<td>Replay history</td>
<td>No</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Extra server to run</td>
<td>No</td>
<td>No</td>
<td>No</td>
<td>Yes</td>
</tr>
</tbody>
</table></div>
<p>Pick Streams when you already run Redis and need durable events with a few consumer groups. Pick a
dedicated broker such as Kafka when the event log is the centre of your system, needs to be kept for
weeks and is read by many teams. For background jobs with retries and delays, a queue library is less
work; see our <a href="/blog/bullmq-redis-job-queue/">Redis job queue guide</a>.</p>
<h2 id="a-worker-loop-you-can-copy" tabindex="-1"><a class="header-anchor" href="#a-worker-loop-you-can-copy">A worker loop you can copy</a></h2>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">import Redis from 'ioredis';
const r = new Redis(process.env.REDIS_URL);
const STREAM = 'orders', GROUP = 'billing', ME = `c-${process.pid}`;

// '0' = start from the oldest entry, so nothing written before the first worker is missed
await r.xgroup('CREATE', STREAM, GROUP, '0', 'MKSTREAM').catch((e) =&gt; {
  if (!String(e.message).includes('BUSYGROUP')) throw e;           // only &quot;group exists&quot; is fine
});

async function handle(id, fields) { /* your idempotent work */ }

for (;;) {
  // 1. take over anything a crashed consumer left behind for over 60 s
  const [, stale] = await r.xautoclaim(STREAM, GROUP, ME, 60000, '0', 'COUNT', 100);
  // 2. read new entries in a batch, waiting up to 5 s
  const res = await r.xreadgroup('GROUP', GROUP, ME, 'COUNT', 200, 'BLOCK', 5000, 'STREAMS', STREAM, '&gt;');
  const entries = [...stale, ...(res ? res[0][1] : [])];
  // (production: check delivery counts with XPENDING STREAM GROUP - + 100 and move entries
  //  delivered 5+ times to a dead-letter stream before handling them)
  for (const [id, fields] of entries) await handle(id, fields);
  if (entries.length) await r.xack(STREAM, GROUP, ...entries.map(([id]) =&gt; id));
}
</code></pre>
</div>
<p>Producers trim as they write:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">await r.xadd('orders', 'MAXLEN', '~', 100000, '*', 'id', '42', 'total', '499');
</code></pre>
</div>
<p>Run the loop as a long-running process, separate from your web server. On Voroa that is a
<a href="/background-workers/">background worker</a>, which never sleeps, next to a key value store in the same
Mumbai region; the store’s connection URL goes in an <a href="/docs/environment-variables/">environment variable</a>.
Store details are in the <a href="/docs/key-value/">Key Value guide</a>.</p>
<h2 id="run-streams-next-to-your-workers" tabindex="-1"><a class="header-anchor" href="#run-streams-next-to-your-workers">Run Streams next to your workers</a></h2>
<p>Voroa runs a Redis-compatible key value store in Mumbai beside the services and background workers you
deploy, with data written to disk and a free size to start. See the sizes on the
<a href="/managed-redis-india/">managed Redis page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>What is the difference between Redis Streams and Pub/Sub?</strong>
Streams store entries, so a consumer that was offline can catch up, and entries can be acknowledged and
retried. Pub/Sub stores nothing. In our tests, Streams delivered 100 of 100 entries to a late consumer;
Pub/Sub delivered 0.</p>
<p><strong>Does XACK delete the entry?</strong>
No. It only removes the entry from the group’s pending list. Trim the stream with <code>MAXLEN</code> or <code>MINID</code>
to free memory.</p>
<p><strong>How many consumers can a group have?</strong>
As many as you like. Each entry goes to one consumer in the group, so adding consumers spreads the
load.</p>
<p><strong>Are Redis Streams durable?</strong>
They are as durable as your Redis persistence. With data written to disk, entries survive a restart,
except possibly the last moments before a crash; on a store with no persistence, a restart loses the
stream.</p>
<p><strong>Can Redis Streams replace Kafka?</strong>
For modest volumes and a handful of consumer groups, often yes, and you avoid running another system.
For long retention, very high throughput or many teams reading the same log, a dedicated broker fits
better.</p>
<p><strong>Do Streams work in Valkey?</strong>
Yes. Streams are part of the core that Valkey kept from Redis 7.2. See
<a href="/blog/valkey-vs-redis/">Valkey vs Redis</a> for where the two differ.</p>
<p><em>Tested 7 November 2026 with Redis 8.10.3 in WSL2 on a laptop. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Redis TTL and EXPIRE: How Key Expiry Really Works, Tested</title>
      <link>https://getvoroa.com/blog/redis-ttl-expire/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/redis-ttl-expire/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>Node.js</category>
      <description><![CDATA[Redis TTL and EXPIRE explained with tests: return values, the SET command that silently removes a TTL, how fast expired keys free memory, and hash field expiry.]]></description>
      <content:encoded><![CDATA[<p>A Redis TTL (time to live) is the number of seconds a key has left before Redis deletes it on its own.
Expiry is what keeps a cache fresh, logs a session out, resets a rate limit and stops a small store from
filling up. This guide is for developers who set expiries and want to be sure they behave. It covers the
commands, the return values, one common mistake that silently turns an expiring key into a permanent
one, and what we measured about how quickly expired keys actually leave memory.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#the-commands">The commands</a></li>
<li><a href="#what-ttl-returns-1-and-2">What TTL returns: -1 and -2</a></li>
<li><a href="#the-set-that-deletes-your-expiry">The SET that deletes your expiry</a></li>
<li><a href="#how-expiry-actually-happens">How expiry actually happens</a></li>
<li><a href="#tested-how-fast-expired-keys-free-memory">Tested: how fast expired keys free memory</a></li>
<li><a href="#expiring-one-field-of-a-hash">Expiring one field of a hash</a></li>
<li><a href="#expiry-and-a-full-store">Expiry and a full store</a></li>
<li><a href="#four-patterns-worth-copying">Four patterns worth copying</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="the-commands" tabindex="-1"><a class="header-anchor" href="#the-commands">The commands</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Command</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SET key value EX 60</code></td>
<td>Set a value that expires in 60 seconds (<code>PX</code> for milliseconds)</td>
</tr>
<tr>
<td><code>SET key value EXAT 1798761600</code></td>
<td>Expire at a Unix timestamp (<code>PXAT</code> for milliseconds)</td>
</tr>
<tr>
<td><code>EXPIRE key 60</code></td>
<td>Add or change an expiry on an existing key</td>
</tr>
<tr>
<td><code>EXPIRE key 60 NX</code></td>
<td>Only if the key has no expiry yet (also <code>XX</code>, <code>GT</code>, <code>LT</code>; Redis 7 and later)</td>
</tr>
<tr>
<td><code>TTL key</code> / <code>PTTL key</code></td>
<td>Seconds / milliseconds left</td>
</tr>
<tr>
<td><code>PERSIST key</code></td>
<td>Remove the expiry; the key lives forever</td>
</tr>
<tr>
<td><code>GETEX key EX 60</code></td>
<td>Read a value and reset its expiry in one step</td>
</tr>
<tr>
<td><code>SET key value KEEPTTL</code></td>
<td>Change the value but keep the current expiry</td>
</tr>
<tr>
<td><code>HEXPIRE key 60 FIELDS 1 field</code></td>
<td>Expire one field of a hash (Redis 7.4, Valkey 9)</td>
</tr>
</tbody>
</table></div>
<p>The full reference is in the <a href="https://redis.io/docs/latest/commands/expire/">EXPIRE documentation</a>.</p>
<h2 id="what-ttl-returns-1-and-2" tabindex="-1"><a class="header-anchor" href="#what-ttl-returns-1-and-2">What TTL returns: -1 and -2</a></h2>
<p><strong>Setup for every test on this page:</strong> Redis 8.10.3 in a Linux virtual machine (WSL2) on a laptop, with
a Node.js 24 client using <code>ioredis</code>. Default configuration. Measured 7 November 2026.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Key</th>
<th><code>TTL</code> returned</th>
</tr>
</thead>
<tbody>
<tr>
<td>Set with <code>EX 60</code></td>
<td><code>60</code></td>
</tr>
<tr>
<td>Exists, no expiry</td>
<td><code>-1</code></td>
</tr>
<tr>
<td>Does not exist (or has already expired)</td>
<td><code>-2</code></td>
</tr>
</tbody>
</table></div>
<p>So <code>-1</code> means “this key will never expire”, which is often a bug in a cache, and <code>-2</code> means “it is
gone”. Check for both explicitly; treating any negative number as “expired” hides the <code>-1</code> case.</p>
<h2 id="the-set-that-deletes-your-expiry" tabindex="-1"><a class="header-anchor" href="#the-set-that-deletes-your-expiry">The SET that deletes your expiry</a></h2>
<p>This is a common cause of “my cache never expires” and “my store is full of old sessions”.
A plain <code>SET</code> on a key that already has an expiry <strong>removes the expiry</strong>.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Step</th>
<th><code>TTL</code> afterwards</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SET session v1 EX 3600</code></td>
<td>3600</td>
</tr>
<tr>
<td><code>SET session v2</code> (no options)</td>
<td><strong>-1, never expires</strong></td>
</tr>
<tr>
<td><code>SET session v3 EX 3600</code>, then <code>SET session v4 KEEPTTL</code></td>
<td>3600</td>
</tr>
<tr>
<td><code>INCR counter</code>, <code>EXPIRE counter 60</code>, then <code>INCR counter</code></td>
<td>60 (kept)</td>
</tr>
</tbody>
</table></div>
<p>Commands that change a value in place, such as <code>INCR</code>, <code>APPEND</code>, <code>HSET</code> or <code>LPUSH</code>, keep the expiry.
<code>SET</code> replaces the whole value and clears the expiry unless you pass <code>EX</code>, <code>PX</code> or <code>KEEPTTL</code>.
<code>GETSET</code> always clears it; use <code>SET key value GET KEEPTTL</code> instead. The safe habit: every <code>SET</code> on an expiring key carries its own <code>EX</code>, or <code>KEEPTTL</code> if you
mean to keep the old one.</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// wrong: the session silently becomes permanent
await redis.set(`sess:${id}`, JSON.stringify(session));

// right: refresh the full lifetime, or keep the remaining one
await redis.set(`sess:${id}`, JSON.stringify(session), 'EX', 1800);
await redis.set(`sess:${id}`, JSON.stringify(session), 'KEEPTTL');
</code></pre>
</div>
<h2 id="how-expiry-actually-happens" tabindex="-1"><a class="header-anchor" href="#how-expiry-actually-happens">How expiry actually happens</a></h2>
<p>Redis does not delete a key the instant its time is up. It uses two methods together:</p>
<ol>
<li><strong>On access.</strong> When you read a key whose time has passed, Redis deletes it there and then and
answers as if it never existed. You never get an expired value back.</li>
<li><strong>In the background.</strong> Several times a second, Redis samples keys that have an expiry and deletes
the expired ones it finds. It keeps going while it keeps finding many, so a large batch of expired
keys is cleared quickly.</li>
</ol>
<p>The result is that expired keys are invisible to your app immediately, but can take a moment to leave
memory, and <code>DBSIZE</code> may briefly count keys that have already expired.</p>
<h2 id="tested-how-fast-expired-keys-free-memory" tabindex="-1"><a class="header-anchor" href="#tested-how-fast-expired-keys-free-memory">Tested: how fast expired keys free memory</a></h2>
<p><img src="https://media.getvoroa.com/img/2994ccbfb8a65c03.webp" alt="Diagram: two ways Redis removes expired keys - on access, when a read finds an expired key and deletes it before answering; and in the background, when sampling several times a second finds and deletes expired keys; plus our measurement that 200,000 keys expiring together were gone in 2.5 seconds while 1.5 percent lingered when mixed with long-lived keys" width="700" loading="eager" decoding="async"></p>
<p><strong>All keys expiring together.</strong> We wrote 200,000 keys with 100-byte values and a 2-second expiry
(36.9 MB), then watched.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Time after loading finished</th>
<th>Keys counted by <code>DBSIZE</code></th>
<th>Memory above empty</th>
</tr>
</thead>
<tbody>
<tr>
<td>1.0 s</td>
<td>161,861</td>
<td>28.9 MB</td>
</tr>
<tr>
<td>2.5 s</td>
<td>0</td>
<td>0.0 MB</td>
</tr>
<tr>
<td>10 s</td>
<td>0</td>
<td>0.0 MB</td>
</tr>
</tbody>
</table></div>
<p>Some keys had expired during the load itself. Within 2.5 seconds of the last write, every key and every
byte was gone, without any reads. When most keys in a store are expired, the background cleanup finds
them fast.</p>
<p><strong>Expiring keys mixed with long-lived ones.</strong> We then wrote 200,000 keys where every second key expired
after 2 seconds and the rest after an hour, so 100,000 should remain.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Time</th>
<th>Keys counted by <code>DBSIZE</code></th>
<th>Expired keys still in memory</th>
</tr>
</thead>
<tbody>
<tr>
<td>3 s</td>
<td>101,714</td>
<td>1,714</td>
</tr>
<tr>
<td>5 s</td>
<td>101,607</td>
<td>1,607</td>
</tr>
<tr>
<td>10 s</td>
<td>101,483</td>
<td>1,483</td>
</tr>
</tbody>
</table></div>
<p>Here the cleanup slowed down: after 10 seconds about 1.5% of the expired keys were still in memory,
because random samples now mostly hit keys that are not due. They are still invisible to your app, and
a read deletes them on the spot. The practical lesson is small but real: on a nearly full store, do not
count on expired keys freeing their memory instantly.</p>
<h2 id="expiring-one-field-of-a-hash" tabindex="-1"><a class="header-anchor" href="#expiring-one-field-of-a-hash">Expiring one field of a hash</a></h2>
<p>Until recently an expiry applied to a whole key. Redis 7.4 added field-level expiry for hashes, and
Valkey 9.0 added the same commands.</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">text</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-text">HSET user:1 name Asha otp 482913
HEXPIRE user:1 2 FIELDS 1 otp      -&gt; 1  (expiry set)
# 2.5 seconds later
HGETALL user:1                     -&gt; name: Asha   (otp is gone)
</code></pre>
</div>
<p>We tested exactly that: after 2.5 seconds the <code>otp</code> field had disappeared and <code>name</code> remained. It is a
neat fit for one-time codes, per-device tokens or feature flags stored on a user record. Check that your
host’s engine version supports it before you rely on it.</p>
<h2 id="expiry-and-a-full-store" tabindex="-1"><a class="header-anchor" href="#expiry-and-a-full-store">Expiry and a full store</a></h2>
<p>Expiry and eviction are different things. Expiry deletes keys whose time is up. Eviction deletes keys
early because memory is full, and only if the store is configured to do so.</p>
<ul>
<li>With a <strong>no-eviction</strong> setting, a full store refuses new writes. Expiries are then your main way of
making room, so every cache key should have one.</li>
<li>The <strong>volatile</strong> eviction policies (<code>volatile-lru</code>, <code>volatile-ttl</code> and others) only ever evict keys
that have an expiry. If none of your keys have one, those policies behave like no-eviction.</li>
<li>The <strong>allkeys</strong> policies evict any key, expiry or not.</li>
</ul>
<p>On Voroa, a key value store’s <strong>When memory is full</strong> setting defaults to <strong>Keep everything</strong>, which
refuses writes when full, and you can switch it to a drop option for a pure cache. On the 128 MB free
size, consistent expiries are what keep a cache from filling up. The setting is explained in the
<a href="/docs/key-value/">Key Value guide</a>.</p>
<h2 id="four-patterns-worth-copying" tabindex="-1"><a class="header-anchor" href="#four-patterns-worth-copying">Four patterns worth copying</a></h2>
<p><strong>Sliding session expiry.</strong> Reset the lifetime on every request, in one command:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">const session = await redis.getex(`sess:${id}`, 'EX', 1800); // read and extend to 30 min
</code></pre>
</div>
<p><strong>Fixed-window rate limit.</strong> Count requests and start the window only on the first one:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// Redis 7+: count and set the window in one atomic transaction
const [[, n]] = await redis.multi()
  .incr(`rl:${ip}`)
  .expire(`rl:${ip}`, 60, 'NX')      // only starts the window; never extends it
  .exec();
if (n &gt; 100) return res.status(429).end();
</code></pre>
</div>
<p>On older versions you will see <code>INCR</code> followed by <code>EXPIRE</code> only when the count is 1. That works until
the process dies between the two calls: the counter then never expires, and that user is blocked for
good. On a version before 7, do the increment and the first expiry together in a short Lua script
instead.</p>
<p><strong>Cache with jitter.</strong> Add a few random seconds so thousands of keys written together do not all expire
in the same second and send every request to your database at once:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">const ttl = 300 + Math.floor(Math.random() * 60);
await redis.set(`cache:${key}`, value, 'EX', ttl);
</code></pre>
</div>
<p><strong>One-time codes.</strong> Store the code with a short expiry and delete it on use:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">await redis.set(`otp:${phone}`, code, 'EX', 300);
// on verify
const ok = (await redis.getdel(`otp:${phone}`)) === submitted;
</code></pre>
</div>
<p>If you run cleanup work on a timer instead, such as a nightly purge of old records in your database, a
<a href="/docs/scheduled-tasks-cron/">scheduled task</a> is the right tool; let Redis handle expiry for its own
keys. Jobs that must run later are a job queue’s delayed jobs, covered in our
<a href="/blog/bullmq-redis-job-queue/">Redis job queue guide</a>.</p>
<h2 id="run-it-next-to-your-app" tabindex="-1"><a class="header-anchor" href="#run-it-next-to-your-app">Run it next to your app</a></h2>
<p>Voroa runs a Redis-compatible key value store in Mumbai beside the <a href="/web-services/">web services</a> you
deploy, with a free size to start; the connection URL goes in an
<a href="/docs/environment-variables/">environment variable</a>. See the sizes on the
<a href="/managed-redis-india/">managed Redis page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>What does TTL -1 mean in Redis?</strong>
The key exists and has no expiry, so it will never be deleted on its own. A plain <code>SET</code> on an expiring
key is a common cause.</p>
<p><strong>What does TTL -2 mean?</strong>
The key does not exist, either because it was never set or because it has expired.</p>
<p><strong>Does SET remove the TTL?</strong>
Yes. <code>SET</code> without <code>EX</code>, <code>PX</code> or <code>KEEPTTL</code> clears any existing expiry. <code>INCR</code>, <code>HSET</code>, <code>LPUSH</code> and
similar in-place changes keep it.</p>
<p><strong>Are expired keys deleted immediately?</strong>
They become invisible immediately and are deleted when accessed or by background cleanup. In our test,
200,000 keys expiring together were gone from memory within 2.5 seconds; when mixed with long-lived
keys, about 1.5% lingered after 10 seconds.</p>
<p><strong>Can I set a TTL on one field of a hash?</strong>
Yes, with <code>HEXPIRE</code> on Redis 7.4 and later or Valkey 9.0 and later.</p>
<p><strong>What is the maximum TTL?</strong>
Expiry times are stored as millisecond Unix timestamps, so in practice any lifetime you need works. For
“forever”, do not set an expiry at all.</p>
<p><em>Tested 7 November 2026 with Redis 8.10.3 in WSL2 on a laptop. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Redis Vector Database: Vector Sets Tested on Memory and Recall</title>
      <link>https://getvoroa.com/blog/redis-vector-database/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/redis-vector-database/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Node.js</category>
      <description><![CDATA[A Redis vector database guide: vector sets in Redis 8, tested on 20,000 embeddings for memory per vector, query speed and recall, and when to use pgvector.]]></description>
      <content:encoded><![CDATA[<p>Running a Redis vector database means storing embeddings in Redis, the lists of numbers an AI model produces
for a piece of text or an image, and asking Redis for the ones most similar to a query. Redis 8 added a
data type for exactly this, vector sets, alongside the older search engine. This guide is for developers
adding semantic search, recommendations or retrieval for an AI feature. It explains the options, then
reports what we measured when we loaded 20,000 embeddings into Redis 8.10.3 in three storage formats:
memory per vector, insert speed, query time and how often the results matched an exact search.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#two-ways-to-store-vectors-in-redis">Two ways to store vectors in Redis</a></li>
<li><a href="#vector-sets-in-five-commands">Vector sets in five commands</a></li>
<li><a href="#what-we-measured">What we measured</a></li>
<li><a href="#quantization-the-setting-that-decides-memory">Quantization: the setting that decides memory</a></li>
<li><a href="#how-many-vectors-fit-in-your-store">How many vectors fit in your store</a></li>
<li><a href="#filtering-results-by-attributes">Filtering results by attributes</a></li>
<li><a href="#redis-or-pgvector">Redis or pgvector?</a></li>
<li><a href="#before-you-go-to-production">Before you go to production</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="two-ways-to-store-vectors-in-redis" tabindex="-1"><a class="header-anchor" href="#two-ways-to-store-vectors-in-redis">Two ways to store vectors in Redis</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Vector sets</th>
<th>Query engine (search)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Added in</td>
<td>Redis 8 (introduced as beta)</td>
<td>Earlier, as a module; part of Redis 8’s core distribution</td>
</tr>
<tr>
<td>Model</td>
<td>One key holds a set of named vectors</td>
<td>Vectors as fields inside hashes or JSON documents, with an index</td>
</tr>
<tr>
<td>Commands</td>
<td><code>VADD</code>, <code>VSIM</code>, <code>VREM</code>, <code>VSETATTR</code></td>
<td><code>FT.CREATE</code>, <code>FT.SEARCH</code></td>
</tr>
<tr>
<td>Index</td>
<td>HNSW, built automatically</td>
<td>HNSW or flat, defined in the index</td>
</tr>
<tr>
<td>Best for</td>
<td>Simple similarity search with light filtering</td>
<td>Combining vector search with full-text and numeric queries</td>
</tr>
<tr>
<td>Valkey</td>
<td>Not available</td>
<td>The separate valkey-search module</td>
</tr>
</tbody>
</table></div>
<p>Vector sets are the simplest way in, and they are what we tested. The rest of this guide uses them.</p>
<h2 id="vector-sets-in-five-commands" tabindex="-1"><a class="header-anchor" href="#vector-sets-in-five-commands">Vector sets in five commands</a></h2>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">text</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-text">VADD docs VALUES 3 0.12 0.48 0.31 doc:1        add a vector named doc:1
VSIM docs VALUES 3 0.10 0.50 0.30 COUNT 5      the 5 most similar
VSIM docs ELE doc:1 COUNT 5                    neighbours of an existing item
VREM docs doc:1                                remove one
VINFO docs                                     dimensions, size, quantization
</code></pre>
</div>
<p>From Node.js with <code>ioredis</code>, sending vectors as binary float32 is faster than text:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const f32 = (arr) =&gt; Buffer.from(new Float32Array(arr).buffer);

// store: embedding is an array of numbers from your model, e.g. 384 or 1536 long
await redis.call('VADD', 'docs', 'FP32', f32(embedding), `doc:${id}`);

// search: the 10 closest items to a query embedding
const ids = await redis.call('VSIM', 'docs', 'FP32', f32(queryEmbedding), 'COUNT', 10);
</code></pre>
</div>
<p>Vector sets compare by cosine similarity, so normalised embeddings from common text models work as they
come.</p>
<h2 id="what-we-measured" tabindex="-1"><a class="header-anchor" href="#what-we-measured">What we measured</a></h2>
<p><strong>Setup.</strong> Redis 8.10.3 built from source, in a Linux virtual machine (WSL2) on a laptop, with a Node.js
24 client on the same laptop. We generated 20,000 synthetic 384-dimension vectors, a common size for small
sentence-embedding models, grouped around 200 centres so that they behave more like real topics than pure
noise, plus 200 query vectors from the same distribution. For each storage format we inserted all 20,000
in pipelined batches, ran the 200 queries one at a time asking for the 10 nearest, and compared each
answer with an exact brute-force search. Measured 7 November 2026.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Format (<code>VADD</code> option)</th>
<th>Memory per vector</th>
<th>Insert rate</th>
<th>Query time, median (p99)</th>
<th>Recall@10</th>
</tr>
</thead>
<tbody>
<tr>
<td>Full precision (<code>NOQUANT</code>)</td>
<td>2,295 bytes</td>
<td>800 per second</td>
<td>1.73 ms (10.42 ms)</td>
<td>100.0%</td>
</tr>
<tr>
<td>8-bit, the default (<code>Q8</code>)</td>
<td>1,142 bytes</td>
<td>1,921 per second</td>
<td>1.25 ms (3.74 ms)</td>
<td>98.7%</td>
</tr>
<tr>
<td>Binary (<code>BIN</code>)</td>
<td>805 bytes</td>
<td>3,695 per second</td>
<td>1.02 ms (4.25 ms)</td>
<td>36.4%</td>
</tr>
</tbody>
</table></div>
<p><img src="https://media.getvoroa.com/img/5b96c91e3bb4bbba.webp" alt="Bar chart: memory per 384-dimension vector and recall at 10 for three formats - full precision 2,295 bytes and 100 percent, 8-bit 1,142 bytes and 98.7 percent, binary 805 bytes and 36.4 percent" width="700" loading="eager" decoding="async"></p>
<p><strong>What we found.</strong></p>
<ol>
<li><strong>The default is the right default.</strong> 8-bit storage halved memory against full precision and still
returned 98.7% of the exact top-10 results, while inserting more than twice as fast.</li>
<li><strong>Binary is a trap for this kind of data.</strong> It saved almost another third of the memory but found only 36% of
the true neighbours. It can suit models designed for binary embeddings; test before you use it.</li>
<li><strong>Queries were about a millisecond.</strong> All three formats answered in 1 to 2 ms at the median on 20,000
vectors. Most of a real request’s time will be spent creating the query embedding, not searching.</li>
<li><strong>The index costs extra.</strong> A raw 384-dimension float32 vector is 1,536 bytes; full precision used
2,295 bytes per vector, the difference being the graph that makes search fast.</li>
</ol>
<p>Synthetic data is kinder or harsher than your own depending on the model, so treat the recall figures as
a guide and check recall on a sample of your real queries.</p>
<h2 id="quantization-the-setting-that-decides-memory" tabindex="-1"><a class="header-anchor" href="#quantization-the-setting-that-decides-memory">Quantization: the setting that decides memory</a></h2>
<p>Quantization stores each number with fewer bits. <code>VADD</code> uses 8-bit by default, which you can confirm
with <code>VINFO</code> (it reports <code>quant-type int8</code>). Use <code>NOQUANT</code> only if your results must match an exact search
as closely as possible, and <code>BIN</code> only if your embedding model is designed for it. The choice is made
when the first vector is added to a key, so decide before you load data. The full option list is in the
<a href="https://redis.io/docs/latest/develop/data-types/vector-sets/">vector sets documentation</a>.</p>
<h2 id="how-many-vectors-fit-in-your-store" tabindex="-1"><a class="header-anchor" href="#how-many-vectors-fit-in-your-store">How many vectors fit in your store</a></h2>
<p>Plain division of the space by the measured size per vector, before attributes and headroom (1 MB =
1,048,576 bytes):</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Store memory available for data</th>
<th>384-dimension vectors, 8-bit</th>
<th>1,536-dimension vectors, 8-bit (estimate)</th>
</tr>
</thead>
<tbody>
<tr>
<td>75 MB</td>
<td>about 69,000</td>
<td>about 34,000</td>
</tr>
<tr>
<td>600 MB</td>
<td>about 550,000</td>
<td>about 270,000</td>
</tr>
<tr>
<td>6 GB</td>
<td>about 5.6 million</td>
<td>about 2.8 million</td>
</tr>
</tbody>
</table></div>
<p>The 1,536-dimension column is our estimate: in our test about 384 bytes of each 8-bit vector was the
vector itself and about 760 bytes the index, so we scaled only the vector part by four, to about 2.3 KB.
Measure your own.
Remember that a managed store’s advertised size is not all available for data. On Voroa, for example,
up to 40% of each instance can be held back for the nightly save, so a 1 GB store has about 600 MB for
data.</p>
<h2 id="filtering-results-by-attributes" tabindex="-1"><a class="header-anchor" href="#filtering-results-by-attributes">Filtering results by attributes</a></h2>
<p>Real searches usually need a filter: only this user’s documents, only products in stock. Vector sets let
you attach a small JSON object to each item and filter on it during the search:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">text</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-text">VSETATTR docs doc:1 '{&quot;lang&quot;:&quot;hi&quot;,&quot;year&quot;:2026}'
VSIM docs VALUES 3 0.10 0.50 0.30 COUNT 5 FILTER '.lang == &quot;hi&quot; and .year &gt;= 2025'
</code></pre>
</div>
<p>Filtering works best when many items match. If a filter keeps only a tiny fraction of the set, results
can come back slower or fewer than you asked for, because the search walks past many non-matching items.
For heavily filtered search, the query engine or a database with proper indexes is a better fit.</p>
<h2 id="redis-or-pgvector" tabindex="-1"><a class="header-anchor" href="#redis-or-pgvector">Redis or pgvector?</a></h2>
<p>Both are reasonable. The question is where the rest of your data lives.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Redis vector sets</th>
<th>Postgres with pgvector</th>
</tr>
</thead>
<tbody>
<tr>
<td>Speed</td>
<td>About 1 ms per query in our test</td>
<td>Fast with an HNSW index; depends on the query plan</td>
</tr>
<tr>
<td>Memory</td>
<td>All vectors held in RAM</td>
<td>Index can live on disk, data on disk</td>
</tr>
<tr>
<td>Filtering</td>
<td>Simple attribute filters</td>
<td>Full SQL: joins, permissions, any condition</td>
</tr>
<tr>
<td>Durability</td>
<td>As good as your Redis persistence</td>
<td>A real database, with backups and transactions</td>
</tr>
<tr>
<td>Best when</td>
<td>A fast similarity layer over data kept elsewhere</td>
<td>Vectors belong next to the rows they describe</td>
</tr>
</tbody>
</table></div>
<p>Choose Redis when you need very low latency and the vector set is a cache or a derived index you can
rebuild. Choose pgvector when vectors sit beside your business data and you want SQL filters,
transactions and backups in one place. We covered that route in
<a href="/blog/postgres-vector-database-india/">Postgres as a vector database</a>.</p>
<h2 id="before-you-go-to-production" tabindex="-1"><a class="header-anchor" href="#before-you-go-to-production">Before you go to production</a></h2>
<ul>
<li><strong>Check your engine version.</strong> Vector sets need Redis 8 or later. Valkey does not have them; it offers
vector search through its separate search module. See <a href="/blog/valkey-vs-redis/">Valkey vs Redis</a>.</li>
<li><strong>Plan for rebuilds.</strong> Keep the source text and model version in your database, so you can re-embed
and reload when you change models. Vectors from different models cannot be mixed in one set.</li>
<li><strong>Persistence.</strong> Vectors held only in memory are lost on a restart without persistence; re-embedding
everything is slow and costs API money. Use a store that writes to disk.</li>
<li><strong>Do not evict.</strong> A vector set is one key. An eviction policy that removes keys under memory pressure
can delete your entire index at once. Use a no-eviction setting, and watch memory.</li>
<li><strong>Keep it close.</strong> Each search is a round trip; keep the store in the same region as the app that
creates the query embedding.</li>
</ul>
<h2 id="vector-search-on-voroa" tabindex="-1"><a class="header-anchor" href="#vector-search-on-voroa">Vector search on Voroa</a></h2>
<p>Voroa’s key value store runs Redis 7.4 today, which does not have vector sets, so it is not the place for
this. For vector search on Voroa, use <a href="/managed-postgresql-india/">managed PostgreSQL</a> with the pgvector
extension, which you can turn on from the database’s settings on a paid plan; the steps are in
<a href="/docs/postgres-extensions/">enabling PostgreSQL extensions</a>. Your <a href="/web-services/">web services</a> reach it
over the private network in Mumbai. The key value store remains a good fit for caching the results of
those searches; see the <a href="/managed-redis-india/">managed Redis page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Can Redis be used as a vector database?</strong>
Yes. Redis 8 has vector sets for similarity search, and its query engine supports vector indexes inside
hashes and JSON documents.</p>
<p><strong>How much memory does a vector take in Redis?</strong>
In our test, a 384-dimension vector took about 1,142 bytes with the default 8-bit format and 2,295 bytes
at full precision, including the search index.</p>
<p><strong>How accurate is Redis vector search?</strong>
With the default format, 98.7% of the exact top-10 results in our test, and 100% at full precision.
Binary storage dropped to 36% on our data.</p>
<p><strong>Is Redis faster than pgvector?</strong>
Redis answered our queries in about a millisecond from memory. pgvector can also be fast with an HNSW
index, and it offers SQL filters and durability. Pick by where your data lives.</p>
<p><strong>Does Valkey support vector sets?</strong>
No. Valkey provides vector search through the valkey-search module, which uses a different set of
commands.</p>
<p><strong>What embedding size should I use?</strong>
That is set by your model, for example 384 for small sentence models and 1,536 or more for larger ones.
Memory grows roughly in proportion to the size.</p>
<p><em>Tested 7 November 2026 with Redis 8.10.3 in WSL2 on a laptop, using synthetic embeddings. Found
something out of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Redis vs Memcached in 2026: Features, Memory and Speed Tested</title>
      <link>https://getvoroa.com/blog/redis-vs-memcached/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/redis-vs-memcached/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Redis vs Memcached compared on data types, persistence, threading and limits, with a test we ran on memory use and speed on a small server. Which cache to pick.]]></description>
      <content:encoded><![CDATA[<p>Redis vs Memcached is the oldest question in caching, and most answers to it are a decade old. Both
keep data in memory and answer in well under a millisecond; Memcached does one thing, caching strings,
while Redis adds data types, persistence and replication on top. This guide is for developers choosing
a cache for a web app in 2026. It compares the two on what actually differs today, and reports what we
measured when we ran the current release of each on the same small server: memory used for the same
data, and requests per second.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#quick-verdict-for-a-new-app">Quick verdict for a new app</a></li>
<li><a href="#feature-by-feature">Feature by feature</a></li>
<li><a href="#we-tested-both-on-the-same-machine">We tested both on the same machine</a></li>
<li><a href="#threads-the-argument-that-used-to-decide-it">Threads: the argument that used to decide it</a></li>
<li><a href="#what-memcached-still-does-better">What Memcached still does better</a></li>
<li><a href="#what-only-redis-can-do">What only Redis can do</a></li>
<li><a href="#which-one-to-use">Which one to use</a></li>
<li><a href="#moving-from-memcached-to-redis">Moving from Memcached to Redis</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="quick-verdict-for-a-new-app" tabindex="-1"><a class="header-anchor" href="#quick-verdict-for-a-new-app">Quick verdict for a new app</a></h2>
<p>For a new web app, use Redis (or Valkey). On a small server it is at least as fast as Memcached in our
test, uses about the same memory for plain cached strings, and also gives you sessions that survive a
restart, counters, queues and pub/sub from the same service. Choose Memcached only when you need a pure
string cache on a large many-core machine, or your stack already depends on it.</p>
<h2 id="feature-by-feature" tabindex="-1"><a class="header-anchor" href="#feature-by-feature">Feature by feature</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Redis 8</th>
<th>Memcached 1.6</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data types</td>
<td>Strings, hashes, lists, sets, sorted sets, streams, JSON and more</td>
<td>Strings (bytes) only</td>
</tr>
<tr>
<td>Largest value</td>
<td>512 MB</td>
<td>1 MB by default</td>
</tr>
<tr>
<td>Persistence to disk</td>
<td>Snapshots and an append-only log</td>
<td>None; a restart empties it</td>
</tr>
<tr>
<td>Replication and failover</td>
<td>Yes</td>
<td>No (clients spread keys across servers)</td>
</tr>
<tr>
<td>Expiry</td>
<td>Per key, and per hash field</td>
<td>Per key</td>
</tr>
<tr>
<td>When memory is full</td>
<td>Configurable: refuse writes, or evict by several policies</td>
<td>Evicts least recently used, per size class</td>
</tr>
<tr>
<td>Threading</td>
<td>One thread runs commands; optional I/O threads</td>
<td>Multi-threaded</td>
</tr>
<tr>
<td>Transactions and scripts</td>
<td><code>MULTI</code>, Lua scripts and functions</td>
<td>Compare-and-set (<code>cas</code>) and atomic <code>incr</code> / <code>decr</code> only</td>
</tr>
<tr>
<td>Pub/Sub, queues</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Licence</td>
<td>RSALv2, SSPLv1 or AGPLv3 (Valkey: BSD)</td>
<td>BSD</td>
</tr>
</tbody>
</table></div>
<p>The pattern is clear: every feature Memcached has, Redis has too. The reverse is not true.</p>
<h2 id="we-tested-both-on-the-same-machine" tabindex="-1"><a class="header-anchor" href="#we-tested-both-on-the-same-machine">We tested both on the same machine</a></h2>
<p>Most comparisons quote benchmarks from years ago, or none at all. We ran Redis 8.10.3 and Memcached
1.6.39, both built from source, side by side.</p>
<p><strong>Setup.</strong> A Linux virtual machine (WSL2) on a 4-vCPU laptop. Each server got 2 CPU cores; Memcached ran
2 worker threads, Redis its default single command thread with persistence off. The same client, a
small raw-socket program we wrote that speaks both protocols, ran on the other 2 cores with 50
connections against 100,000 keys holding 100-byte values. Five runs of 5 seconds each; we report the
median. Measured 7 November 2026.</p>
<p><strong>Memory for the same data</strong></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>100,000 keys, 100-byte values</th>
<th>Redis 8.10.3</th>
<th>Memcached 1.6.39</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data size reported by the server</td>
<td>16.7 MB (<code>used_memory</code>)</td>
<td>16.8 MB (<code>bytes</code>)</td>
</tr>
<tr>
<td>Process memory, empty</td>
<td>7.4 MB</td>
<td>2.4 MB</td>
</tr>
<tr>
<td>Process memory with the data</td>
<td>24.8 MB</td>
<td>23.0 MB</td>
</tr>
</tbody>
</table></div>
<p>For plain cached strings the two are effectively the same. Memcached starts smaller, and grew by a few
megabytes more while loading, because it hands out memory in fixed-size slabs.</p>
<p><strong>Requests per second</strong> (2 server cores, 50 connections)</p>
<p><img src="https://media.getvoroa.com/img/b60834641e4566a7.webp" alt="Bar chart: median requests per second on a 2-core server - without pipelining Redis 79,682 GET and 69,527 SET, Memcached 50,243 GET and 42,100 SET; with 16 requests per round trip Redis 705,680 GET and 563,171 SET, Memcached 676,650 GET and 569,376 SET" width="700" loading="eager" decoding="async"></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Test</th>
<th>Redis 8.10.3, median (5 runs)</th>
<th>Memcached 1.6.39, median (5 runs)</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET, one request at a time</td>
<td>79,682 /s (79k-81k)</td>
<td>50,243 /s (48k-52k)</td>
</tr>
<tr>
<td>SET, one request at a time</td>
<td>69,527 /s (62k-71k)</td>
<td>42,100 /s (40k-44k)</td>
</tr>
<tr>
<td>GET, 16 per round trip</td>
<td>705,680 /s (633k-796k)</td>
<td>676,650 /s (621k-718k)</td>
</tr>
<tr>
<td>SET, 16 per round trip</td>
<td>563,171 /s (476k-659k)</td>
<td>569,376 /s (540k-595k)</td>
</tr>
</tbody>
</table></div>
<p><strong>What we found.</strong></p>
<ol>
<li><strong>Without pipelining, Redis was faster here,</strong> by about 60%, and the run ranges did not overlap.
Read that with care: the client is our own program, it parses two different protocols, and we did
not measure how much of the gap its parsing accounts for. Our guess is that on 2 cores,
Memcached’s threads compete with each other for the same cores, but we did not prove it.</li>
<li><strong>With pipelining, they tied.</strong> The ranges overlap, and at around 600,000 to 700,000 requests a
second our single-threaded client is probably part of the limit for both.</li>
<li><strong>Pipelining mattered far more than the choice of cache:</strong> about 8 to 14 times the throughput on
both.</li>
</ol>
<p>This is a small-machine test on purpose, because that is what most apps rent. It is not the whole
story: Memcached’s design pays off when a server has many cores and thousands of clients, which this
test does not cover. Treat the numbers as “what to expect on a small instance”, not a universal ranking.</p>
<h2 id="threads-the-argument-that-used-to-decide-it" tabindex="-1"><a class="header-anchor" href="#threads-the-argument-that-used-to-decide-it">Threads: the argument that used to decide it</a></h2>
<p>The classic advice was “Memcached is multi-threaded, Redis is single-threaded, so Memcached scales
better.” That was true and still partly is: Memcached spreads requests across all its threads, while
Redis runs every command on one main thread.</p>
<p>Two things have changed. Since version 6, Redis can move network reading and writing onto extra
I/O threads, which is designed to narrow the gap on bigger machines (we did not test that). And most apps never come close to the
limit of one Redis thread, which handled about 80,000 requests a second in our test without any
batching. If you reach that, pipelining or a second instance is usually cheaper than switching.</p>
<h2 id="what-memcached-still-does-better" tabindex="-1"><a class="header-anchor" href="#what-memcached-still-does-better">What Memcached still does better</a></h2>
<ul>
<li><strong>Many cores, one big cache.</strong> On a large machine serving huge numbers of small GETs, Memcached uses
every core without any tuning.</li>
<li><strong>Simplicity.</strong> There is almost nothing to configure and nothing to persist. For a throwaway cache,
that is a feature.</li>
<li><strong>Predictable memory.</strong> The slab allocator avoids fragmentation surprises on long-running caches,
at the cost of some wasted space per size class.</li>
<li><strong>Existing stacks.</strong> Older PHP setups and some frameworks have long-standing Memcached support, and
WordPress can use it through common object-cache plugins (as it can Redis).</li>
</ul>
<h2 id="what-only-redis-can-do" tabindex="-1"><a class="header-anchor" href="#what-only-redis-can-do">What only Redis can do</a></h2>
<p>These are the reasons most new apps pick Redis even when they only “need a cache” today:</p>
<ul>
<li><strong>Sessions that survive a restart.</strong> With persistence on, users stay logged in after the cache
restarts. Memcached logs everyone out.</li>
<li><strong>Richer counters and rate limits.</strong> Both have atomic increments, but Redis can combine a counter, its
expiry and a check atomically in one Lua script or <code>MULTI</code> transaction.</li>
<li><strong>Job queues and events.</strong> Lists, Streams and Pub/Sub power libraries such as Sidekiq, Celery and
BullMQ. See our <a href="/blog/bullmq-redis-job-queue/">Redis job queue guide</a> and
<a href="/blog/redis-streams/">Redis Streams tested</a>.</li>
<li><strong>Rich structures.</strong> Leaderboards with sorted sets, objects with hashes, unique visitors with
HyperLogLog.</li>
<li><strong>A choice of what happens when memory is full.</strong> Refuse writes to protect important data, or evict
for a cache.</li>
</ul>
<h2 id="which-one-to-use" tabindex="-1"><a class="header-anchor" href="#which-one-to-use">Which one to use</a></h2>
<ul>
<li><strong>A new web app, any language:</strong> Redis or Valkey. One service covers cache, sessions and queues.</li>
<li><strong>A pure cache on a large many-core server, already tuned for Memcached:</strong> keep Memcached.</li>
<li><strong>WordPress object cache on shared hosting:</strong> use whichever your host provides; both work.</li>
<li><strong>Sessions or anything you would mind losing on restart:</strong> Redis with persistence. Memcached cannot
do it.</li>
<li><strong>You care about an open source licence:</strong> Memcached is BSD; so is Valkey, the Redis fork. Our
<a href="/blog/valkey-vs-redis/">Valkey vs Redis comparison</a> covers that choice.</li>
</ul>
<p>Distance matters more than either engine. A cache in another country adds its round trip to every
read; put it in the same region as the app.</p>
<h2 id="moving-from-memcached-to-redis" tabindex="-1"><a class="header-anchor" href="#moving-from-memcached-to-redis">Moving from Memcached to Redis</a></h2>
<ol>
<li><strong>Swap the client, not the logic.</strong> <code>get</code>, <code>set</code> with an expiry and <code>delete</code> map directly to Redis
<code>GET</code>, <code>SET key value EX seconds</code> and <code>DEL</code>.</li>
<li><strong>Do not copy the data.</strong> A cache rebuilds itself. Point the app at Redis and let it warm up.</li>
<li><strong>Pick an eviction policy.</strong> For a pure cache, set <code>allkeys-lru</code> so Redis behaves like Memcached
when full (<a href="https://redis.io/docs/latest/develop/reference/eviction/">eviction policies</a> explains each one). If you will also store sessions or queues there, keep “refuse writes” instead, or run two
separate stores.</li>
<li><strong>Watch memory for a day</strong> and size the instance for your working set.</li>
</ol>
<p>On Voroa, a key value store asks this as <strong>When memory is full</strong>: keep <strong>Keep everything</strong> for sessions
and queues, or choose a drop option for a pure cache. It runs in Mumbai next to the
<a href="/web-services/">web services</a> you deploy, and the connection URL goes in an
<a href="/docs/environment-variables/">environment variable</a>. Details are in the
<a href="/docs/key-value/">Key Value guide</a>.</p>
<h2 id="put-your-cache-next-to-your-app" tabindex="-1"><a class="header-anchor" href="#put-your-cache-next-to-your-app">Put your cache next to your app</a></h2>
<p>A Redis-compatible store in Mumbai, billed in rupees with a GST invoice, with a free size to start.
See the sizes on the <a href="/managed-redis-india/">managed Redis page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Is Redis faster than Memcached?</strong>
On our 2-core test, Redis was faster without pipelining and tied with it. Memcached can pull ahead on
large many-core servers with many clients, which we did not test.</p>
<p><strong>Does Memcached use less memory than Redis?</strong>
For plain strings, about the same: 16.7 MB in Redis and 16.8 MB in Memcached for our 100,000 keys.
Memcached’s empty process is smaller.</p>
<p><strong>Can Memcached save data to disk?</strong>
No. A restart empties it. Redis can write snapshots and an append-only log.</p>
<p><strong>Is Memcached still used in 2026?</strong>
Yes, especially in older PHP stacks and at very large scale. Most new projects choose Redis or Valkey
for the extra features.</p>
<p><strong>Can I use Redis just as a cache?</strong>
Yes. Set a memory limit, an eviction policy such as <code>allkeys-lru</code>, and an expiry on your keys, and it
behaves like Memcached.</p>
<p><strong>What is the maximum value size?</strong>
Redis strings can be up to 512 MB. Memcached items are limited to 1 MB by default.</p>
<p><em>Tested 7 November 2026 with Redis 8.10.3 and Memcached 1.6.39 in WSL2 on a laptop. Found something out
of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Redis vs Postgres: When You Actually Need Both, Measured</title>
      <link>https://getvoroa.com/blog/redis-vs-postgres/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/redis-vs-postgres/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Redis vs Postgres for caching, sessions, counters and queues, with latency we measured for both. When Postgres alone is enough, and when Redis earns its place.]]></description>
      <content:encoded><![CDATA[<p>Redis vs Postgres is usually framed as a fight, but most production apps run both: Postgres as the
database that keeps the truth, Redis as fast memory beside it. The real question is whether your app
needs the second one yet. This guide is for developers deciding whether to add Redis to a Postgres app,
or to drop it. We measured the same key-value operations on both, on the same laptop, and the result
surprises many people: for a single lookup, Postgres is already fast. Redis earns its place under heavy
load and for other jobs, which we list with the numbers.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#what-each-one-is-for">What each one is for</a></li>
<li><a href="#we-measured-the-same-operations-on-both">We measured the same operations on both</a></li>
<li><a href="#what-the-numbers-really-say">What the numbers really say</a></li>
<li><a href="#where-redis-earns-its-place">Where Redis earns its place</a></li>
<li><a href="#what-postgres-can-do-instead">What Postgres can do instead</a></li>
<li><a href="#a-simple-decision-guide">A simple decision guide</a></li>
<li><a href="#running-both-without-trouble">Running both without trouble</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="what-each-one-is-for" tabindex="-1"><a class="header-anchor" href="#what-each-one-is-for">What each one is for</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Postgres</th>
<th>Redis</th>
</tr>
</thead>
<tbody>
<tr>
<td>Role</td>
<td>The system of record</td>
<td>Fast working memory</td>
</tr>
<tr>
<td>Data lives</td>
<td>On disk, cached in memory</td>
<td>In memory, optionally saved to disk</td>
</tr>
<tr>
<td>Durability</td>
<td>Every committed write survives a crash</td>
<td>The last moments before a crash can be lost</td>
</tr>
<tr>
<td>Queries</td>
<td>SQL: joins, filters, aggregates, transactions</td>
<td>Lookups by key, plus data-structure commands</td>
</tr>
<tr>
<td>Built-in expiry</td>
<td>No</td>
<td>Yes, per key</td>
</tr>
<tr>
<td>Strong at</td>
<td>Relationships, correctness, reporting</td>
<td>Counters, sessions, rate limits, queues, pub/sub</td>
</tr>
</tbody>
</table></div>
<p>Neither replaces the other well. Postgres is a poor place for data that should vanish on a timer; Redis
is a poor place for anything you cannot afford to lose or need to query in new ways later.</p>
<h2 id="we-measured-the-same-operations-on-both" tabindex="-1"><a class="header-anchor" href="#we-measured-the-same-operations-on-both">We measured the same operations on both</a></h2>
<p><strong>Setup.</strong> On one laptop: Postgres 16.15 running natively on Windows, and Redis 8.10.3 in a Linux
virtual machine (WSL2), both loaded with 100,000 keys holding 100-byte values (a Postgres table with a
text primary key). The client was Node.js 24 with <code>ioredis</code> and <code>pg</code>, one connection each, sending 5,000
operations one after another with random keys, after a warm-up. We repeated each test three times; the
table shows the third run, and the other two were within a few hundredths of a millisecond. Measured 7
November 2026.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Operation</th>
<th>Median</th>
<th>99th percentile</th>
</tr>
</thead>
<tbody>
<tr>
<td>Redis <code>PING</code> (round trip only)</td>
<td>0.306 ms</td>
<td>0.794 ms</td>
</tr>
<tr>
<td>Redis <code>GET</code></td>
<td>0.335 ms</td>
<td>0.687 ms</td>
</tr>
<tr>
<td>Redis <code>SET</code></td>
<td>0.334 ms</td>
<td>0.717 ms</td>
</tr>
<tr>
<td>Postgres <code>SELECT 1</code> (round trip only)</td>
<td>0.123 ms</td>
<td>0.330 ms</td>
</tr>
<tr>
<td>Postgres <code>SELECT v FROM kv WHERE k = $1</code></td>
<td>0.151 ms</td>
<td>0.372 ms</td>
</tr>
<tr>
<td>Postgres upsert, normal table</td>
<td>0.315 ms</td>
<td>0.629 ms</td>
</tr>
<tr>
<td>Postgres upsert, <code>UNLOGGED</code> table</td>
<td>0.174 ms</td>
<td>0.408 ms</td>
</tr>
</tbody>
</table></div>
<p>One caution on reading this table: Redis sat behind a virtual machine boundary and Postgres did not, so
Redis’s round trip was about 0.2 ms slower before it did any work. That is why we include a do-nothing
command for each. Compare each operation with its own round trip, not the two databases’ raw numbers.</p>
<p><img src="https://media.getvoroa.com/img/aa210e29acffe93f.webp" alt="Bar chart: time each server spent on the work beyond its own round trip - Redis GET about 0.03 ms, Postgres primary-key SELECT about 0.03 ms, Postgres unlogged upsert about 0.05 ms, Postgres normal upsert about 0.19 ms" width="700" loading="eager" decoding="async"></p>
<p><strong>Memory for the same data:</strong> Redis used 17.9 MB of RAM for the 100,000 keys; the Postgres table and
its index took 18.8 MB, on disk and cached in memory when used often.</p>
<h2 id="what-the-numbers-really-say" tabindex="-1"><a class="header-anchor" href="#what-the-numbers-really-say">What the numbers really say</a></h2>
<p>Subtract each server’s round trip and you see the work itself:</p>
<ol>
<li><strong>A primary-key read costs Postgres about 0.03 ms.</strong> Redis’s <code>GET</code> cost about the same beyond its
round trip. For “fetch the row with this ID” at low concurrency, a cache in front of Postgres barely
changes how long one request waits. We measured latency with one connection, not capacity: under many
concurrent clients Postgres spends far more CPU per lookup than Redis, and that is where a cache in
front of it often pays off. We did not measure that here.</li>
<li><strong>A durable write costs Postgres about 0.19 ms more</strong> than its round trip, because it records the
change safely before answering. That cost depends heavily on the disk; ours was a laptop SSD, and
cloud volumes are often slower. The same upsert on an <code>UNLOGGED</code> table, which skips that step, cost
about 0.05 ms. Redis writes were close to its round trip.</li>
<li><strong>The network dwarfs all of it.</strong> From a server in Mumbai we measured about 9 ms to a store in the
Mumbai region at another provider (on the same private network it is far less) and about 86 ms to Singapore (see our
<a href="/blog/free-redis-hosting/">free Redis hosting comparison</a>). A 9 ms round trip is about fifty times the slowest
database work we measured, so the region matters far more than which database it is.</li>
</ol>
<p>So “put Redis in front of Postgres to make reads fast” is clearly right for expensive queries and for
high-concurrency read load, and much less important for occasional simple lookups by key.</p>
<h2 id="where-redis-earns-its-place" tabindex="-1"><a class="header-anchor" href="#where-redis-earns-its-place">Where Redis earns its place</a></h2>
<p>These are the jobs where Redis is clearly the better tool, even when Postgres is fast:</p>
<ul>
<li><strong>Heavy read load.</strong> Many concurrent requests for the same hot data cost Postgres far more CPU than
Redis. A cache lets the database spend its capacity on queries only it can answer.</li>
<li><strong>Expensive results.</strong> A query that joins five tables and takes 300 ms is worth caching. Compute once,
store the result with an expiry, serve it in a millisecond.</li>
<li><strong>Data that should disappear on a timer.</strong> Sessions, one-time codes, rate-limit windows. Redis deletes
expired keys for you; Postgres needs a cleanup job. Our <a href="/blog/redis-ttl-expire/">Redis TTL guide</a>
covers the traps.</li>
<li><strong>Very frequent small writes to the same rows.</strong> Counters, view counts, “last seen” timestamps. In
Postgres every update writes a new row version and must later be cleaned up; a busy counter becomes a
hot spot. In Redis it is one <code>INCR</code>.</li>
<li><strong>Background job queues.</strong> Libraries such as BullMQ, Sidekiq and Celery run on Redis. See our
<a href="/blog/bullmq-redis-job-queue/">Redis job queue guide</a>.</li>
<li><strong>Fan-out to many servers.</strong> Pub/Sub delivers a message to every connected instance, which is how chat
and live dashboards stay in sync across servers.</li>
<li><strong>Protecting the database from spikes.</strong> A cache absorbs a burst of identical reads so they never reach
Postgres.</li>
</ul>
<h2 id="what-postgres-can-do-instead" tabindex="-1"><a class="header-anchor" href="#what-postgres-can-do-instead">What Postgres can do instead</a></h2>
<p>If you would rather run one system for now, Postgres covers more than people expect:</p>
<ul>
<li><strong><code>UNLOGGED</code> tables for cache-like data.</strong> Faster writes, as measured above, at the cost of being
emptied after a crash. The <a href="https://www.postgresql.org/docs/current/sql-createtable.html">CREATE TABLE documentation</a>
describes the trade-off.</li>
<li><strong><code>SELECT ... FOR UPDATE SKIP LOCKED</code></strong> turns a table into a reliable job queue that many workers can
pull from without stepping on each other.</li>
<li><strong><code>LISTEN</code> / <code>NOTIFY</code></strong> sends simple notifications between connected clients.</li>
<li><strong>A scheduled cleanup</strong> (<code>DELETE ... WHERE expires_at &lt; now()</code>) stands in for key expiry at modest
volumes.</li>
</ul>
<p>Each of these is a reasonable starting point. Each becomes awkward at high volume, which is usually the
moment to add Redis.</p>
<h2 id="a-simple-decision-guide" tabindex="-1"><a class="header-anchor" href="#a-simple-decision-guide">A simple decision guide</a></h2>
<ul>
<li><strong>New app, modest traffic:</strong> start with Postgres alone. Add Redis when you have a specific job from the
list above, not “for speed”.</li>
<li><strong>Slow pages:</strong> find the slow query first. If it is a lookup by key, add an index. If it is genuinely
expensive, cache its result in Redis.</li>
<li><strong>Sessions:</strong> fine in Postgres at low volume; move them to Redis with an expiry when they become a large
share of your writes.</li>
<li><strong>Background jobs:</strong> Redis with a queue library, or <code>SKIP LOCKED</code> in Postgres if you want one system.</li>
<li><strong>Anything you cannot lose:</strong> Postgres, always.</li>
</ul>
<h2 id="running-both-without-trouble" tabindex="-1"><a class="header-anchor" href="#running-both-without-trouble">Running both without trouble</a></h2>
<ul>
<li><strong>Postgres is the source of truth.</strong> Anything in Redis should be rebuildable from Postgres, or be
something you can afford to lose.</li>
<li><strong>Set expiries on cached data,</strong> so a missed invalidation corrects itself.</li>
<li><strong>Invalidate on write.</strong> When you update a row, delete its cache key in the same code path.</li>
<li><strong>Keep them together.</strong> Put both in the same region as your app; a cache in another region is slower
than no cache.</li>
</ul>
<p>On Voroa, a <a href="/managed-postgresql-india/">managed PostgreSQL</a> database and a Redis-compatible key value
store run side by side in Mumbai, next to the <a href="/web-services/">web services</a> you deploy, each with a free
size to start. The store’s details are in the <a href="/docs/key-value/">Key Value guide</a>, and its sizes on the
<a href="/managed-redis-india/">managed Redis page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Is Redis faster than Postgres?</strong>
For the work itself, both handled a single lookup by key in about 0.03 ms in our one-connection test;
under heavy concurrent load Redis serves many more lookups per CPU core, which we did not measure. Redis is faster for
writes that do not need to be durable and for counters. The network round trip, not the database, set
most of the time.</p>
<p><strong>Should I use Redis as my main database?</strong>
Usually not. It keeps data in memory, can lose the last moments before a crash, and cannot run the
queries a relational database can. Use it beside Postgres.</p>
<p><strong>Can Postgres replace Redis?</strong>
For small apps, often yes: unlogged tables for cache-like data, <code>SKIP LOCKED</code> for queues, <code>LISTEN</code> and
<code>NOTIFY</code> for notifications. At high volumes Redis is simpler and faster.</p>
<p><strong>Do I need Redis for sessions?</strong>
Not at first. Postgres handles sessions fine at modest traffic. Redis helps when session writes become a
large share of your database load, and its expiry clears old sessions automatically.</p>
<p><strong>How do I keep Redis and Postgres in sync?</strong>
Treat Postgres as the truth, write there first, then delete or update the Redis key. Give cached keys an
expiry so any missed update corrects itself.</p>
<p><strong>Which is cheaper to run?</strong>
For the same data, our test used similar memory in both. Redis must hold everything in memory, while
Postgres keeps data on disk and caches what is used, so large datasets are usually cheaper in Postgres.</p>
<p><em>Measured 7 November 2026 with Postgres 16.15 and Redis 8.10.3 on a laptop. Found something out of date?
Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Systemd Timer: A Tested Guide to OnCalendar and Persistent</title>
      <link>https://getvoroa.com/blog/systemd-timer/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/systemd-timer/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <description><![CDATA[How to write a systemd timer: the service and timer pair, OnCalendar syntax with timezones, Persistent catch-up and overlap, tested on systemd 259.]]></description>
      <content:encoded><![CDATA[<p>A systemd timer runs a program on a schedule, like a cron job, but it is managed by systemd. You get
logs in the journal, protection against overlapping runs, and an option to catch up on runs the machine
missed while it was off.</p>
<p>This guide walks through a working example, the <code>OnCalendar</code> syntax, and three behaviours we measured on
systemd 259 (Ubuntu): how accurate the start time is, what happens when a run is slow, and how
<code>Persistent=</code> catches up.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#how-it-works-two-files">How it works: two files</a></li>
<li><a href="#a-complete-example">A complete example</a></li>
<li><a href="#oncalendar-syntax">OnCalendar syntax</a></li>
<li><a href="#timezones-in-oncalendar">Timezones in OnCalendar</a></li>
<li><a href="#what-we-measured">What we measured</a></li>
<li><a href="#commands-you-will-use">Commands you will use</a></li>
<li><a href="#timers-compared-with-cron">Timers compared with cron</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="how-it-works-two-files" tabindex="-1"><a class="header-anchor" href="#how-it-works-two-files">How it works: two files</a></h2>
<p>A timer is always a pair of unit files with the same name:</p>
<ul>
<li><code>backup.service</code> says <strong>what</strong> to run.</li>
<li><code>backup.timer</code> says <strong>when</strong> to run it.</li>
</ul>
<p>When the timer elapses, systemd starts the service. Because the service is an ordinary unit, you can
also run it by hand with <code>systemctl start backup.service</code> to test it, without waiting for the schedule.</p>
<h2 id="a-complete-example" tabindex="-1"><a class="header-anchor" href="#a-complete-example">A complete example</a></h2>
<p>A nightly backup at 2 am. Put both files in <code>/etc/systemd/system/</code>:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">ini</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-ini"># backup.service
[Unit]
Description=Nightly database backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">ini</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-ini"># backup.timer
[Unit]
Description=Run the backup every night at 2 am

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
</code></pre>
</div>
<p>Then load and enable it:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
</code></pre>
</div>
<p>Enable the <strong>timer</strong>, not the service. The service has no <code>[Install]</code> section on purpose: it should only
start when the timer fires.</p>
<h2 id="oncalendar-syntax" tabindex="-1"><a class="header-anchor" href="#oncalendar-syntax">OnCalendar syntax</a></h2>
<p>The format is <code>DayOfWeek Year-Month-Day Hour:Minute:Second</code>, and any part can be <code>*</code>. Check an expression
before you use it with <code>systemd-analyze calendar</code>. These were all checked that way:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>You want</th>
<th>OnCalendar</th>
</tr>
</thead>
<tbody>
<tr>
<td>Every day at midnight</td>
<td><code>daily</code> (same as <code>*-*-* 00:00:00</code>)</td>
</tr>
<tr>
<td>Every day at 9 am</td>
<td><code>*-*-* 09:00</code></td>
</tr>
<tr>
<td>Every 5 minutes</td>
<td><code>*:0/5</code></td>
</tr>
<tr>
<td>Weekdays at 9:30 am</td>
<td><code>Mon..Fri 09:30</code></td>
</tr>
<tr>
<td>Mondays at 2 am</td>
<td><code>Mon *-*-* 02:00</code></td>
</tr>
<tr>
<td>1st of every month at 2 am</td>
<td><code>*-*-01 02:00</code></td>
</tr>
</tbody>
</table></div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">systemd-analyze calendar --iterations=2 'Mon..Fri 09:30'
</code></pre>
</div>
<p>It prints the normalised form and the next run times, which catches most mistakes. For a schedule
relative to boot or to the last run instead of the clock, use <code>OnBootSec=</code> or <code>OnUnitActiveSec=</code>.</p>
<h2 id="timezones-in-oncalendar" tabindex="-1"><a class="header-anchor" href="#timezones-in-oncalendar">Timezones in OnCalendar</a></h2>
<p>A plain <code>OnCalendar</code> uses the machine’s timezone, which is usually UTC on a cloud server. You can also
add a timezone name at the end. We checked both of these with <code>systemd-analyze</code> on a UTC machine:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>OnCalendar</th>
<th>Next run, in UTC</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>*-*-* 09:00 Asia/Kolkata</code></td>
<td>03:30</td>
</tr>
<tr>
<td><code>*-*-01 02:00 Asia/Kolkata</code></td>
<td>20:30 on the last day of the previous month</td>
</tr>
</tbody>
</table></div>
<p>The second line matters. With standard cron on a UTC server, “2 am IST on the 1st” cannot be written,
because it falls on a different date each month in UTC. Our <a href="/blog/cron-timezone-ist/">cron timezone guide</a>
explains that trap. A timer with a timezone name handles it.</p>
<h2 id="what-we-measured" tabindex="-1"><a class="header-anchor" href="#what-we-measured">What we measured</a></h2>
<p>We ran three tests with user timers on systemd 259. Each one wrote a timestamp to a file whenever its
service started and ended.</p>
<p><strong>1. Start accuracy.</strong> systemd’s documentation says the default <code>AccuracySec=1min</code> lets it delay a start
by up to a minute, to group wake-ups together. On our idle machine, timers with the default and with
<code>AccuracySec=1s</code> both started within one second of the minute. If you need the exact second, set
<code>AccuracySec=1s</code> anyway, because a busier machine may not behave like ours.</p>
<p><strong>2. Slow runs do not overlap.</strong> A timer every 10 seconds started a job that took 25 seconds. In 60
seconds it started <strong>3 runs, back to back</strong>: at 0, 25 and 50 seconds. A new run began the moment the
last one ended, and two never ran at once.</p>
<p><img src="https://media.getvoroa.com/img/17d6e8a8687f32bd.webp" alt="Diagram: a job scheduled every 10 seconds that takes 25 seconds; cron would start a new copy every 10 seconds so up to three run at once, while the timer ran them back to back with never more than one" width="700" loading="eager" decoding="async"></p>
<p>Cron behaves differently. It starts a new copy at every match, so the same job would have had up to three
copies running together. That is why cron jobs need <code>flock</code>, as our
<a href="/blog/cron-job-not-running/">cron troubleshooting checklist</a> explains.</p>
<p><strong>3. Persistent catches up once.</strong> We stopped a 10-second timer for 35 seconds, which skipped three runs,
then started it again. With <code>Persistent=false</code>, nothing ran until the next scheduled time. With
<code>Persistent=true</code>, <strong>one</strong> run started 0.1 seconds after the restart, not three.</p>
<p>So <code>Persistent=true</code> is the setting for a laptop or a server that reboots: a daily job missed overnight
runs once at boot. It also means a daily job can run soon after every boot if the machine is often off at
the scheduled time, which surprises some people.</p>
<h2 id="commands-you-will-use" tabindex="-1"><a class="header-anchor" href="#commands-you-will-use">Commands you will use</a></h2>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">systemctl list-timers --all              # every timer, last and next run
systemctl status backup.timer            # is it enabled and waiting?
journalctl -u backup.service --since today   # output of the runs
sudo systemctl start backup.service      # run the job now, by hand
sudo systemctl disable --now backup.timer    # stop scheduling it
</code></pre>
</div>
<p>For a timer that belongs to your own user, put the files in <code>~/.config/systemd/user/</code> and add <code>--user</code>
to each <code>systemctl</code> command. A user timer only runs while you are logged in, unless lingering is enabled
with <code>loginctl enable-linger</code>.</p>
<h2 id="timers-compared-with-cron" tabindex="-1"><a class="header-anchor" href="#timers-compared-with-cron">Timers compared with cron</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Cron</th>
<th>systemd timers</th>
</tr>
</thead>
<tbody>
<tr>
<td>Files per job</td>
<td>One crontab line</td>
<td>A <code>.service</code> and a <code>.timer</code></td>
</tr>
<tr>
<td>Output</td>
<td>Mailed or lost unless redirected</td>
<td>In the journal</td>
</tr>
<tr>
<td>Overlapping runs</td>
<td>Starts a new copy every time</td>
<td>Does not start a second copy</td>
</tr>
<tr>
<td>Missed runs while off</td>
<td>Lost</td>
<td>Caught up once with <code>Persistent=true</code> (<code>OnCalendar</code> timers only)</td>
</tr>
<tr>
<td>Timezone per job</td>
<td>Only with <code>CRON_TZ</code> on some crons</td>
<td>A zone name in <code>OnCalendar</code></td>
</tr>
<tr>
<td>Check a schedule</td>
<td>By eye</td>
<td><code>systemd-analyze calendar</code></td>
</tr>
</tbody>
</table></div>
<p>Cron is still quicker for a one-line job. Timers are better when the job matters: you get its output,
no overlap and a catch-up after downtime. For the basics of cron itself, see
<a href="/blog/what-is-a-cron-job/">what a cron job is</a>.</p>
<p>The full option list is in the systemd manual pages for
<a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html">systemd.timer</a> and
<a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html">calendar expressions</a>.</p>
<h2 id="scheduled-jobs-on-voroa" tabindex="-1"><a class="header-anchor" href="#scheduled-jobs-on-voroa">Scheduled jobs on Voroa</a></h2>
<p>On a hosting platform you usually do not get systemd at all: your app runs in a container. On Voroa, a
<strong>Cron Job</strong> or <strong>Scheduled Task</strong> takes a cron expression, read in India Standard Time, and runs your
command inside a service that stays running.</p>
<p>It behaves more like cron than a timer. A slow run can overlap the next, and runs missed while the
service is stopped are skipped, not caught up. So keep jobs safe to run twice. See
<a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a> and
<a href="/docs/worker-or-cron-job/">worker or cron job</a>. Programs that should run all the time belong on the
<a href="/background-workers/">background workers page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>How do I list systemd timers?</strong>
Run <code>systemctl list-timers --all</code>. It shows each timer with its last and next run.</p>
<p><strong>Do I enable the timer or the service?</strong>
The timer. Enabling it with <code>systemctl enable --now name.timer</code> makes it start at boot and begin
scheduling.</p>
<p><strong>How do I run a timer every 5 minutes?</strong>
Use <code>OnCalendar=*:0/5</code>, or <code>OnUnitActiveSec=5min</code> for every 5 minutes after the last run.</p>
<p><strong>What does Persistent=true do?</strong>
If the machine was off when a run was due, the job runs once as soon as the timer starts again, for
example at boot. It works only with <code>OnCalendar=</code> timers, and does nothing on a timer’s very first start.</p>
<p><strong>Can a systemd timer run twice at the same time?</strong>
No. In our test a slow job was started again only after the previous run ended.</p>
<p><strong>Which timezone does OnCalendar use?</strong>
The machine’s timezone, unless you add a zone name such as <code>Asia/Kolkata</code> at the end of the expression.</p>
<p><em>Tested on systemd 259 (Ubuntu) on 7 November 2026. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Upstash Pricing Explained: What a Real App Pays in 2026</title>
      <link>https://getvoroa.com/blog/upstash-pricing/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/upstash-pricing/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Upstash pricing for Redis worked out for real apps: when pay-as-you-go beats fixed plans, what idle queue workers cost, and the numbers in rupees for India.]]></description>
      <content:encoded><![CDATA[<p>Upstash pricing has two very different models for the same Redis database: pay per command, or a fixed
monthly price. Which one is cheaper depends almost entirely on how many commands your app sends, a number
most people do not know until the bill arrives. This guide is for developers deciding whether Upstash fits
their app, especially in India. It lays out the plans as published on 7 November 2026, then works through
real workloads, including queue workers whose command counts we measured ourselves, and shows the totals
in dollars and rupees.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#the-plans-at-a-glance">The plans at a glance</a></li>
<li><a href="#the-break-even-point-about-2-commands-a-second">The break-even point: about 2 commands a second</a></li>
<li><a href="#five-real-workloads-priced">Five real workloads, priced</a></li>
<li><a href="#why-queue-workers-change-the-answer">Why queue workers change the answer</a></li>
<li><a href="#how-to-count-your-own-commands">How to count your own commands</a></li>
<li><a href="#the-free-tier-and-its-limits">The free tier, and its limits</a></li>
<li><a href="#where-upstash-is-the-right-choice">Where Upstash is the right choice</a></li>
<li><a href="#alternatives-billed-by-memory">Alternatives billed by memory</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="the-plans-at-a-glance" tabindex="-1"><a class="header-anchor" href="#the-plans-at-a-glance">The plans at a glance</a></h2>
<p>As published on Upstash’s Redis pricing page on 7 November 2026. Rupee figures use Rs 93.92 per US
dollar (the European Central Bank reference rate for 6 November 2026) and exclude any tax.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Plan</th>
<th>Price</th>
<th>Data</th>
<th>Commands</th>
<th>Bandwidth</th>
</tr>
</thead>
<tbody>
<tr>
<td>Free</td>
<td>$0</td>
<td>250 MB</td>
<td>500,000 a month</td>
<td>10 GB a month</td>
</tr>
<tr>
<td>Pay as you go</td>
<td>$0.20 per 100,000 commands, storage $0.25 per GB beyond the first 1 GB</td>
<td>up to 100 GB</td>
<td>up to 10,000 per second</td>
<td>200 GB free, then $0.03 per GB</td>
</tr>
<tr>
<td>Fixed 250 MB</td>
<td>$10 a month (about Rs 939)</td>
<td>250 MB</td>
<td>up to 10,000 per second, no monthly count</td>
<td>50 GB</td>
</tr>
<tr>
<td>Fixed 1 GB</td>
<td>$20 a month (about Rs 1,878)</td>
<td>1 GB</td>
<td>up to 10,000 per second</td>
<td>100 GB</td>
</tr>
<tr>
<td>Fixed 5 GB</td>
<td>$100 a month</td>
<td>5 GB</td>
<td>up to 10,000 per second</td>
<td>500 GB</td>
</tr>
<tr>
<td>Fixed 10 GB</td>
<td>$200 a month</td>
<td>10 GB</td>
<td>up to 10,000 per second</td>
<td>1 TB</td>
</tr>
</tbody>
</table></div>
<p>Fixed plans add a charge for each extra read region. A Prod Pack (uptime commitment, multi-zone, encryption
at rest, SOC-2 and monitoring) adds $200 a month per database.</p>
<p>One counting rule matters a lot: Upstash bills a Lua script as one command plus one for every Redis call
the script makes. Libraries that do their work in scripts, such as job queues, use many more billed
commands than their network traffic suggests.</p>
<h2 id="the-break-even-point-about-2-commands-a-second" tabindex="-1"><a class="header-anchor" href="#the-break-even-point-about-2-commands-a-second">The break-even point: about 2 commands a second</a></h2>
<p>The Fixed 250 MB plan costs $10. At $0.20 per 100,000 commands, $10 buys 5 million pay-as-you-go
commands. Spread over a 30-day month, that is about <strong>1.9 commands per second, on average, around the
clock</strong>.</p>
<ul>
<li>Below that, pay-as-you-go is cheaper.</li>
<li>Above it, the fixed plan is cheaper, and the gap grows with every command.</li>
</ul>
<p>That holds while your data fits in 250 MB. Pay-as-you-go includes the first 1 GB of storage free, so
for 250 MB to 1 GB of data the fair comparison is Fixed 1 GB at $20, which breaks even at 10 million
commands, about 3.9 a second.</p>
<p><img src="https://media.getvoroa.com/img/c3aa5658b60fdda2.webp" alt="Line chart: monthly cost against average commands per second - pay-as-you-go rises in a straight line from zero while the fixed 250 MB plan stays flat at 10 dollars; the lines cross at about 1.9 commands per second" width="700" loading="eager" decoding="async"></p>
<p>Two commands a second sounds like a lot for a small app, but it is easy to reach without many users, as
the next section shows.</p>
<h2 id="five-real-workloads-priced" tabindex="-1"><a class="header-anchor" href="#five-real-workloads-priced">Five real workloads, priced</a></h2>
<p>Pay-as-you-go cost for 30 days at $0.20 per 100,000 commands, against the fixed plan that would hold the
same data.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Workload</th>
<th>Commands a month</th>
<th>Pay-as-you-go</th>
<th>Fixed 250 MB</th>
</tr>
</thead>
<tbody>
<tr>
<td>Hobby project, a few thousand cache reads a day</td>
<td>about 100,000</td>
<td>$0.20 (Rs 19)</td>
<td>$10 (Rs 939)</td>
</tr>
<tr>
<td>One idle Celery worker, no tasks</td>
<td>about 4.3 million</td>
<td>$8.64 (Rs 811)</td>
<td>$10 (Rs 939)</td>
</tr>
<tr>
<td>One idle BullMQ worker, no jobs</td>
<td>about 4.8 million</td>
<td>$9.54 (Rs 896)</td>
<td>$10 (Rs 939)</td>
</tr>
<tr>
<td>Small app: 1 request a second, 3 commands each</td>
<td>about 7.8 million</td>
<td>$15.55 (Rs 1,461)</td>
<td>$10 (Rs 939)</td>
</tr>
<tr>
<td>Busier app: 20 requests a second, 2 commands each</td>
<td>about 104 million</td>
<td>$207.36 (Rs 19,475)</td>
<td>$10 (Rs 939)</td>
</tr>
</tbody>
</table></div>
<p>The last row is the one that surprises people. A steady 20 requests a second is a modest production app,
and on pay-as-you-go its cache costs twenty times the fixed plan. The first row is where pay-as-you-go
shines: almost no traffic, almost no bill.</p>
<h2 id="why-queue-workers-change-the-answer" tabindex="-1"><a class="header-anchor" href="#why-queue-workers-change-the-answer">Why queue workers change the answer</a></h2>
<p>The two idle-worker rows come from our own measurements, not estimates. We ran one worker of each library
against Redis with an empty queue and counted every command the server executed, including commands run
inside the libraries’ Lua scripts, which is how Upstash bills them:</p>
<ul>
<li>A <strong>BullMQ</strong> worker executed about 110 commands a minute while idle. Details in our
<a href="/blog/bullmq-redis-job-queue/">BullMQ measurements</a>.</li>
<li>A <strong>Celery</strong> worker executed about 100 commands a minute while idle. Details in our
<a href="/blog/celery-redis/">Celery guide</a>.</li>
</ul>
<p>An idle worker polls for new work and checks on its peers every few seconds, all day. On a per-command
plan you pay for that waiting; each worker costs roughly $9 to $10 a month before it does any work, and
real jobs add about 30 commands each on BullMQ. Upstash’s own billing documentation says the same thing
and recommends a fixed plan for worker-heavy setups. If your app has a job queue, compare against the
fixed plans, or against a host that does not count commands at all.</p>
<h2 id="how-to-count-your-own-commands" tabindex="-1"><a class="header-anchor" href="#how-to-count-your-own-commands">How to count your own commands</a></h2>
<p>Before choosing, measure. On any Redis you already run:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">redis-cli INFO stats | grep total_commands_processed
# wait one hour, run it again, subtract, multiply by 720 for a 30-day month
</code></pre>
</div>
<p><a href="https://redis.io/docs/latest/commands/info/"><code>INFO commandstats</code></a> breaks the total down by command, which shows quickly whether a worker’s polling or
your own reads dominate. In the Upstash console, the database page shows daily command counts once you
have traffic.</p>
<h2 id="the-free-tier-and-its-limits" tabindex="-1"><a class="header-anchor" href="#the-free-tier-and-its-limits">The free tier, and its limits</a></h2>
<p>The free database gives 250 MB, 500,000 commands and 10 GB of bandwidth a month, one database per
account, with no card. Two catches matter in practice: one idle queue worker uses the 500,000 commands in
about three days, and a free database is archived after at least 30 days without activity (you can
restore it). For a side-by-side of every free Redis tier, see our
<a href="/blog/free-redis-hosting/">free Redis hosting comparison</a>.</p>
<h2 id="where-upstash-is-the-right-choice" tabindex="-1"><a class="header-anchor" href="#where-upstash-is-the-right-choice">Where Upstash is the right choice</a></h2>
<ul>
<li><strong>Serverless and edge functions.</strong> Upstash answers over HTTP as well as the Redis protocol, so a
function does not need to hold a connection open. That is its real strength, and few hosts match it.</li>
<li><strong>Spiky or very low traffic.</strong> Pay-as-you-go costs almost nothing when nothing happens.</li>
<li><strong>Global reads.</strong> Read regions put copies of your data near users in several countries.</li>
</ul>
<p>It is a weaker fit for long-running servers with steady traffic and for anything with background
workers, where a flat price is simpler and usually cheaper. Upstash offers that too, through its fixed
plans; the mistake is staying on pay-as-you-go out of habit.</p>
<h2 id="alternatives-billed-by-memory" tabindex="-1"><a class="header-anchor" href="#alternatives-billed-by-memory">Alternatives billed by memory</a></h2>
<p>If your app runs in India on long-running servers, a store billed by memory removes the command-count
question entirely. For comparison, Voroa’s key value sizes are priced in rupees, billed with a GST
invoice, and never count commands:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Voroa key value size</th>
<th>Memory</th>
<th>Price before GST</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cache Nano (free)</td>
<td>128 MB</td>
<td>Rs 0</td>
</tr>
<tr>
<td>Cache Starter</td>
<td>256 MB</td>
<td>Rs 425 per month</td>
</tr>
<tr>
<td>Cache Standard</td>
<td>1 GB</td>
<td>Rs 1,349 per month</td>
</tr>
</tbody>
</table></div>
<p>The memory figure is the whole instance, and up to 40% can be held back for the nightly save, so a
256 MB store may hold about 150 MB of data. It runs in Mumbai beside the
<a href="/web-services/">web services</a> and <a href="/background-workers/">background workers</a> you deploy, with no
eviction by default, which is what a job queue needs. The other sizes are on the
<a href="/managed-redis-india/">managed Redis page</a>, and setup is in the <a href="/docs/key-value/">Key Value guide</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>How much does Upstash Redis cost per month?</strong>
Pay-as-you-go is $0.20 per 100,000 commands plus storage beyond 1 GB. Fixed plans start at $10 a month
for 250 MB with no monthly command count. The free plan includes 500,000 commands.</p>
<p><strong>Is Upstash pay-as-you-go or fixed cheaper?</strong>
Below about 5 million commands a month (roughly 2 a second on average), pay-as-you-go. Above that, the
fixed plan.</p>
<p><strong>Why is my Upstash bill high?</strong>
Usually background polling: job queue workers, cron libraries and health checks send commands all day.
Scripts are billed per Redis call inside them, too.</p>
<p><strong>Does Upstash charge for idle connections?</strong>
It charges for commands, not connections. An idle queue worker still sends commands while it waits, and
those are billed.</p>
<p><strong>Is there an Upstash region in India?</strong>
Upstash lists Mumbai (<code>ap-south-1</code>) among its AWS regions. Choose it if your app runs in India.</p>
<p><strong>Can I pay Upstash in rupees with a GST invoice?</strong>
Upstash prices in US dollars. If you need rupee billing with a GST invoice, look for an Indian provider.</p>
<p><em>Prices from Upstash’s Redis pricing page and billing documentation on 7 November 2026. Converted at
Rs 93.92 per US dollar. Prices change; check the Upstash page before you commit. Found something out of
date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Valkey vs Redis in 2026: Licence, Speed and Which to Pick</title>
      <link>https://getvoroa.com/blog/valkey-vs-redis/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/valkey-vs-redis/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Valkey vs Redis compared on licence, features, compatibility and cost, plus a benchmark we ran on both. Which one to pick for your app, and how to switch.]]></description>
      <content:encoded><![CDATA[<p>Valkey vs Redis is mostly a licence story with a performance argument attached. Valkey is the
open source fork of Redis that started in 2024 when Redis changed its licence; both speak the same
protocol and your client library works with either. This guide is for developers deciding which one
to run or which one a host should run for them. It covers what actually differs in November 2026,
what we measured when we benchmarked the latest release of each on the same machine, and when the
choice matters at all.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#the-short-answer">The short answer</a></li>
<li><a href="#how-one-database-became-two">How one database became two</a></li>
<li><a href="#side-by-side-what-differs-in-november-2026">Side by side: what differs in November 2026</a></li>
<li><a href="#we-benchmarked-both-on-the-same-machine">We benchmarked both on the same machine</a></li>
<li><a href="#where-the-two-are-no-longer-interchangeable">Where the two are no longer interchangeable</a></li>
<li><a href="#which-licence-affects-you">Which licence affects you</a></li>
<li><a href="#cost-where-the-engine-changes-the-bill">Cost: where the engine changes the bill</a></li>
<li><a href="#which-one-to-pick">Which one to pick</a></li>
<li><a href="#switching-from-redis-to-valkey">Switching from Redis to Valkey</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="the-short-answer" tabindex="-1"><a class="header-anchor" href="#the-short-answer">The short answer</a></h2>
<p>For caching, sessions, rate limits and queues, Redis and Valkey behave the same and perform the
same on small machines. Pick Valkey if you want a permissive BSD licence and lower managed prices.
Pick Redis 8 if you need its built-in JSON, search and vector features, or Redis Ltd’s own
cloud and support. If a host runs one for you, the engine rarely changes your code.</p>
<h2 id="how-one-database-became-two" tabindex="-1"><a class="header-anchor" href="#how-one-database-became-two">How one database became two</a></h2>
<p>Redis was BSD-licensed open source for 15 years. Then the licence changed, a fork followed, and both
projects have shipped steadily since.</p>
<p><img src="https://media.getvoroa.com/img/4efe3495b6832c96.webp" alt="Timeline: March 2024 Redis announces RSALv2 and SSPLv1 from 7.4; March 2024 Valkey forks Redis 7.2.4 under the Linux Foundation; September 2024 Valkey 8.0 with I/O threads; May 2025 Redis 8 adds AGPLv3; October 2025 Valkey 9.0; October 2026 Redis 8.10.3 and Valkey 9.1.3" width="700" loading="eager" decoding="async"></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>When</th>
<th>What happened</th>
</tr>
</thead>
<tbody>
<tr>
<td>March 2024</td>
<td>Redis announces a move from BSD to a dual RSALv2 / SSPLv1 licence, starting with 7.4. Neither is an open source licence.</td>
</tr>
<tr>
<td>March 2024</td>
<td>Contributors fork the last BSD release, 7.2.4, and start Valkey under the Linux Foundation.</td>
</tr>
<tr>
<td>September 2024</td>
<td>Valkey 8.0 ships with a new I/O threading design.</td>
</tr>
<tr>
<td>May 2025</td>
<td>Redis 8 adds AGPLv3, an open source licence, as a third option, and folds JSON, time series, probabilistic types and the query engine into the core.</td>
</tr>
<tr>
<td>October 2025</td>
<td>Valkey 9.0 adds hash field expiration and atomic slot migration for clusters.</td>
</tr>
<tr>
<td>May 2026</td>
<td>Valkey 9.1, announced as a security and performance release.</td>
</tr>
<tr>
<td>26 October 2026</td>
<td>The latest releases at the time of writing: Redis 8.10.3 and Valkey 9.1.3.</td>
</tr>
</tbody>
</table></div>
<p>So both are open source again, in different ways. Redis offers a copyleft licence (AGPLv3) next to its
source-available ones. Valkey stays permissive (BSD-3-Clause).</p>
<h2 id="side-by-side-what-differs-in-november-2026" tabindex="-1"><a class="header-anchor" href="#side-by-side-what-differs-in-november-2026">Side by side: what differs in November 2026</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Redis</th>
<th>Valkey</th>
</tr>
</thead>
<tbody>
<tr>
<td>Licence</td>
<td>RSALv2, SSPLv1 or AGPLv3, your choice</td>
<td>BSD-3-Clause</td>
</tr>
<tr>
<td>Governed by</td>
<td>Redis Ltd</td>
<td>Linux Foundation, many companies</td>
</tr>
<tr>
<td>Latest release</td>
<td>8.10.3 (26 Oct 2026)</td>
<td>9.1.3 (26 Oct 2026)</td>
</tr>
<tr>
<td>Protocol and core commands</td>
<td>RESP2 / RESP3</td>
<td>Same</td>
</tr>
<tr>
<td>Hash field expiry</td>
<td>Since 7.4</td>
<td>Since 9.0, same API</td>
</tr>
<tr>
<td>JSON, Bloom filters, search</td>
<td>Built into the core since Redis 8</td>
<td>Separate modules (valkey-json, valkey-bloom, valkey-search), shipped together as valkey-bundle</td>
</tr>
<tr>
<td>Vector similarity</td>
<td>Vector sets data type (added in Redis 8 as beta) and the query engine</td>
<td>valkey-search module</td>
</tr>
<tr>
<td>Reads data files from</td>
<td>Its own versions</td>
<td>Redis up to 7.2</td>
</tr>
<tr>
<td>Managed by the big clouds</td>
<td>Redis Cloud; Azure Managed Redis</td>
<td>AWS ElastiCache and MemoryDB, Google Memorystore, Aiven and many smaller hosts</td>
</tr>
</tbody>
</table></div>
<p>The everyday commands you use for a cache or a queue (<code>GET</code>, <code>SET</code>, <code>EXPIRE</code>, <code>INCR</code>, lists, hashes,
sorted sets, streams, pub/sub, Lua scripts) are the same in both. What a managed store adds on top of
the engine (backups, TLS, password rotation) is covered in <a href="/docs/managed-databases/">managed databases on Voroa</a>.</p>
<h2 id="we-benchmarked-both-on-the-same-machine" tabindex="-1"><a class="header-anchor" href="#we-benchmarked-both-on-the-same-machine">We benchmarked both on the same machine</a></h2>
<p>Most comparison pages repeat headline numbers like “1 million requests per second” or “37% faster”
without saying where they came from. Valkey’s own 1.19 million figure was measured with 8 I/O threads
on a 64-core AWS machine. Most apps run on something much smaller, so we tested that.</p>
<p><strong>Setup.</strong> We built Redis 8.10.3 and Valkey 9.1.3 from source and ran both inside the same Linux
virtual machine (WSL2) on a 4-vCPU laptop. That is a modest, noisy machine, which is part of the point:
it is closer to a small cloud instance than a 64-core benchmark rig. We gave the server 2 CPU cores and the benchmark client 2 other cores, and turned persistence off on both. The same
client (<code>redis-benchmark</code>) sent 1,000,000 commands per run with 50 connections, 256-byte values and
100,000 random keys. We ran every test 5 times and report the median. Measured 7 November 2026.</p>
<p><img src="https://media.getvoroa.com/img/88594106025512cf.webp" alt="Bar chart: median requests per second on a 2-core server - without pipelining Redis 116k SET and 111k GET, Valkey 116k SET and 114k GET; with 16-command pipelining Redis 687k SET and 868k GET, Valkey 742k SET and 770k GET" width="700" loading="lazy" decoding="async"></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Test (2 cores, 1 I/O thread)</th>
<th>Redis 8.10.3, median (range of 5 runs)</th>
<th>Valkey 9.1.3, median (range of 5 runs)</th>
</tr>
</thead>
<tbody>
<tr>
<td>SET, one command at a time</td>
<td>116,469 /s (109k-134k)</td>
<td>115,969 /s (57k-134k)</td>
</tr>
<tr>
<td>GET, one command at a time</td>
<td>110,951 /s (110k-116k)</td>
<td>114,090 /s (106k-115k)</td>
</tr>
<tr>
<td>SET, 16 commands per round trip</td>
<td>687,285 /s (641k-884k)</td>
<td>742,391 /s (637k-760k)</td>
</tr>
<tr>
<td>GET, 16 commands per round trip</td>
<td>868,056 /s (722k-908k)</td>
<td>770,416 /s (645k-840k)</td>
</tr>
</tbody>
</table></div>
<p><strong>What we found.</strong></p>
<ol>
<li><strong>On a small server it is a tie.</strong> The differences are 13% at most, and they flip direction from test
to test. The ranges above overlap in every test: Redis’s own five SET runs with pipelining
spread from 641k to 884k, far wider than the gap between the two engines.</li>
<li><strong>Pipelining beats the engine choice by a mile.</strong> Sending 16 commands per round trip gave 6 to 8
times the throughput on both. If your app is slow on Redis, batch your commands before you change
engines.</li>
<li><strong>I/O threads need spare cores.</strong> With 2 I/O threads on the same 2 cores, both engines got slower
without pipelining. GET fell from 110,951 to 91,819 per second on Redis and from 114,090 to 90,818
on Valkey, and the five-run ranges did not overlap (88k-94k against 106k-116k). Valkey’s threading gains appear
when the server has cores to spare, which a small instance does not.</li>
<li><strong>Memory use is the same.</strong> In a separate run that wrote 200,000 random keys with 256-byte values
(about 126,000 distinct keys after repeats), Redis used 48.1 MB and Valkey 48.7 MB.</li>
</ol>
<p>This is a small-machine test on purpose. On a large instance with many cores and many clients, the
threading work in Valkey 8 and later can open a real gap; if that is your scale, test on your own
hardware.</p>
<h2 id="where-the-two-are-no-longer-interchangeable" tabindex="-1"><a class="header-anchor" href="#where-the-two-are-no-longer-interchangeable">Where the two are no longer interchangeable</a></h2>
<p>For most apps the switch is invisible. These are the places it is not:</p>
<ul>
<li><strong>Data files from Redis 7.4 and later.</strong> Valkey reads RDB and AOF files from Redis up to 7.2. A
snapshot written by Redis 7.4 or 8 cannot be loaded into Valkey directly. Move the data by
replication or by copying keys instead.</li>
<li><strong>Redis 8’s built-in types.</strong> Vector sets (<code>VADD</code>, <code>VSIM</code> and friends) exist only in Redis. JSON,
Bloom filters and search exist in Valkey as modules, so check that your host loads them before you
rely on them.</li>
<li><strong>Redis Ltd’s own products.</strong> Redis Cloud features such as Redis Flex and Active-Active are Redis
only.</li>
<li><strong>Client libraries.</strong> Every major Redis client works with Valkey. Valkey also publishes its own
client, GLIDE, but you do not need it.</li>
</ul>
<h2 id="which-licence-affects-you" tabindex="-1"><a class="header-anchor" href="#which-licence-affects-you">Which licence affects you</a></h2>
<p><strong>If you use the database inside your own app:</strong> neither licence costs you anything or asks anything
of you. Running Redis 8 or Valkey behind your SaaS, your API or your mobile app is free under every
option.</p>
<p><strong>If you change the database’s own source code:</strong> under AGPLv3, if users interact with your modified
version over a network, you must offer them your changes. Valkey’s BSD licence has no such condition.
Very few teams modify the database itself.</p>
<p><strong>If you sell the database as a hosted service:</strong> this is what RSALv2 and SSPLv1 were written to
restrict. It is why AWS, Google and many smaller hosts moved to Valkey. The details are on the
<a href="https://redis.io/legal/licenses/">official Redis licence page</a>.</p>
<h2 id="cost-where-the-engine-changes-the-bill" tabindex="-1"><a class="header-anchor" href="#cost-where-the-engine-changes-the-bill">Cost: where the engine changes the bill</a></h2>
<p>The engine affects price mainly on the big clouds. On AWS ElastiCache, Valkey is priced 20% lower
than Redis OSS on node-based clusters and 33% lower on serverless, and the serverless minimum is
100 MB for Valkey against 1 GB for Redis OSS (<a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache pricing</a>).
For a small cache that minimum is often the biggest number on the bill.</p>
<p>On smaller hosts you usually pay for memory and do not choose the engine at all. Our
<a href="/blog/free-redis-hosting/">free Redis hosting comparison</a> lists what each free tier limits and where
it runs.</p>
<h2 id="which-one-to-pick" tabindex="-1"><a class="header-anchor" href="#which-one-to-pick">Which one to pick</a></h2>
<ul>
<li><strong>A cache, sessions, rate limits or a job queue:</strong> either. Pick the one your host offers in the region
closest to your users; distance matters far more than the engine.</li>
<li><strong>You want a permissive licence and a vendor-neutral project:</strong> Valkey.</li>
<li><strong>You want vector search or JSON without loading modules:</strong> Redis 8.</li>
<li><strong>You are on AWS or Google Cloud:</strong> Valkey, for the lower price and because that is where the managed
service is going.</li>
<li><strong>You want Redis Ltd’s support or Redis Cloud features:</strong> Redis.</li>
<li><strong>You use BullMQ, Sidekiq or Celery:</strong> either should work, since they rely on core commands and Lua
scripts, which both support; run your test suite before you switch. Run the
consumer as a <a href="/background-workers/">background worker</a> in the same region as the store.</li>
</ul>
<h2 id="switching-from-redis-to-valkey" tabindex="-1"><a class="header-anchor" href="#switching-from-redis-to-valkey">Switching from Redis to Valkey</a></h2>
<ol>
<li><strong>Check the source version.</strong> From Redis 7.2 or earlier, a snapshot loads straight into Valkey.
From 7.4 or 8, plan to replicate or copy keys.</li>
<li><strong>Check for Redis-only commands.</strong> Search your code for vector set commands and module commands
(<code>JSON.</code>, <code>FT.</code>, <code>BF.</code>) and confirm the target has them.</li>
<li><strong>Change the URL, not the client.</strong> Point your existing client at the new address. Run your test
suite.</li>
<li><strong>Watch memory and eviction for a day.</strong> Make sure the new instance has the same <code>maxmemory</code> and
eviction policy as the old one.</li>
</ol>
<p>On Voroa, a key value store is Redis-compatible: your existing client connects with the URL you copy
from the database page, and nothing else in your code changes. Moving existing data in is covered in
<a href="/docs/import-existing-database/">importing an existing database</a>, and connection details are in the
<a href="/docs/key-value/">Key Value guide</a>.</p>
<h2 id="run-a-redis-compatible-store-next-to-your-app" tabindex="-1"><a class="header-anchor" href="#run-a-redis-compatible-store-next-to-your-app">Run a Redis-compatible store next to your app</a></h2>
<p>Whichever engine you prefer, what decides speed for an app in India is distance. Voroa runs a
Redis-compatible key value store in Mumbai beside the services you deploy, billed in rupees with a GST
invoice, with a free size to start. See the sizes on the <a href="/managed-redis-india/">managed Redis page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Is Valkey a drop-in replacement for Redis?</strong>
For Redis up to 7.2 and for everyday commands, yes. Clients, commands and the protocol are the same.
Data files from Redis 7.4 and later, and Redis 8’s built-in vector sets, are the exceptions.</p>
<p><strong>Is Valkey faster than Redis?</strong>
On our 2-core test, no: the two were within 13% of each other and the winner changed from test to test. Valkey’s
published gains come from I/O threads on large many-core machines.</p>
<p><strong>Is Redis still open source?</strong>
Redis 8 and later can be used under AGPLv3, which is an open source licence. Redis 7.4 was
source-available only. Redis 7.2 and earlier are BSD.</p>
<p><strong>Why did AWS and Google move to Valkey?</strong>
Redis’s 2024 licences restrict offering Redis as a managed service. Valkey’s BSD licence does not, so
AWS, Google and many smaller hosts now run Valkey. Azure’s managed offering is Redis.</p>
<p><strong>Will my BullMQ, Sidekiq or Celery setup work on Valkey?</strong>
They should. They rely on core commands and Lua scripts, which Valkey supports. We have not tested
each library on Valkey, so run your own test suite before you switch.</p>
<p><strong>Should I use Valkey or Redis for a new project in India?</strong>
Choose by host and region first. A store in Mumbai next to your app will be faster than either engine
in Singapore. Distance matters more than the engine.</p>
<p><em>Last updated: 7 November 2026. We re-check versions and re-run the benchmark when a major release
ships. Found something out of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Vercel Cron Jobs: Limits, Setup and the Traps in 2026</title>
      <link>https://getvoroa.com/blog/vercel-cron-jobs/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/vercel-cron-jobs/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>Next.js</category>
      <description><![CDATA[How Vercel cron jobs work, the Hobby once-a-day limit, UTC-only schedules, no retries and duplicate runs, plus securing them with CRON_SECRET and alternatives.]]></description>
      <content:encoded><![CDATA[<p>Vercel cron jobs call one of your functions on a schedule. You add a path and a cron expression to
<code>vercel.json</code>, deploy, and Vercel sends a request to that path at the times you set.</p>
<p>It is simple, and it has rules that surprise people: a once-a-day limit on the free plan, UTC-only
times, no retries, and occasional duplicate runs. This guide covers the setup and each rule, all from
Vercel’s own documentation, checked on 7 November 2026.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#how-they-work">How they work</a></li>
<li><a href="#setup-in-two-files">Setup in two files</a></li>
<li><a href="#plan-limits">Plan limits</a></li>
<li><a href="#seven-rules-that-catch-people-out">Seven rules that catch people out</a></li>
<li><a href="#securing-the-endpoint-with-cron_secret">Securing the endpoint with CRON_SECRET</a></li>
<li><a href="#schedules-the-replacement">Schedules: the replacement</a></li>
<li><a href="#when-to-run-cron-somewhere-else">When to run cron somewhere else</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="how-they-work" tabindex="-1"><a class="header-anchor" href="#how-they-work">How they work</a></h2>
<p>At each scheduled time, Vercel makes an HTTP GET request to your <strong>production</strong> deployment, at the path
you configured. The request carries the user agent <code>vercel-cron/1.0</code> and an <code>x-vercel-cron-schedule</code>
header with the expression that triggered it.</p>
<p>Your function runs as a normal request. So the job is limited by your function’s maximum duration, and
it only exists while that request is being handled.</p>
<h2 id="setup-in-two-files" tabindex="-1"><a class="header-anchor" href="#setup-in-two-files">Setup in two files</a></h2>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">json</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-json">// vercel.json
{
  &quot;crons&quot;: [
    { &quot;path&quot;: &quot;/api/digest&quot;, &quot;schedule&quot;: &quot;30 3 * * *&quot; }
  ]
}
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">// app/api/digest/route.js  (Next.js App Router)
export async function GET(request) {
  if (request.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response('Unauthorized', { status: 401 });
  }
  await sendDailyDigest();           // your code
  return Response.json({ ok: true });
}
</code></pre>
</div>
<p><code>30 3 * * *</code> is 3:30 UTC, which is <strong>9:00 am in India</strong>. Vercel schedules in UTC only.</p>
<h2 id="plan-limits" tabindex="-1"><a class="header-anchor" href="#plan-limits">Plan limits</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Hobby</th>
<th>Pro</th>
<th>Enterprise</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cron jobs per project</td>
<td>100</td>
<td>100</td>
<td>100</td>
</tr>
<tr>
<td>Shortest interval</td>
<td>Once a day</td>
<td>Once a minute</td>
<td>Once a minute</td>
</tr>
<tr>
<td>Timing</td>
<td>Anywhere within the scheduled hour</td>
<td>Within the scheduled minute</td>
<td>Within the scheduled minute</td>
</tr>
</tbody>
</table></div>
<p>On Hobby, an expression that runs more than once a day, such as <code>0 * * * *</code>, fails the deployment. And a
job set for <code>0 8 * * *</code> can start any time from 08:00 to 08:59 UTC. Source:
<a href="https://vercel.com/docs/schedules/cron-jobs/usage-and-pricing">Vercel cron usage and pricing</a>.</p>
<p><img src="https://media.getvoroa.com/img/b0781df2d5e63ba9.webp" alt="Diagram: a job scheduled for 8:00 UTC on Vercel Hobby can start anywhere between 8:00 and 8:59, while on Pro it starts within 8:00 to 8:00:59; 8:00 UTC is 1:30 pm in India" width="700" loading="eager" decoding="async"></p>
<h2 id="seven-rules-that-catch-people-out" tabindex="-1"><a class="header-anchor" href="#seven-rules-that-catch-people-out">Seven rules that catch people out</a></h2>
<p><strong>1. Failed runs are not retried.</strong> If the function errors, Vercel does not try again. You find out
only from the logs.</p>
<p><strong>2. Delivery is best effort.</strong> Vercel says a network error can occasionally stop a run from reaching
your function, and then nothing runs and nothing is logged.</p>
<p><strong>3. A run can arrive twice.</strong> Vercel also says the same scheduled run can occasionally be delivered more
than once. Make the job safe to repeat.</p>
<p><strong>4. Runs can overlap.</strong> If a run takes longer than the interval, Vercel can start the next one anyway.
Vercel suggests a lock, for example in Redis, to allow only one at a time.</p>
<p><strong>5. Only production runs crons.</strong> Preview deployments never trigger them. Test by calling the path
yourself.</p>
<p><strong>6. Redirects end the run.</strong> Cron requests do not follow redirects. With Next.js <code>trailingSlash</code>
turned on, <code>/api/digest</code> redirects to <code>/api/digest/</code> and your function never runs. Put the trailing
slash in the configured path.</p>
<p><strong>7. Expression limits.</strong> No names such as <code>MON</code> or <code>JAN</code>, and you cannot set both day-of-month and
day-of-week; one of them must be <code>*</code>.</p>
<p>These come from Vercel’s
<a href="https://vercel.com/docs/schedules/cron-jobs/manage-cron-jobs">managing cron jobs</a> page. Rules 3 and 4 are the
same lessons we measured for in-process schedulers: our
<a href="/blog/cron-job-in-node-js/">node-cron tests</a> saw both duplicate and overlapping runs.</p>
<h2 id="securing-the-endpoint-with-cron_secret" tabindex="-1"><a class="header-anchor" href="#securing-the-endpoint-with-cron_secret">Securing the endpoint with CRON_SECRET</a></h2>
<p>A cron path is a public URL, so anyone could call it. Vercel’s fix:</p>
<ol>
<li>Add an environment variable <code>CRON_SECRET</code>, a random string of at least 16 characters.</li>
<li>Vercel sends it in the <code>Authorization</code> header as <code>Bearer &lt;secret&gt;</code> on every cron call.</li>
<li>Your function rejects any request without the matching header, as in the code above.</li>
</ol>
<p>Each deployment keeps the secret it was built with. Changing <code>CRON_SECRET</code> does nothing until you
redeploy, and an instant rollback brings back the older deployment’s secret.</p>
<h2 id="schedules-the-replacement" tabindex="-1"><a class="header-anchor" href="#schedules-the-replacement">Schedules: the replacement</a></h2>
<p>Vercel now describes Cron Jobs as a legacy feature that will move to <strong>Schedules</strong>, which add timezones
and private function targets. Schedules were in public beta at the time of writing.</p>
<p>The limits change too. According to Vercel’s migration guide, Hobby schedules can run as often as once
every 15 minutes, within a 15-minute window, instead of once a day.</p>
<p>If timezones are the reason you are reading this, check the Schedules documentation before building
around UTC. Existing cron routes can move to Schedules without code changes, according to Vercel.</p>
<h2 id="when-to-run-cron-somewhere-else" tabindex="-1"><a class="header-anchor" href="#when-to-run-cron-somewhere-else">When to run cron somewhere else</a></h2>
<p>Vercel cron is a good fit for short tasks that are safe to retry by hand: refreshing a cache, sending a
digest, syncing a small list. Consider a separate cron service when:</p>
<ul>
<li><strong>The job runs longer</strong> than your function’s maximum duration.</li>
<li><strong>You are on Hobby with legacy Cron Jobs</strong> and need more than one run a day.</li>
<li><strong>Your users are in India</strong> and you want schedules in IST without converting from UTC (legacy Cron Jobs are UTC-only).</li>
</ul>
<p>For the general limits of each platform side by side, see our
<a href="/blog/cron-every-5-minutes/">guide to cron every 5 minutes</a>.</p>
<h2 id="running-scheduled-jobs-on-voroa" tabindex="-1"><a class="header-anchor" href="#running-scheduled-jobs-on-voroa">Running scheduled jobs on Voroa</a></h2>
<p>On Voroa, a <strong>Cron Job</strong> runs a command from your repository on a schedule, as a process rather than an
HTTP request to a function. Schedules are in India Standard Time and can run as often as every minute.</p>
<p>Voroa builds the repository into a small service that stays running, so a slow run can overlap the next.
Runs are skipped while the service is stopped, for example after a free workspace uses its monthly free
hours. Voroa does not retry a failed run either, so keep jobs idempotent and record each success.</p>
<p>Setup is in <a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a>, and
you can host the Next.js app itself as a <a href="/web-services/">web service</a>. Long-running consumers belong on
the <a href="/background-workers/">background workers page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Are Vercel cron jobs free?</strong>
They are included on every plan; you pay for the function time they use. On Hobby they can run at most
once a day.</p>
<p><strong>What timezone do Vercel cron jobs use?</strong>
UTC, always. For 9 am in India, write <code>30 3 * * *</code>.</p>
<p><strong>Why is my Vercel cron job not running?</strong>
Common causes: it is a preview deployment, a redirect such as <code>trailingSlash</code>, a 401 from a
<code>CRON_SECRET</code> mismatch, or a Hobby expression that runs more than once a day.</p>
<p><strong>Does Vercel retry failed cron jobs?</strong>
No. A failed invocation is not retried. Check the logs from the Cron Jobs settings page.</p>
<p><strong>Can a Vercel cron job run twice?</strong>
Yes, occasionally. Vercel says delivery can repeat a run, and a long run can overlap the next.</p>
<p><strong>How do I secure a Vercel cron job?</strong>
Set <code>CRON_SECRET</code>, then reject requests whose <code>Authorization</code> header is not <code>Bearer</code> plus that secret.</p>
<p><em>Checked 7 November 2026 against Vercel’s documentation. Found something out of date? Write to
<a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>What Is a Cron Job? Syntax, Examples and Where to Run It</title>
      <link>https://getvoroa.com/blog/what-is-a-cron-job/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/what-is-a-cron-job/</guid>
      <pubDate>Sat, 07 Nov 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[What is a cron job: how cron works, how to read the five-field syntax, crontab commands, real examples, and the timezone and overlap mistakes to avoid.]]></description>
      <content:encoded><![CDATA[<p>What is a cron job? It is a command that a computer runs automatically on a schedule: every night at
2 am, every 15 minutes, every Monday morning. The name comes from <code>cron</code>, the Unix program that has done
this since the 1970s.</p>
<p>This guide explains how cron works, how to read and write a schedule, and the few mistakes that cause
most cron problems. It ends with where to run cron jobs when you do not manage your own server.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#how-cron-works">How cron works</a></li>
<li><a href="#the-five-fields">The five fields</a></li>
<li><a href="#examples-you-can-copy">Examples you can copy</a></li>
<li><a href="#working-with-crontab">Working with crontab</a></li>
<li><a href="#five-mistakes-that-break-cron-jobs">Five mistakes that break cron jobs</a></li>
<li><a href="#cron-job-or-background-worker">Cron job or background worker?</a></li>
<li><a href="#where-to-run-cron-jobs-today">Where to run cron jobs today</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="how-cron-works" tabindex="-1"><a class="header-anchor" href="#how-cron-works">How cron works</a></h2>
<p>On a Linux server, a small program called the cron daemon wakes up every minute. It reads a list of
jobs, checks which ones are due at that minute, and runs them.</p>
<p>Each job is one line: a schedule followed by a command. The list is called a <strong>crontab</strong> (“cron table”).
Each user on the server has their own.</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code># minute hour day-of-month month day-of-week  command
0 2 * * * /home/app/backup.sh
</code></pre>
</div>
<p>That line runs <code>backup.sh</code> at 2:00 am every day.</p>
<h2 id="the-five-fields" tabindex="-1"><a class="header-anchor" href="#the-five-fields">The five fields</a></h2>
<p><img src="https://media.getvoroa.com/img/d80084b3596725b2.webp" alt="Diagram: the five fields of a cron expression, minute, hour, day of month, month and day of week, with allowed values and the example 30 9 * * 1-5 meaning 9:30 am Monday to Friday" width="700" loading="eager" decoding="async"></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Field</th>
<th>Allowed values</th>
<th>Special characters</th>
</tr>
</thead>
<tbody>
<tr>
<td>Minute</td>
<td>0-59</td>
<td><code>*</code> any, <code>,</code> list, <code>-</code> range, <code>/</code> step</td>
</tr>
<tr>
<td>Hour</td>
<td>0-23</td>
<td>same</td>
</tr>
<tr>
<td>Day of month</td>
<td>1-31</td>
<td>same</td>
</tr>
<tr>
<td>Month</td>
<td>1-12 or JAN-DEC</td>
<td>same</td>
</tr>
<tr>
<td>Day of week</td>
<td>0-7 or SUN-SAT (0 and 7 are Sunday)</td>
<td>same</td>
</tr>
</tbody>
</table></div>
<p>Read it left to right. <code>30 9 * * 1-5</code> means minute 30, hour 9, any day of the month, any month, Monday
to Friday: 9:30 am on weekdays.</p>
<p>One quirk: if you set both day-of-month and day-of-week, standard cron runs when <strong>either</strong> matches.
<code>0 9 1 * 1</code> runs on the 1st of every month <strong>and</strong> every Monday.</p>
<h2 id="examples-you-can-copy" tabindex="-1"><a class="header-anchor" href="#examples-you-can-copy">Examples you can copy</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What you want</th>
<th>Expression</th>
</tr>
</thead>
<tbody>
<tr>
<td>Every minute</td>
<td><code>* * * * *</code></td>
</tr>
<tr>
<td>Every 5 minutes</td>
<td><code>*/5 * * * *</code></td>
</tr>
<tr>
<td>Every hour, on the hour</td>
<td><code>0 * * * *</code></td>
</tr>
<tr>
<td>Every day at midnight</td>
<td><code>0 0 * * *</code></td>
</tr>
<tr>
<td>Every day at 2:30 am</td>
<td><code>30 2 * * *</code></td>
</tr>
<tr>
<td>Weekdays at 9 am</td>
<td><code>0 9 * * 1-5</code></td>
</tr>
<tr>
<td>Every Sunday at 6 pm</td>
<td><code>0 18 * * 0</code></td>
</tr>
<tr>
<td>First day of every month</td>
<td><code>0 0 1 * *</code></td>
</tr>
<tr>
<td>Every 15 minutes, 9 am to 5 pm on weekdays</td>
<td><code>*/15 9-16 * * 1-5</code></td>
</tr>
</tbody>
</table></div>
<p>Some step values do not divide an hour evenly and leave short gaps. Our
<a href="/blog/cron-every-5-minutes/">guide to cron every 5 minutes</a> shows which ones and why.</p>
<h2 id="working-with-crontab" tabindex="-1"><a class="header-anchor" href="#working-with-crontab">Working with crontab</a></h2>
<p>On a Linux server:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">crontab -e      # edit your crontab (opens an editor)
crontab -l      # list your jobs
crontab -r      # remove ALL your jobs - careful
</code></pre>
</div>
<p>A safe line looks like this:</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>*/15 * * * * cd /home/app &amp;&amp; /usr/bin/node scripts/sync.js &gt;&gt; /var/log/sync.log 2&gt;&amp;1
</code></pre>
</div>
<p>It uses full paths, changes to the right folder, and sends both output and errors to a log file so you
can see what happened.</p>
<h2 id="five-mistakes-that-break-cron-jobs" tabindex="-1"><a class="header-anchor" href="#five-mistakes-that-break-cron-jobs">Five mistakes that break cron jobs</a></h2>
<p><strong>1. The wrong timezone.</strong> Cron uses the server’s timezone, which is usually UTC on cloud servers.
<code>0 9 * * *</code> then runs at 2:30 pm in India. To run at 9 am IST on a UTC server, write <code>30 3 * * *</code>.</p>
<p><strong>2. A missing PATH.</strong> Cron runs with a very short PATH, so <code>node</code> or <code>python</code> may not be found. Use
full paths such as <code>/usr/bin/node</code>.</p>
<p><strong>3. No output anywhere.</strong> By default cron’s output is mailed or lost. Redirect it to a log file, as
above, or nothing tells you the job failed.</p>
<p><strong>4. Overlapping runs.</strong> Cron starts each run on time even if the last one is still going. On Linux,
<code>flock -n</code> skips a run while the previous one holds a lock.</p>
<p><strong>5. Duplicate runs.</strong> If the same crontab, or the same in-app scheduler, runs on two servers, every job
runs twice. We measured exactly that with
<a href="/blog/cron-job-in-node-js/">node-cron in Node.js</a>: two instances, 40 runs instead of 20.</p>
<h2 id="cron-job-or-background-worker" tabindex="-1"><a class="header-anchor" href="#cron-job-or-background-worker">Cron job or background worker?</a></h2>
<p>Both run code with no web page. The difference is whether the program is meant to stop.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Your program</th>
<th>Use</th>
</tr>
</thead>
<tbody>
<tr>
<td>Runs a report, sends a digest or cleans up, then exits</td>
<td>Cron job</td>
</tr>
<tr>
<td>Waits for jobs on a queue and processes them as they arrive</td>
<td>Background worker</td>
</tr>
<tr>
<td>Holds a websocket or a long-lived connection</td>
<td>Background worker</td>
</tr>
</tbody>
</table></div>
<p>If your script ends on its own, it is a cron job. If it should still be running tomorrow, it is a
worker.</p>
<h2 id="where-to-run-cron-jobs-today" tabindex="-1"><a class="header-anchor" href="#where-to-run-cron-jobs-today">Where to run cron jobs today</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Option</th>
<th>Good for</th>
<th>Watch out for</th>
</tr>
</thead>
<tbody>
<tr>
<td>Your own Linux server</td>
<td>Full control, any schedule</td>
<td>You maintain the server; timezone is the server’s</td>
</tr>
<tr>
<td>A scheduler inside your app (node-cron, APScheduler, Celery beat)</td>
<td>Jobs tied to app code</td>
<td>Runs twice if the app runs twice</td>
</tr>
<tr>
<td>A platform’s cron feature</td>
<td>No server to manage</td>
<td>Minimum intervals, UTC by default, best-effort timing</td>
</tr>
</tbody>
</table></div>
<p>Hosting platforms differ a lot here. GitHub Actions runs schedules at most every 5 minutes, Heroku
Scheduler every 10 minutes, and most use UTC. The
<a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows">GitHub Actions schedule docs</a>
are a typical example.</p>
<h2 id="cron-jobs-on-voroa" tabindex="-1"><a class="header-anchor" href="#cron-jobs-on-voroa">Cron jobs on Voroa</a></h2>
<p>On Voroa you can create a <strong>Cron Job</strong> from a GitHub repository: give it a command and a schedule, from
a preset such as Every hour or a cron expression. Schedules run in India Standard Time, so <code>0 9 * * *</code>
means 9:00 am IST.</p>
<p>Voroa builds the repository into a small service that stays running and runs your command inside it.
A slow run can overlap the next, so keep jobs safe to run twice. Runs are also skipped while the service
is stopped, for example after a free workspace uses its monthly free hours, so do not rely on every run
firing.</p>
<p>Details are in <a href="/docs/scheduled-tasks-cron/">scheduling a task on Voroa</a>, and
<a href="/docs/worker-or-cron-job/">worker or cron job</a> helps you pick. Long-running processes go on the
<a href="/background-workers/">background workers page</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>What is a cron job in simple words?</strong>
A command that runs automatically on a schedule, such as every night or every 15 minutes.</p>
<p><strong>What does <code>* * * * *</code> mean?</strong>
Every minute of every hour of every day. Each star means “any value” for that field.</p>
<p><strong>What is the difference between cron and crontab?</strong>
Cron is the program that runs scheduled jobs. A crontab is the file listing those jobs.</p>
<p><strong>What timezone does a cron job use?</strong>
The server’s timezone, usually UTC in the cloud. Check it before you pick an hour.</p>
<p><strong>Can a cron job run every second?</strong>
Not with standard cron, whose smallest unit is one minute. Some in-app schedulers accept a seconds
field.</p>
<p><strong>How do I see if my cron job ran?</strong>
Send its output to a log file, or have the job record its result somewhere you can check, such as a row
in a database.</p>
<p><em>Updated 7 November 2026. Found something out of date? Write to <a href="mailto:support@getvoroa.com">support@getvoroa.com</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Free Postgres Database: Every Generous Free Tier in 2026</title>
      <link>https://getvoroa.com/blog/free-postgres-database/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/free-postgres-database/</guid>
      <pubDate>Fri, 16 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Free tier</category>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Yes - Neon, Supabase, Prisma, CockroachDB and others give you a free Postgres database. 14 free tiers compared on storage, pauses, expiry and India latency.]]></description>
      <content:encoded><![CDATA[<p>Every free Postgres database has a catch. Most comparison pages list the storage number and stop
there. The catch is somewhere else: a database that is paused after a quiet week, one that is
deleted on day 44, one whose compute runs out on day 17, or one that sits in Singapore and adds a
fifth of a second to every page your Indian users load.</p>
<p>This page compares free PostgreSQL hosting the way you would actually use it: as free database
hosting for an app that has to keep working. We run a Postgres fleet in Mumbai, so we did not only
read pricing pages. We measured.
Every latency number below was taken by us on 16 October 2026, from a home broadband connection in
India and from a server in Mumbai. Every limit was re-checked against the provider’s own pricing page
or documentation on 14 November 2026.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#which-postgres-providers-have-generous-free-tiers">Which Postgres providers have generous free tiers?</a></li>
<li><a href="#best-free-postgres-database-for-each-use-case">Best free Postgres database for each use case</a></li>
<li><a href="#free-postgresql-hosting-compared-14-options">Free PostgreSQL hosting compared: 14 options</a></li>
<li><a href="#each-free-tier-in-a-few-lines">Each free tier in a few lines</a></li>
<li><a href="#free-postgres-latency-from-india-measured">Free Postgres latency from India, measured</a></li>
<li><a href="#the-four-ways-a-free-postgres-stops-working">The four ways a free Postgres stops working</a></li>
<li><a href="#neon-free-tier-the-day-17-problem">Neon free tier: the day-17 problem</a></li>
<li><a href="#when-to-leave-a-free-tier-and-what-the-paid-floor-costs">When to leave a free tier, and what the paid floor costs</a></li>
<li><a href="#how-to-create-a-free-postgres-database-online-step-by-step">How to create a free Postgres database online, step by step</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="which-postgres-providers-have-generous-free-tiers" tabindex="-1"><a class="header-anchor" href="#which-postgres-providers-have-generous-free-tiers">Which Postgres providers have generous free tiers?</a></h2>
<p>Yes, several do. Judged on three things - how much storage you get, whether the database expires,
and whether it stops when idle - the most generous free Postgres tiers in November 2026 are
CockroachDB Basic, Layerbase, Netlify Database, Prisma Postgres and Neon. None of them expires, but
each one sleeps, meters usage or caps something else.</p>
<ol>
<li><strong>CockroachDB Basic:</strong> 10 GiB and 50 million request units a month. Postgres-compatible rather than
Postgres, and the allowance is for accounts paying by card.</li>
<li><strong>Layerbase:</strong> 5 GB across up to 2 databases, no card. Databases sleep and wake on connect.</li>
<li><strong>Netlify Database:</strong> 5 GB per database, up to 3. Its compute is paid from the same 300 monthly
credits as your Netlify site.</li>
<li><strong>Prisma Postgres:</strong> 2 GB shared by up to 50 databases, with 200,000 queries a month.</li>
<li><strong>Neon:</strong> 1 GB per project across up to 100 projects (20 GB in total), with 100 compute hours per
project.</li>
</ol>
<p>If your users are in India, add a fourth test: a region near them. Of these five, only CockroachDB
Basic offers Mumbai.</p>
<h2 id="best-free-postgres-database-for-each-use-case" tabindex="-1"><a class="header-anchor" href="#best-free-postgres-database-for-each-use-case">Best free Postgres database for each use case</a></h2>
<ul>
<li><strong>Learning SQL, or a project you open a few times a week:</strong> Neon. 1 GB per project, a real Postgres,
and scale-to-zero means you only spend compute while you are working.</li>
<li><strong>An app with logins, file uploads and a database in one place:</strong> Supabase, in its Mumbai region.
Keep it busy every week or it is paused.</li>
<li><strong>A database that has to stay awake 24 hours a day for free:</strong> one that does not meter compute
hours, such as Nile or Aiven. Neon’s allowance runs out around day 17 if the database never sleeps
(the maths is below).</li>
<li><strong>Many small databases, one per experiment:</strong> Prisma Postgres, up to 50 databases sharing 2 GB.</li>
<li><strong>A hackathon or a demo next week:</strong> anything on this list. Write down the expiry date if you pick
Render.</li>
<li><strong>Your users are in India:</strong> pick a Mumbai region. From our tests, Singapore adds about 40 ms to
every query from a home connection and about 55 ms from a Mumbai server, compared with Mumbai.</li>
</ul>
<h2 id="free-postgresql-hosting-compared-14-options" tabindex="-1"><a class="header-anchor" href="#free-postgresql-hosting-compared-14-options">Free PostgreSQL hosting compared: 14 options</a></h2>
<p>Every free Postgres hosting option we could verify, with limits as published by each provider on 14
November 2026. Sources: <a href="https://neon.com/docs/introduction/plans">Neon plans</a>, Supabase pricing and
compute docs, Aiven free tier docs, Render free tier and pricing, Railway plans, Koyeb databases docs,
Prisma Postgres free plan, Nile pricing, CockroachDB Basic, Netlify Database limits, Layerbase, the AWS
free tier page, Microsoft’s Azure free account guide and <a href="/docs/free-instances/">Voroa free instances</a>.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Provider</th>
<th>Free storage</th>
<th>Compute</th>
<th>Sleeps or pauses?</th>
<th>Deleted after?</th>
<th>Backups on free</th>
<th>Connections</th>
<th>Nearest region to India</th>
</tr>
</thead>
<tbody>
<tr>
<td>Neon</td>
<td>1 GB per project, 100 projects, 20 GB total</td>
<td>100 CU-hours per project per month</td>
<td>Scales to zero after 5 min idle</td>
<td>No</td>
<td>6-hour restore window</td>
<td>Not stated on plans page</td>
<td>Singapore</td>
</tr>
<tr>
<td>Supabase</td>
<td>500 MB database, 2 active projects</td>
<td>Shared CPU, 0.5 GB RAM</td>
<td>Paused after 1 week of inactivity</td>
<td>No (1-year restore window)</td>
<td>None</td>
<td>60 direct, 200 pooled</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Aiven</td>
<td>1 GB</td>
<td>1 CPU, 1 GB RAM</td>
<td>Can be powered off when unused</td>
<td>No</td>
<td>Included, frequency not stated</td>
<td>20, no pooling</td>
<td>Not selectable on free</td>
</tr>
<tr>
<td>Render</td>
<td>1 GB</td>
<td>256 MB RAM</td>
<td>No, but may be restarted at any time</td>
<td>Yes, 30 days + 14-day grace</td>
<td>None</td>
<td>100</td>
<td>Singapore</td>
</tr>
<tr>
<td>Railway</td>
<td>0.5 GB volume</td>
<td>$5 trial credit, then $1 a month; 0.5 GB RAM, 1 vCPU on Free</td>
<td>Stops when credit runs out</td>
<td>Volume data deleted 30 days after expiry</td>
<td>Not stated</td>
<td>Not stated</td>
<td>Singapore</td>
</tr>
<tr>
<td>Koyeb</td>
<td>1 GB</td>
<td>a quarter of a vCPU, 1 GB RAM, 5 hours a month</td>
<td>Sleeps after 5 min idle</td>
<td>No</td>
<td>Not stated</td>
<td>About 112</td>
<td>Singapore</td>
</tr>
<tr>
<td>Prisma Postgres</td>
<td>2 GB per workspace, up to 50 databases</td>
<td>200,000 queries a month</td>
<td>Scales to zero; queries stop at the monthly limit</td>
<td>Only unclaimed instant databases, after 24 hours</td>
<td>None</td>
<td>10 pooled + 10 direct per database</td>
<td>Singapore</td>
</tr>
<tr>
<td>Nile</td>
<td>1 GB</td>
<td>50M query tokens</td>
<td>Never pauses</td>
<td>No</td>
<td>None</td>
<td>500</td>
<td>None in Asia (US, EU)</td>
</tr>
<tr>
<td>CockroachDB Basic</td>
<td>10 GiB</td>
<td>50M request units a month</td>
<td>Scales to zero</td>
<td>No</td>
<td>Not stated on Basic plan page</td>
<td>Not stated</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Netlify Database</td>
<td>5 GB per database, up to 3</td>
<td>1 compute unit, 48 units a month, paid from 300 credits</td>
<td>Sleeps after 5 min idle</td>
<td>No</td>
<td>Kept 3 days</td>
<td>Not stated</td>
<td>Not stated</td>
</tr>
<tr>
<td>Layerbase</td>
<td>5 GB, up to 2 databases</td>
<td>Not stated</td>
<td>Sleeps, wakes on connect</td>
<td>No</td>
<td>One manual backup slot</td>
<td>Not stated</td>
<td>Not stated</td>
</tr>
<tr>
<td>AWS RDS / Aurora</td>
<td>Credit-based</td>
<td>$100 credit (+$100 more), up to 6 months</td>
<td>No</td>
<td>Account closes after 6 months or when credit runs out</td>
<td>Yes</td>
<td>Instance limit</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Azure Flexible Server</td>
<td>32 GB</td>
<td>B1MS, 750 hours a month</td>
<td>No</td>
<td>Free for 12 months only</td>
<td>32 GB backup storage</td>
<td>Instance limit</td>
<td>Central India (Pune)</td>
</tr>
<tr>
<td>Voroa</td>
<td>1 GB</td>
<td>0.1 CPU, 256 MB RAM</td>
<td>Not for inactivity; stopped if the workspace’s 750 free hours run out</td>
<td>No</td>
<td>None</td>
<td>20</td>
<td>Mumbai</td>
</tr>
</tbody>
</table></div>
<p>Three notes on the table. CockroachDB speaks the Postgres wire protocol but is a different database
underneath, so some Postgres features and extensions will not work. AWS and Azure free periods end.
Netlify Database’s compute and bandwidth are taken from the same credits as the rest of a Netlify
team; our guide to <a href="/blog/netlify-free-plan-limits/">Netlify free plan limits</a> explains how that pool
runs out.</p>
<h2 id="each-free-tier-in-a-few-lines" tabindex="-1"><a class="header-anchor" href="#each-free-tier-in-a-few-lines">Each free tier in a few lines</a></h2>
<h3 id="neon" tabindex="-1"><a class="header-anchor" href="#neon">Neon</a></h3>
<p>A real Postgres with 1 GB per project and up to 100 projects, capped at 20 GB in total. Compute scales
to zero after 5 minutes and cannot be kept on. When a project uses its 100 CU-hours, compute is
suspended until next month, but Neon says none of its limits delete your data.</p>
<h3 id="supabase" tabindex="-1"><a class="header-anchor" href="#supabase">Supabase</a></h3>
<p>500 MB of database, shared CPU and 0.5 GB RAM, with two active projects. A project is paused after a
week of inactivity, and there are no automatic backups on Free. It is the only big free tier here
with a Mumbai region.</p>
<h3 id="aiven" tabindex="-1"><a class="header-anchor" href="#aiven">Aiven</a></h3>
<p>1 GB on a 1 CPU, 1 GB RAM machine, with no time limit and no card. The catch is 20 connections with no
pooling, and Aiven chooses the cloud and region for you. One free service of each type per
organisation.</p>
<h3 id="render" tabindex="-1"><a class="header-anchor" href="#render">Render</a></h3>
<p>1 GB and 256 MB RAM, one free database per workspace. It is deleted 30 days after creation unless you
upgrade within a 14-day grace period, and it has no backups.</p>
<h3 id="railway" tabindex="-1"><a class="header-anchor" href="#railway">Railway</a></h3>
<p>There is no free database as such: a one-time $5 trial credit, then $1 of credit a month on the Free
plan, with a 0.5 GB volume. When the credit runs out, workloads stop, and volume data is deleted 30
days after the plan expires.</p>
<h3 id="koyeb" tabindex="-1"><a class="header-anchor" href="#koyeb">Koyeb</a></h3>
<p>One free database instance with 1 GB, a quarter of a vCPU and 1 GB RAM, but only 5 hours of compute a
month. It sleeps after 5 minutes idle, which is what makes those 5 hours last.</p>
<h3 id="prisma-postgres" tabindex="-1"><a class="header-anchor" href="#prisma-postgres">Prisma Postgres</a></h3>
<p>The free plan grew this year: 2 GB per workspace shared by up to 50 databases, and 200,000 queries a
month, with no card. Idle databases scale to zero, and once the monthly queries are used, queries stop
until the 1st. There are no backups on Free.</p>
<h3 id="nile" tabindex="-1"><a class="header-anchor" href="#nile">Nile</a></h3>
<p>1 GB, 50 million query tokens and up to 500 connections, and the database never pauses. There are no
backups on Free, and the regions are in the US and Europe only.</p>
<h3 id="cockroachdb-basic" tabindex="-1"><a class="header-anchor" href="#cockroachdb-basic">CockroachDB Basic</a></h3>
<p>The biggest free storage allowance here: 10 GiB and 50 million request units a month, worth $15 of
usage. It scales to zero when idle and offers Mumbai. The allowance applies to organisations paying
monthly by card or marketplace, and it is CockroachDB, not Postgres.</p>
<h3 id="netlify-database" tabindex="-1"><a class="header-anchor" href="#netlify-database">Netlify Database</a></h3>
<p>Managed Postgres, generally available since 28 April 2026. Free gives up to 3 databases with 5 GB each,
sleeping after 5 minutes, with backups kept 3 days. Compute and bandwidth come out of the 300 credits a
Netlify team gets each month, so a busy database can pause the site too.</p>
<h3 id="layerbase" tabindex="-1"><a class="header-anchor" href="#layerbase">Layerbase</a></h3>
<p>5 GB across up to 2 databases, no card and no expiry. Small databases sleep and wake when you connect,
in about one to five seconds by Layerbase’s own figures. Its page does not state connection limits or a
region for free databases.</p>
<h3 id="voroa" tabindex="-1"><a class="header-anchor" href="#voroa">Voroa</a></h3>
<p>One free Postgres per account in Mumbai: 1 GB, 0.1 CPU, 256 MB RAM and 20 connections. It is not
paused for inactivity, but it shares the workspace’s 750 free hours a month, and there are no backups
on the free size. More detail is in the Voroa section below.</p>
<h2 id="free-postgres-latency-from-india-measured" tabindex="-1"><a class="header-anchor" href="#free-postgres-latency-from-india-measured">Free Postgres latency from India, measured</a></h2>
<p>We timed TCP connections to each provider’s public database endpoint, 20 attempts each, and report
the median. A TCP connect is one network round trip, which is the floor for every query you send.
For Neon we also created a free project in Singapore and timed real queries end to end with
<code>psycopg</code>, including new connections and a cold start after the database had scaled to zero.</p>
<p><strong>From a home broadband connection in India</strong></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Region</th>
<th>Median round trip</th>
</tr>
</thead>
<tbody>
<tr>
<td>Voroa, Mumbai</td>
<td>29 ms</td>
</tr>
<tr>
<td>Supabase, Mumbai</td>
<td>31 ms</td>
</tr>
<tr>
<td>AWS Mumbai (baseline)</td>
<td>32 ms</td>
</tr>
<tr>
<td>Neon, Singapore</td>
<td>70 ms</td>
</tr>
<tr>
<td>Supabase, Singapore</td>
<td>72 ms</td>
</tr>
<tr>
<td>Render, Singapore</td>
<td>74 ms</td>
</tr>
<tr>
<td>Neon, Frankfurt</td>
<td>162 ms</td>
</tr>
<tr>
<td>Render, Frankfurt</td>
<td>161 ms</td>
</tr>
<tr>
<td>Render, Oregon</td>
<td>238 ms</td>
</tr>
<tr>
<td>Neon, US East</td>
<td>265 ms</td>
</tr>
</tbody>
</table></div>
<p><strong>From a server in Mumbai</strong> (where your app runs, if you host it in India)</p>
<p><img src="https://media.getvoroa.com/img/a63fd3d1190a77e5.png" alt="Bar chart: median round trip from a server in Mumbai to each free Postgres region - Supabase Mumbai 3 ms, Render Singapore 56 ms, Supabase Singapore 59 ms, Neon Singapore 62 ms, Neon Frankfurt 133 ms, Neon US East 205 ms, Render Oregon 235 ms" width="700" loading="eager" decoding="async"></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Region</th>
<th>Median round trip</th>
</tr>
</thead>
<tbody>
<tr>
<td>Supabase, Mumbai</td>
<td>3 ms</td>
</tr>
<tr>
<td>Render, Singapore</td>
<td>56 ms</td>
</tr>
<tr>
<td>Supabase, Singapore</td>
<td>59 ms</td>
</tr>
<tr>
<td>Neon, Singapore</td>
<td>62 ms</td>
</tr>
<tr>
<td>Neon, Frankfurt</td>
<td>133 ms</td>
</tr>
<tr>
<td>Neon, US East</td>
<td>205 ms</td>
</tr>
<tr>
<td>Render, Oregon</td>
<td>235 ms</td>
</tr>
</tbody>
</table></div>
<p><strong>Real queries against a free Neon database in Singapore, from India</strong></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What</th>
<th>Median</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SELECT 1</code> on an open connection</td>
<td>63-69 ms</td>
</tr>
<tr>
<td><code>INSERT</code> and <code>COMMIT</code> on an open connection</td>
<td>190-205 ms</td>
</tr>
<tr>
<td>Opening a new connection and running one query</td>
<td>560-570 ms</td>
</tr>
<tr>
<td>First connection and query after 7 minutes idle (cold start)</td>
<td>1,236-1,644 ms (3 runs)</td>
</tr>
</tbody>
</table></div>
<h2 id="what-those-numbers-mean-for-your-app" tabindex="-1"><a class="header-anchor" href="#what-those-numbers-mean-for-your-app">What those numbers mean for your app</a></h2>
<p><strong>A cold start costs 0.7 to 1.1 seconds on top.</strong> After 7 minutes idle the free Neon database had
scaled to zero.</p>
<p>Across three runs the first connection then took 1.24 to 1.64 seconds against 0.56 seconds warm, so
the wake-up itself was roughly 0.7 to 1.1 seconds. The connection straight after it was back to normal. That is
fast for a database waking from zero, and it is still the first thing a visitor waits for after a quiet
spell.</p>
<p><strong>Latency multiplies by the number of queries.</strong> A typical API request does not send one query. It
checks the session, loads the user, loads the thing the page is about, and writes a log row. Call it
five queries, sent one after another.</p>
<p>From a server in Mumbai that is about 15 ms of waiting with the
database in Mumbai, and about 300 ms with the database in Singapore. Your code has not done anything
yet, and the user already feels it.</p>
<p><strong>New connections are the expensive part.</strong> Opening a connection to Postgres over TLS takes several
round trips before the first query runs: TCP, TLS, then password authentication. Against Singapore
from India we measured about 560 ms for that.</p>
<p>A long-running server opens its connections once and
reuses them, so it pays this rarely. A serverless function that opens a fresh connection on every
request pays it every time. If you deploy on serverless, use the provider’s pooled connection string
or a pooling driver.</p>
<p><strong>Put the app and the database in the same region.</strong> A database in Mumbai with an app in Oregon is
worse than both in Singapore. Check where your app runs before you choose where the database lives.</p>
<h2 id="the-four-ways-a-free-postgres-stops-working" tabindex="-1"><a class="header-anchor" href="#the-four-ways-a-free-postgres-stops-working">The four ways a free Postgres stops working</a></h2>
<p>Free tiers end in one of four ways. Knowing which one applies tells you more than the storage figure.</p>
<p><img src="https://media.getvoroa.com/img/817271ee05fc2bbe.png" alt="Diagram: four ways a free Postgres stops working - it sleeps (Neon, Koyeb), it is paused (Supabase), it hits a wall (Supabase, Neon, Aiven), it is deleted (Render, trials)" width="700" loading="lazy" decoding="async"></p>
<p><strong>1. It sleeps.</strong> Neon, Koyeb, Prisma Postgres, Netlify Database, Layerbase and CockroachDB scale
compute to zero when idle and wake on the next query. Nothing is lost; the first request after a quiet
period is slower.</p>
<p><strong>2. It is paused or stopped.</strong> Supabase pauses a free project after a week without enough activity. A
paused project does not answer at all until you restore it from the dashboard. Supabase emails a
warning about a week before. A paused project can be restored for up to a year
(<a href="https://supabase.com/docs/guides/platform/free-project-pausing">Supabase project pausing</a>). Our
<a href="/blog/supabase-free-tier-limits/">Supabase free tier limits</a> guide covers each limit in detail, and
the <a href="/blog/supabase-backup-guide/">Supabase backup guide</a> shows how to keep your own copy. On Voroa, a
free database is stopped, with its data kept, when the workspace’s free hours run out, and starts
again on the 1st.</p>
<p><strong>3. It is deleted.</strong> Render deletes a free database 30 days after creation, plus a 14-day grace
period (Render free tier docs; more in our <a href="/blog/render-alternative-india/">Render alternative</a>
comparison). Prisma deletes an instant database you never claimed after 24 hours. AWS closes the
free-plan account after 6 months or when the credit runs out, and says you are not charged unless you
move to a paid plan. Azure’s free amounts end after 12 months, and then normal billing applies.</p>
<p><strong>4. It hits a wall.</strong> Every free tier has a hard limit that changes behaviour without warning your
users:</p>
<ul>
<li>Supabase puts the database into <strong>read-only mode above 500 MB</strong>. Inserts start failing with
<code>cannot execute INSERT in a read-only transaction</code>.</li>
<li>Neon <strong>blocks writes that would grow storage past 1 GB</strong>, and when a project uses up its 100
CU-hours, <strong>its compute is suspended until the next month</strong>: existing connections drop and new ones
cannot open (Neon free plan limits). How Neon and Supabase differ beyond the free tier is in
<a href="/blog/neon-vs-supabase-india/">Neon vs Supabase</a>.</li>
<li>Prisma Postgres <strong>stops answering queries once the month’s 200,000 are used</strong>, until the 1st or
until you upgrade.</li>
<li>Aiven allows <strong>20 connections and no pooling</strong>. A serverless app can use that up in one busy
minute and start failing with <code>too many connections</code>.</li>
</ul>
<h2 id="neon-free-tier-the-day-17-problem" tabindex="-1"><a class="header-anchor" href="#neon-free-tier-the-day-17-problem">Neon free tier: the day-17 problem</a></h2>
<p>Neon’s free compute is generous for a database that sleeps, and tight for one that never does. The
free compute starts at 0.25 CU, so 100 CU-hours buys 400 hours of running time. A month has about 730
hours. If something touches the database at least every five minutes, around the clock, it never
scales to zero and the allowance runs out after roughly 16.7 days. From then until the 1st of next
month, the project’s compute is suspended.</p>
<p>What keeps a database awake is often not your users. An uptime monitor that pings an endpoint every
minute, a cron job, or a worker that polls a queue is enough. If you need a database that is awake
24 hours a day, either remove those callers or pick a tier that does not meter compute hours.</p>
<h2 id="when-to-leave-a-free-tier-and-what-the-paid-floor-costs" tabindex="-1"><a class="header-anchor" href="#when-to-leave-a-free-tier-and-what-the-paid-floor-costs">When to leave a free tier, and what the paid floor costs</a></h2>
<p>Move to a paid size when any of these becomes true:</p>
<ul>
<li><strong>You need backups.</strong> Supabase, Render, Prisma Postgres, Nile and Voroa have none on their free
sizes.</li>
<li><strong>Real users depend on it.</strong> A pause, a monthly cap or a deletion date is fine for a demo, not for
customers.</li>
<li><strong>You are close to a cap.</strong> Storage, compute hours or monthly queries: hitting one stops writes or
the whole database, usually at a busy moment.</li>
<li><strong>You need it awake all day.</strong> A tier that sleeps or meters compute hours adds cold starts and a
month-end cliff.</li>
</ul>
<p>The cheapest paid step on each provider, from their own pricing pages on 14 November 2026:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Provider</th>
<th>Cheapest paid option</th>
<th>What it adds</th>
</tr>
</thead>
<tbody>
<tr>
<td>Neon</td>
<td>Launch, pay for what you use: $0.106 per CU-hour, $0.35 per GB-month</td>
<td>No monthly minimum; compute and storage metered</td>
</tr>
<tr>
<td>Supabase</td>
<td>Pro, from $25/month</td>
<td>8 GB disk per project, $10 of compute credits, daily backups kept 7 days</td>
</tr>
<tr>
<td>Prisma Postgres</td>
<td>Starter, $10/month</td>
<td>1 million operations, 10 GB, daily backups kept 7 days</td>
</tr>
<tr>
<td>Render</td>
<td>$6/month</td>
<td>256 MB RAM, 100 connections; storage $0.30 per GB</td>
</tr>
<tr>
<td>Aiven</td>
<td>Developer, $5/month</td>
<td>1 CPU, 1 GB RAM, 8 GB storage</td>
</tr>
<tr>
<td>Railway</td>
<td>Hobby, $5/month including $5 of usage</td>
<td>Then about $10 per GB of RAM and $0.15 per GB of volume a month</td>
</tr>
<tr>
<td>Voroa</td>
<td>DB Starter, Rs 299/month plus GST</td>
<td>1 GB RAM, 0.5 CPU, 1 GB storage (Rs 21 per extra GB), 100 connections, always on; nightly backups can be switched on</td>
</tr>
</tbody>
</table></div>
<p>Dollar prices are before tax and your card’s foreign currency markup. Voroa’s sizes are billed in
rupees with a GST invoice; the full list is on the <a href="/pricing/">pricing page</a>.</p>
<h2 id="free-postgres-tiers-that-disappeared" tabindex="-1"><a class="header-anchor" href="#free-postgres-tiers-that-disappeared">Free Postgres tiers that disappeared</a></h2>
<p>The list of free Postgres options keeps changing, which is a reason to keep your own copy of your
data whatever you choose:</p>
<ul>
<li><strong>Heroku</strong> removed free Postgres on 28 November 2022 and deleted databases that were not upgraded.</li>
<li><strong>ElephantSQL</strong> stopped taking sign-ups in May 2024 and shut down on 27 January 2025.</li>
<li><strong>Vercel Postgres</strong> moved every database to Neon in December 2024.</li>
<li><strong>Tembo</strong> stopped creating new cloud instances on 5 May 2025.</li>
<li><strong>Xata</strong> no longer has a managed free tier; its pricing page offers a free trial, and the
open-source version is free to run yourself.</li>
<li><strong><a href="http://Fly.io">Fly.io</a></strong> no longer gives free Postgres to new accounts.</li>
<li><strong>Neon</strong> raised free storage from 0.5 GB to 1 GB per project on 2 October 2026, and <strong>Prisma
Postgres</strong> now gives 2 GB per workspace. Many comparison pages still show the old figures.</li>
</ul>
<p>The habit that protects you on any provider takes one line:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">pg_dump &quot;$DATABASE_URL&quot; --format=custom --file=backup-$(date +%F).dump
</code></pre>
</div>
<p>Run it weekly. If your provider changes its free tier, you restore that file somewhere else with
<code>pg_restore</code> and move on. On Voroa the same file goes in through
<a href="/docs/import-existing-database/">Import an existing database</a>.</p>
<h2 id="is-postgresql-free" tabindex="-1"><a class="header-anchor" href="#is-postgresql-free">Is PostgreSQL free?</a></h2>
<p>Yes. PostgreSQL itself is free and open source under the
<a href="https://www.postgresql.org/about/licence/">PostgreSQL License</a>, a short permissive licence similar to
MIT and BSD. You can use it, change it and ship it inside a commercial product without paying anyone
and without publishing your own code.</p>
<p><strong>Is PostgreSQL free for commercial use?</strong> Yes, with no user limit, no core limit and no paid
“enterprise edition” of the database itself. Companies that sell Postgres sell the work around it:
running the server, backups, upgrades and support.</p>
<p>That is why this page exists. The database costs nothing; the machine it runs on does. A free
Postgres database is a provider choosing to pay for a small machine on your behalf, and the catch in
each free tier is how they keep that cost bounded.</p>
<h2 id="free-managed-postgres-in-mumbai-on-voroa" tabindex="-1"><a class="header-anchor" href="#free-managed-postgres-in-mumbai-on-voroa">Free managed Postgres in Mumbai on Voroa</a></h2>
<p>Voroa’s free plan includes one managed Postgres per account: 1 GB of storage, 256 MB of RAM, 0.1 CPU
and up to 20 connections, in Mumbai, with no card. It is not paused for inactivity and not deleted
after a fixed time.</p>
<p>It does share the workspace’s 750 free running hours a month with your free web services, workers and
cron jobs. One free database on its own runs all month within that. A free database and a free web
service running all month use the hours in about 16 days; then both are stopped, with nothing deleted,
until 12 midnight IST on the 1st. If a paid app depends on the database, move the database to a paid
size too.</p>
<p>The other trade-offs are real: it is a small machine, like Render’s free database; there are no
automatic backups on the free size, so keep your own <code>pg_dump</code>; and free database capacity is shared,
so if the platform is full, a new free database is refused with a clear message rather than queued.</p>
<p>Paid sizes are billed in rupees, by UPI or card, with a GST invoice. Details are in
<a href="/docs/free-instances/">Free instances on Voroa</a>.</p>
<h2 id="how-to-create-a-free-postgres-database-online-step-by-step" tabindex="-1"><a class="header-anchor" href="#how-to-create-a-free-postgres-database-online-step-by-step">How to create a free Postgres database online, step by step</a></h2>
<p>You can get a free Postgres database online in about two minutes, from the browser, with no card
and nothing to install. You need a Voroa account; sign-up is with email or GitHub.</p>
<p><strong>Step 1. Choose Postgres.</strong> In the dashboard, select <strong>New</strong>, then <strong>Postgres</strong> from the list of
services.</p>
<p><img src="https://media.getvoroa.com/img/27a166a5b81fb4c2.png" alt="Voroa dashboard, Create a new service screen, with Postgres in the list next to Web Service, Key Value and Static Site" width="700" loading="lazy" decoding="async"></p>
<p><strong>Step 2. Name the database.</strong> PostgreSQL is already selected as the database type. Type a name
using lowercase letters, numbers and hyphens, for example <code>shop-db</code>. Leave the Postgres database name
empty and Voroa generates one. The region is Mumbai.</p>
<p><img src="https://media.getvoroa.com/img/72ddf3e371b09d21.png" alt="New PostgreSQL database form: PostgreSQL selected, database name shop-db, user postgres, region Mumbai (IN)" width="700" loading="lazy" decoding="async"></p>
<p><strong>Step 3. Pick the free size.</strong> Under <strong>Instance type</strong>, choose <strong>DB Nano</strong>. It shows <strong>Free</strong>, and the
monthly total at the bottom of the page reads Free as well, with 1 GB of storage included.</p>
<p><img src="https://media.getvoroa.com/img/4658cc4d2c3a3802.png" alt="Instance type card: DB Nano, 256 MB RAM, 0.1 CPU, Free" width="360" loading="lazy" decoding="async"></p>
<p><img src="https://media.getvoroa.com/img/139331e088da5ad4.png" alt="Monthly total: DB Nano instance with 1 GB storage, Free" width="700" loading="lazy" decoding="async"></p>
<p>Select <strong>Create database</strong>. If the platform’s shared free capacity is full at that moment, you see a
clear message instead of a half-created database.</p>
<p><strong>Step 4. Copy the connection URL.</strong> Your database opens on the <strong>Connect</strong> tab. With <strong>An app on
Voroa</strong> selected, the URL there is your <code>DATABASE_URL</code>. Select the eye icon to show the password,
then copy the whole line into your app’s environment variables.
This address works only from apps running on Voroa in the same workspace, which keeps the traffic
inside Mumbai.</p>
<p><img src="https://media.getvoroa.com/img/08d1135d4697e463.png" alt="Connect tab: An app on Voroa selected, connection URL with the password hidden behind an eye icon, and a Copy URL template button" width="700" loading="lazy" decoding="async"></p>
<p><strong>Step 5. Connect from your laptop, if you need to.</strong> Switch to <strong>Outside Voroa</strong> to get a second
address for <code>psql</code>, pgAdmin, DBeaver or an app hosted elsewhere. Connections are encrypted with TLS.
If the tab shows <strong>OFF</strong>, choose <strong>Enable external access</strong>; the database restarts for a few seconds.
Choose <strong>Only addresses I trust</strong> if you know the IP addresses that will connect. Full details are in
<a href="/docs/connect-postgres-externally/">connecting to Postgres from outside Voroa</a>.</p>
<p><img src="https://media.getvoroa.com/img/57a36e3f439f9614.png" alt="Outside Voroa tab: who may connect, any source or only trusted addresses, and an Enable external access button" width="700" loading="lazy" decoding="async"></p>
<p><strong>Step 6. Use it from your code.</strong> Under <strong>Connect from your code</strong> pick psql, Node.js, Prisma,
Python or Go and copy a ready snippet that reads <code>DATABASE_URL</code> from the environment.</p>
<p><img src="https://media.getvoroa.com/img/a0278b345f9d5315.png" alt="Connect from your code: Node.js tab with a pg Client snippet that reads process.env.DATABASE_URL" width="700" loading="lazy" decoding="async"></p>
<p>That is the whole setup. What each connection detail means is explained in
<a href="/docs/postgresql-creating-connecting/">PostgreSQL connection details</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>Are there Postgres database providers with generous free tiers?</strong>
Yes. By storage, CockroachDB Basic (10 GiB), Layerbase (5 GB), Netlify Database (5 GB per database)
and Prisma Postgres (2 GB per workspace) give the most, and Neon gives 1 GB per project across up to
100 projects. Each has a catch: sleeping, a usage cap, or a card requirement.</p>
<p><strong>Is there a completely free Postgres database?</strong>
Yes. Neon, Supabase, Aiven, Koyeb, Prisma Postgres, Nile, Netlify Database, Layerbase and Voroa all
offer a free Postgres with no end date. Each has a catch: sleeping, pausing, a usage allowance, or a
small machine.</p>
<p><strong>Which free Postgres tier does not expire?</strong>
Neon, Supabase, Aiven, Koyeb, Prisma Postgres, Nile, CockroachDB Basic, Netlify Database, Layerbase
and Voroa have no expiry date. Render’s free database is deleted 30 days after creation plus a 14-day
grace period. The AWS free plan closes after 6 months and Azure’s free amounts end after 12.</p>
<p><strong>Which free Postgres does not sleep or pause?</strong>
Render, Aiven (unless it is unused) and Nile keep running. Voroa is not paused for inactivity, but it
stops if the workspace’s free hours run out. Neon, Koyeb, Prisma Postgres, Netlify Database and
Layerbase scale to zero when idle. Supabase pauses a project after a week of low activity.</p>
<p><strong>Can I get free Postgres hosted in India?</strong>
Supabase, CockroachDB Basic and Voroa offer a Mumbai region on their free tiers. AWS Mumbai and Azure
Central India are free only on their time-limited offers. Neon, Render, Railway, Koyeb and Prisma
Postgres are closest in Singapore.</p>
<p><strong>Is PostgreSQL free for commercial use?</strong>
Yes. The PostgreSQL License lets you use it in commercial software for free. Hosting it is what costs
money, which is what the free tiers above cover.</p>
<p><strong>Where can I get free Postgres for testing?</strong>
For automated tests, run Postgres in a Docker or CI container so every run starts clean. For a shared
staging database, any free hosted tier on this page works; keep destructive tests off it.</p>
<p><strong>Do I need a credit card for free Postgres hosting?</strong>
Not for Supabase, Aiven, Prisma Postgres, Layerbase or Voroa. CockroachDB’s free allowance is for
organisations paying by card or marketplace, and AWS and Azure free offers are tied to a billing
account.</p>
<p><strong>Is a free Postgres tier good enough for production?</strong>
For a small app with modest traffic, sometimes, if you accept the catch of the tier you pick and keep
your own backups. None of the free tiers in this list comes with an uptime commitment.</p>
<p><strong>How much latency does a Singapore database add for users in India?</strong>
In our tests, about 40 ms per query from a home connection and about 55 ms per query from a server in
Mumbai, compared with a database in Mumbai. Five sequential queries make that roughly 200-300 ms per
request.</p>
<h2 id="get-free-postgres-in-mumbai" tabindex="-1"><a class="header-anchor" href="#get-free-postgres-in-mumbai">Get free Postgres in Mumbai</a></h2>
<p>One free Postgres per account, 1 GB, no card, and not paused for inactivity. See how it compares in
<a href="/managed-postgresql-india/">managed PostgreSQL in India</a>.</p>
<p><em>Last updated: 14 November 2026: every free-tier limit re-checked against each provider’s own page;
latency measured on 16 October 2026. If a provider has changed something we missed, write to us and
we will correct it.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Heroku Pricing in India 2026: Dynos, Postgres and Cost in INR</title>
      <link>https://getvoroa.com/blog/heroku-pricing-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/heroku-pricing-india/</guid>
      <pubDate>Wed, 14 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <description><![CDATA[Heroku pricing explained for India: every dyno tier, Eco hours, Postgres and Key-Value prices, how billing works, GST and forex, and three bills in rupees.]]></description>
      <content:encoded><![CDATA[<p>Heroku pricing is built from small monthly prices that add up. Each process you run is a dyno, each
dyno has a monthly price, and databases and caches are add-ons with their own prices. There has been no
free plan since November 2022.</p>
<p>This guide lists the current prices, explains how Heroku bills, and works three bills through to the
rupee amount on an Indian card. All figures are from Heroku’s
<a href="https://devcenter.heroku.com/articles/dyno-types">dyno types documentation</a> and add-on pages, checked
on 14 October 2026.</p>
<h2 id="heroku-pricing-at-a-glance-every-dyno-tier" tabindex="-1"><a class="header-anchor" href="#heroku-pricing-at-a-glance-every-dyno-tier">Heroku pricing at a glance: every dyno tier</a></h2>
<p>Except for Eco, every price below is the most you pay for one dyno running all month on the Common
Runtime.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Dyno</th>
<th>Price per month</th>
<th>Memory</th>
<th>Sleeps when idle</th>
<th>Scale to more than one</th>
</tr>
</thead>
<tbody>
<tr>
<td>Eco</td>
<td>$5 for a shared pool of hours</td>
<td>0.5 GB</td>
<td>Yes, after 30 minutes</td>
<td>No</td>
</tr>
<tr>
<td>Basic</td>
<td>$7</td>
<td>0.5 GB</td>
<td>No</td>
<td>No</td>
</tr>
<tr>
<td>Standard-1X</td>
<td>$25</td>
<td>0.5 GB</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Standard-2X</td>
<td>$50</td>
<td>1 GB</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Performance-M</td>
<td>$250</td>
<td>2.5 GB</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Performance-L</td>
<td>$500</td>
<td>14 GB</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Performance-L-RAM</td>
<td>$500</td>
<td>30 GB</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Performance-XL</td>
<td>$750</td>
<td>62 GB</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Performance-2XL</td>
<td>$1,500</td>
<td>126 GB</td>
<td>No</td>
<td>Yes</td>
</tr>
</tbody>
</table></div>
<p>In rupees, before tax, at Rs 95.20 to the dollar: Basic is about Rs 666 a dyno, Standard-1X about
Rs 2,380 and Standard-2X about Rs 4,760.</p>
<p>Private and Shield dynos, which run inside a Private Space with network isolation, start at $125 (Private-S)
and $150 (Shield-S) per dyno per month. Performance dynos need an established payment history before
you can use them.</p>
<h2 id="is-heroku-free-eco-dynos-and-the-1000-hour-pool" tabindex="-1"><a class="header-anchor" href="#is-heroku-free-eco-dynos-and-the-1000-hour-pool">Is Heroku free? Eco dynos and the 1,000-hour pool</a></h2>
<p>No. Heroku removed its free dynos in November 2022. The cheapest option is the <strong>Eco</strong> plan: $5 a month
for a pool of 1,000 dyno hours, shared by every Eco dyno on your account.</p>
<ul>
<li><strong>Eco dynos sleep</strong> after 30 minutes without web traffic and wake on the next request, which is slow.</li>
<li><strong>When the 1,000 hours run out</strong>, every Eco dyno on the account sleeps for the rest of the month. You
cannot buy more hours; you can only move apps to Basic.</li>
<li><strong>Eco is for personal apps only.</strong> Apps in a Heroku Team cannot use it.</li>
</ul>
<p>One Eco dyno that never sleeps uses about 744 hours a month, so the pool covers one always-busy app or
a few that sleep most of the time.</p>
<h2 id="how-heroku-bills-per-second-capped-monthly" tabindex="-1"><a class="header-anchor" href="#how-heroku-bills-per-second-capped-monthly">How Heroku bills: per second, capped monthly</a></h2>
<p>Heroku charges by wall-clock time, not CPU time, and prorates every dyno and add-on to the second. The
monthly price is the cap: a Basic dyno that runs for two hours costs about two cents, and one that runs
all month costs $7. Scale a dyno to zero and it stops costing money.</p>
<p>Heroku’s <a href="https://devcenter.heroku.com/articles/usage-and-billing">usage and billing guide</a> adds
two details:</p>
<ul>
<li><strong>You are billed for last month.</strong> The invoice you receive covers the previous month of use, not the
current one.</li>
<li><strong>Every process is a dyno.</strong> A web process and a background worker are two dynos, each with its own price.
That is why a small app with a worker costs twice what the dyno table suggests.</li>
</ul>
<h2 id="heroku-postgres-and-key-value-store-prices" tabindex="-1"><a class="header-anchor" href="#heroku-postgres-and-key-value-store-prices">Heroku Postgres and Key-Value Store prices</a></h2>
<p>Databases and caches are add-ons billed the same way, per second up to a monthly cap.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Heroku Postgres</th>
<th>Price per month</th>
<th>Memory</th>
<th>Storage</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Essential-0</td>
<td>$5</td>
<td>Shared</td>
<td>1 GB</td>
<td>No high availability</td>
</tr>
<tr>
<td>Essential-1</td>
<td>$9</td>
<td>Shared</td>
<td>10 GB</td>
<td>No high availability</td>
</tr>
<tr>
<td>Essential-2</td>
<td>$20</td>
<td>Shared</td>
<td>32 GB</td>
<td>No high availability</td>
</tr>
<tr>
<td>Standard-0</td>
<td>$50</td>
<td>4 GB</td>
<td>64 GB</td>
<td>200 connections, 4-day rollback</td>
</tr>
<tr>
<td>Premium-0</td>
<td>$200</td>
<td>4 GB</td>
<td>64 GB</td>
<td>High availability, 7-day rollback</td>
</tr>
</tbody>
</table></div>
<p>Larger Standard and Premium plans go up to $12,000 and $24,000 a month. Heroku Key-Value Store (the
product formerly called Heroku Redis) starts at $3 a month for Mini (25 MB), then $15 for Premium-0 (50
MB) and $30 for Premium-1.</p>
<h2 id="what-heroku-costs-in-rupees-forex-gst-and-the-mumbai-question" tabindex="-1"><a class="header-anchor" href="#what-heroku-costs-in-rupees-forex-gst-and-the-mumbai-question">What Heroku costs in rupees: forex, GST and the Mumbai question</a></h2>
<p>Heroku prices and bills in US dollars. Three things turn a dollar bill into rupees:</p>
<ul>
<li><strong>The exchange rate.</strong> We use Rs 95.20 to the dollar, the mid-market rate on 14 October 2026.</li>
<li><strong>Card forex charges.</strong> Many Indian cards add a 3.5% markup plus 18% GST on it, about 4.13% in total.</li>
<li><strong>GST.</strong> Heroku says taxes such as VAT are the customer’s responsibility and are not included in its
invoices; it does not mention India. A GST-registered business usually pays 18% under reverse charge
and claims it back. You can add your GST number to the invoice from your billing settings. Our guide
to <a href="/docs/reverse-charge-foreign-cloud-hosting/">reverse charge on foreign hosting</a> has the steps.</li>
</ul>
<p>Location matters too. Common Runtime apps, which is where Eco, Basic and Standard dynos run, are hosted
in the United States or Europe. Heroku’s Mumbai region exists only for Private Spaces, where dynos start
at $125 each. For most Indian teams, every request crosses an ocean.</p>
<h2 id="three-heroku-bills-worked-out-in-rupees" tabindex="-1"><a class="header-anchor" href="#three-heroku-bills-worked-out-in-rupees">Three Heroku bills worked out in rupees</a></h2>
<p><img src="https://media.getvoroa.com/img/1a650917960242fd.png" alt="Diagram: a Heroku bill is one price per dyno, plus each add-on such as Postgres and Key-Value Store, prorated to the second up to a monthly cap, then card forex and reverse-charge GST in India" width="700" loading="eager" decoding="async"></p>
<p>Each bill below assumes everything runs all month, so each line is at its monthly cap.</p>
<p><strong>Bill 1: a side project.</strong> One Eco web dyno and Postgres Essential-0.
Total: 5 + 5 = <strong>$10</strong>.</p>
<p><strong>Bill 2: a small production app.</strong> A Basic web dyno, a Basic worker dyno, Postgres Essential-1 and
Key-Value Mini.
Total: 7 + 7 + 9 + 3 = <strong>$26</strong>.</p>
<p><strong>Bill 3: a growing product.</strong> Two Standard-1X web dynos, one Standard-1X worker, Postgres Standard-0 and
Key-Value Premium-0.
Total: 50 + 25 + 50 + 15 = <strong>$140</strong>.</p>
<p>What reaches your card, and the GST a registered business then pays separately:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Bill</th>
<th>At Rs 95.20</th>
<th>Plus 4.13% card charges</th>
<th>On your card</th>
<th>Reverse-charge GST (18%)</th>
</tr>
</thead>
<tbody>
<tr>
<td>$10</td>
<td>Rs 952</td>
<td>Rs 39</td>
<td>Rs 991</td>
<td>Rs 171</td>
</tr>
<tr>
<td>$26</td>
<td>Rs 2,475</td>
<td>Rs 102</td>
<td>Rs 2,577</td>
<td>Rs 446</td>
</tr>
<tr>
<td>$140</td>
<td>Rs 13,328</td>
<td>Rs 550</td>
<td>Rs 13,878</td>
<td>Rs 2,399</td>
</tr>
</tbody>
</table></div>
<p>Under India’s e-mandate rules, an automatic card payment above Rs 15,000 needs your approval each time.
At today’s rate that is a Heroku bill of about $151.</p>
<h2 id="when-heroku-costs-more-than-it-needs-to" tabindex="-1"><a class="header-anchor" href="#when-heroku-costs-more-than-it-needs-to">When Heroku costs more than it needs to</a></h2>
<p>Heroku’s strengths are real: a long record, a large add-on marketplace and a familiar workflow. The cost
is in the small tiers. A Standard-1X dyno gives 0.5 GB of memory for $25, and a worker doubles the bill.</p>
<p><a href="/web-services/">Voroa’s web services</a> run in Mumbai at fixed rupee prices. The free Nano size has 512 MB
of memory and 0.1 vCPU and sleeps when idle, like Eco but at no cost; its limits are in
<a href="/docs/free-instances/">what the free tier includes</a>. The smallest always-on size, Starter, has 1 GB of
memory and 1 vCPU for Rs 298 a month: less than half the rupee cost of a Heroku Basic dyno, with
twice its memory.
<a href="/managed-postgresql-india/">Managed PostgreSQL in Mumbai</a> has a free size with 1 GB of storage and
paid sizes priced in rupees.</p>
<p>All Voroa prices exclude 18% GST and come with a GST invoice, paid by UPI or an Indian card. The full list
is on the <a href="/pricing/">pricing page</a>. Comparing more platforms? Our guides to
<a href="/blog/vercel-pricing-india/">Vercel pricing</a> and <a href="/blog/railway-pricing-india/">Railway pricing</a> do the
same sums.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-heroku-free-in-2026" tabindex="-1"><a class="header-anchor" href="#is-heroku-free-in-2026">Is Heroku free in 2026?</a></h3>
<p>No. Free dynos ended in November 2022. The cheapest plan is Eco at $5 a month for 1,000 shared hours,
and databases start at $5 a month.</p>
<h3 id="how-much-does-heroku-cost-per-month" tabindex="-1"><a class="header-anchor" href="#how-much-does-heroku-cost-per-month">How much does Heroku cost per month?</a></h3>
<p>A small app with one Basic dyno and the smallest Postgres costs $12 a month. Add a worker dyno and it
is $19. A production app on Standard dynos usually costs $100 or more.</p>
<h3 id="does-heroku-have-servers-in-india" tabindex="-1"><a class="header-anchor" href="#does-heroku-have-servers-in-india">Does Heroku have servers in India?</a></h3>
<p>Only for Private Spaces, where dynos start at $125 each. Standard apps run in the United States or Europe.</p>
<h3 id="does-heroku-charge-gst" tabindex="-1"><a class="header-anchor" href="#does-heroku-charge-gst">Does Heroku charge GST?</a></h3>
<p>Heroku says taxes such as VAT are the customer’s responsibility and are not included in its invoices. A
GST-registered business usually pays 18% under reverse charge.</p>
<h2 id="moving-off-heroku-without-paying-for-two-months" tabindex="-1"><a class="header-anchor" href="#moving-off-heroku-without-paying-for-two-months">Moving off Heroku without paying for two months</a></h2>
<p>Most Indian teams on Heroku pay dyno and Postgres charges on a dollar card. Voroa bills in
rupees from an Indian entity, with GST on the invoice, so a registered business takes the input tax
credit straight from it. There is no reverse-charge GST to pay in cash first, and no card forex markup.</p>
<p>Moving usually costs a month of two bills. To take that off the table, you can apply for
<a href="/migration-credits/">migration credits</a>: up to Rs 10,000 set against your Voroa bills, granted by hand
after a person reads your application. Applying is not the same as being approved.</p>
<p>The full comparison of a dollar bill and a rupee bill is in
<a href="/docs/cloud-hosting-inr-vs-usd/">INR vs USD cloud hosting</a>.</p>
<h2 id="price-your-app-in-rupees-on-voroa" tabindex="-1"><a class="header-anchor" href="#price-your-app-in-rupees-on-voroa">Price your app in rupees on Voroa</a></h2>
<p>Deploy a GitHub repository on the free size and see what paid sizes cost in rupees before you upgrade.
<a href="/docs/deploy-from-github/">Deploy your first app from GitHub</a> takes a few minutes.</p>
<p><em>Last checked: 14 October 2026, against Heroku’s dyno types, usage and billing, Postgres and Key-Value
Store documentation.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Railway Pricing in India 2026: Free Tier, Plans and INR Cost</title>
      <link>https://getvoroa.com/blog/railway-pricing-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/railway-pricing-india/</guid>
      <pubDate>Wed, 14 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <description><![CDATA[Railway pricing for India (the Railway.com hosting platform): free tier and trial, Hobby vs Pro, usage rates, GST and card forex, and three worked bills in INR.]]></description>
      <content:encoded><![CDATA[<p>Railway pricing looks small: a free tier, Hobby at $5 a month and Pro at $20. The catch is that the
plan fee is only a minimum. Railway meters CPU, memory, storage and data transfer by the minute, and
the plan fee simply covers the first $5 or $20 of that usage.</p>
<p>This guide explains each plan, what the free tier really gives you, the usage rates, and three bills
worked through to the rupee amount on an Indian card. All figures are from Railway’s
<a href="https://docs.railway.com/pricing/plans">pricing documentation</a>, checked on 14 October 2026.</p>
<h2 id="railway-pricing-in-one-table-trial-free-hobby-pro" tabindex="-1"><a class="header-anchor" href="#railway-pricing-in-one-table-trial-free-hobby-pro">Railway pricing in one table: Trial, Free, Hobby, Pro</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Trial</th>
<th>Free</th>
<th>Hobby</th>
<th>Pro</th>
</tr>
</thead>
<tbody>
<tr>
<td>Price</td>
<td>$0</td>
<td>$0</td>
<td>$5 per month</td>
<td>$20 per month per workspace</td>
</tr>
<tr>
<td>Usage included</td>
<td>$5 once, for 30 days</td>
<td>$1 per month</td>
<td>$5 per month</td>
<td>$20 per month</td>
</tr>
<tr>
<td>In rupees, before tax (Rs 95.20 per $)</td>
<td>Rs 0</td>
<td>Rs 0</td>
<td>Rs 476</td>
<td>Rs 1,904</td>
</tr>
<tr>
<td>Largest service</td>
<td>1 GB RAM, shared vCPU</td>
<td>0.5 GB RAM, 1 vCPU</td>
<td>48 GB RAM, 48 vCPU</td>
<td>1 TB RAM, 1,000 vCPU</td>
</tr>
<tr>
<td>Projects and services</td>
<td>5 services per project</td>
<td>1 project, 3 services</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Members</td>
<td>-</td>
<td>1</td>
<td>3</td>
<td>Unlimited, no seat fee</td>
</tr>
<tr>
<td>Volume storage</td>
<td>0.5 GB</td>
<td>0.5 GB</td>
<td>5 GB</td>
<td>Up to 1,000 GB</td>
</tr>
<tr>
<td>Log history</td>
<td>7 days</td>
<td>3 days</td>
<td>7 days</td>
<td>30 days</td>
</tr>
</tbody>
</table></div>
<p>Pro is priced per workspace, not per seat: adding teammates does not change the bill. Enterprise is a
custom contract.</p>
<h2 id="railway-free-tier-the-5-trial-and-the-1-free-plan" tabindex="-1"><a class="header-anchor" href="#railway-free-tier-the-5-trial-and-the-1-free-plan">Railway free tier: the $5 trial and the $1 Free plan</a></h2>
<p>Railway’s free tier comes in two steps. A new account gets a <strong>Trial</strong>: a one-time $5 credit that lasts
up to 30 days. If Railway can verify your GitHub account, you get the full trial. If it cannot, often
because the GitHub account is new, you get a <strong>Limited Trial</strong> with restricted outbound network access
and only some ports open.</p>
<p>When the 30 days or the $5 run out, the account moves to the <strong>Free</strong> plan, which adds $1 of credit each
month. You can still deploy code and databases on it, within 0.5 GB of RAM per service, one project
and three services.</p>
<p>The $1 does not go far. Memory costs about $10 per GB per month, so one service holding 0.5 GB all day
uses about $5 a month in memory alone. The $1 lasts roughly six days. Railway staff on its support
forum say services stop when the credit is used up, and you redeploy them yourself once next month’s $1
arrives.</p>
<p>Two more rules matter before you put real data on the free tier:</p>
<ul>
<li><strong>Volumes are deleted.</strong> Railway deletes volumes from Trial accounts 30 days after the credits expire,
unless you upgrade before then.</li>
<li><strong>Sleeping helps, with a cost.</strong> Railway’s Serverless setting puts a service to sleep after 10 minutes
without outbound traffic, which saves credit. The first request after that wakes it, and Railway warns
that this request may return a 502 error.</li>
</ul>
<h2 id="how-railway-pricing-meters-usage" tabindex="-1"><a class="header-anchor" href="#how-railway-pricing-meters-usage">How Railway pricing meters usage</a></h2>
<p>On Hobby and Pro you pay for what each running service consumes, minute by minute. A service that holds
512 MB of RAM at night with no visitors is still billed for that memory. The rates, from Railway’s docs:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Resource</th>
<th>Monthly rate</th>
<th>Per-minute rate</th>
</tr>
</thead>
<tbody>
<tr>
<td>Memory</td>
<td>$10 per GB</td>
<td>$0.000231 per GB</td>
</tr>
<tr>
<td>CPU</td>
<td>$20 per vCPU</td>
<td>$0.000463 per vCPU</td>
</tr>
<tr>
<td>Volume storage</td>
<td>$0.15 per GB</td>
<td>$0.0000035 per GB</td>
</tr>
<tr>
<td>Network egress</td>
<td>$0.05 per GB</td>
<td>-</td>
</tr>
<tr>
<td>Object storage (buckets)</td>
<td>$0.015 per GB, free egress</td>
<td>-</td>
</tr>
</tbody>
</table></div>
<p>Your bill is the plan fee, plus any usage above what the plan includes:</p>
<ul>
<li>usage of $3 on Hobby means a bill of $5;</li>
<li>usage of $7 on Hobby means $5 plus $2, so $7.</li>
</ul>
<p>The included usage resets every billing cycle and does not roll over.</p>
<p>Timing is split. The plan fee is billed <strong>in advance</strong> for the coming month, and any usage above it is
billed <strong>in arrears</strong> for the month just gone. So a single invoice can carry two different months.</p>
<h2 id="hobby-or-pro-what-the-5-and-20-buy" tabindex="-1"><a class="header-anchor" href="#hobby-or-pro-what-the-5-and-20-buy">Hobby or Pro: what the $5 and $20 buy</a></h2>
<p>Hobby suits one developer, or up to three people, running a few small services. Its limits are generous
on size (48 GB of RAM per service) but tight on teams and storage: three members, 5 GB volumes and two
custom domains per service.</p>
<p>Pro removes the member limit, raises volumes to 1,000 GB and custom domains to 20 per service, and keeps
30 days of logs. It also unlocks <strong>committed spend</strong> tiers for larger teams:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Monthly commitment</th>
<th>What it adds</th>
</tr>
</thead>
<tbody>
<tr>
<td>$1,000</td>
<td>90-day log history, HIPAA BAA</td>
</tr>
<tr>
<td>$2,000</td>
<td>Single sign-on, role-based access control, 18-month audit logs</td>
</tr>
<tr>
<td>$5,000</td>
<td>Slack Connect channel, critical support tickets, higher resource limits</td>
</tr>
<tr>
<td>$10,000</td>
<td>Dedicated instances</td>
</tr>
</tbody>
</table></div>
<h2 id="spend-limits-an-email-alert-or-a-hard-stop" tabindex="-1"><a class="header-anchor" href="#spend-limits-an-email-alert-or-a-hard-stop">Spend limits: an email alert or a hard stop</a></h2>
<p>Because the bill follows usage, set a limit on day one. Railway offers two kinds:</p>
<ul>
<li>A <strong>custom email alert</strong> is a soft limit. You get an email at the amount you choose, and services keep
running.</li>
<li>A <strong>hard limit</strong> is a real cap, with a minimum of $10. Railway emails you at 75% and 90%. At 100%,
every workload in the workspace is taken offline until you raise or remove the limit.</li>
</ul>
<p>The hard limit protects your card, not your uptime.</p>
<h2 id="railway-pricing-on-an-indian-card-gst-forex-and-singapore" tabindex="-1"><a class="header-anchor" href="#railway-pricing-on-an-indian-card-gst-forex-and-singapore">Railway pricing on an Indian card: GST, forex and Singapore</a></h2>
<p>Railway prices and bills in US dollars and accepts only bank-issued credit or debit cards. Prepaid and
virtual cards are declined. Its FAQ says it may collect sales tax or VAT based on your billing location;
it does not mention India or GST by name. Four things decide the rupee amount:</p>
<ul>
<li><strong>GST.</strong> Indian GST on an online service from abroad is 18%. If your invoice carries it, add 18%. If
you are GST-registered and it does not, you usually pay it yourself under reverse charge and claim the
credit. Our guide to <a href="/docs/reverse-charge-foreign-cloud-hosting/">reverse charge on foreign hosting</a>
has the steps; confirm your case with your accountant.</li>
<li><strong>The exchange rate.</strong> We use Rs 95.20 to the dollar, the mid-market rate on 14 October 2026.</li>
<li><strong>Card forex charges.</strong> Many Indian cards add a 3.5% markup plus 18% GST on it, about 4.13% in total.
Some cards now offer zero markup.</li>
<li><strong>Declined payments.</strong> Indian users on Railway’s support forum report automatic charges declined by
their banks, especially larger ones. Paying the invoice manually, with an OTP, often works.</li>
</ul>
<p>There is also a speed cost. Railway has no India region: its regions are California, Virginia,
Amsterdam and Singapore. Singapore is the closest, and Indian users on Railway’s forum report extra
round-trip delay compared with a Mumbai server.</p>
<h2 id="railway-pricing-in-practice-three-worked-monthly-bills" tabindex="-1"><a class="header-anchor" href="#railway-pricing-in-practice-three-worked-monthly-bills">Railway pricing in practice: three worked monthly bills</a></h2>
<p><img src="https://media.getvoroa.com/img/2dd72b063c6af3ba.png" alt="Diagram: a Railway bill is the plan fee, plus memory and CPU used per minute, plus volume storage and egress, minus the included credit, then GST and the forex markup your card adds" width="700" loading="eager" decoding="async"></p>
<p>The usage figures below are average use over a month and are assumptions for illustration. Your own
numbers are on Railway’s usage page.</p>
<p><strong>Bill 1: a side project on Hobby.</strong> One small API averaging 0.25 GB of RAM and 5% of a vCPU, with 5 GB of
egress. Usage: $2.50 memory, $1.00 CPU and $0.25 egress, so $3.75, inside the $5.
Total: <strong>$5.00</strong>.</p>
<p><strong>Bill 2: an app and Postgres on Hobby.</strong> The app averages 0.5 GB and 10% of a vCPU. The database
averages 0.5 GB and 5% of a vCPU with a 10 GB volume, and the app sends 20 GB of egress. Usage: $10.00 memory,
$3.00 CPU, $1.50 volume and $1.00 egress, so $15.50.
Total: 5 + (15.50 - 5) = <strong>$15.50</strong>.</p>
<p><strong>Bill 3: a small team on Pro.</strong> Two app services at 1 GB and a quarter of a vCPU each, Postgres at
2 GB and half a vCPU with a 50 GB volume, Redis at 0.25 GB and 5% of a vCPU, and 200 GB of egress. Usage: $42.50
memory, $21.00 CPU, $7.50 volume and $10.00 egress, so $81.00.
Total: 20 + (81.00 - 20) = <strong>$81.00</strong>.</p>
<p>In rupees, with the GST and forex steps above:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Bill</th>
<th>At Rs 95.20</th>
<th>Plus 18% GST</th>
<th>Plus 4.13% card charges</th>
<th>Approx. on your card</th>
</tr>
</thead>
<tbody>
<tr>
<td>$5.00</td>
<td>Rs 476</td>
<td>Rs 562</td>
<td>Rs 23</td>
<td>Rs 585</td>
</tr>
<tr>
<td>$15.50</td>
<td>Rs 1,476</td>
<td>Rs 1,741</td>
<td>Rs 72</td>
<td>Rs 1,813</td>
</tr>
<tr>
<td>$81.00</td>
<td>Rs 7,711</td>
<td>Rs 9,099</td>
<td>Rs 376</td>
<td>Rs 9,475</td>
</tr>
</tbody>
</table></div>
<p>None of these crosses Rs 15,000, the amount above which India’s e-mandate rules ask you to approve each
automatic card payment. At today’s rate that happens at a bill of about $128.</p>
<h2 id="when-a-fixed-rupee-price-beats-usage-billing" tabindex="-1"><a class="header-anchor" href="#when-a-fixed-rupee-price-beats-usage-billing">When a fixed rupee price beats usage billing</a></h2>
<p>Railway pricing suits traffic that comes in bursts and services that can sleep. For a service that has
to stay up all month, a fixed price is easier to plan for. If you host the frontend separately, our
<a href="/blog/vercel-pricing-india/">Vercel pricing breakdown</a> does the same sums for Vercel.</p>
<p><a href="/web-services/">Voroa’s web services</a> run in Mumbai at fixed monthly sizes. The smallest always-on size,
Starter, has 1 GB of memory and 1 vCPU and costs Rs 298 a month, however busy it is. A team plan,
Growth, is Rs 499 per month. Prices exclude 18% GST, every payment gets a GST invoice, and you pay in
rupees by UPI, an Indian card or netbanking, with no forex markup.</p>
<p>For side projects there is a free Nano size with 512 MB of memory and 0.1 vCPU. The limits that apply to
it are listed on the <a href="/pricing/">pricing page</a>. Read
<a href="/blog/railway-alternative-india/">when a Railway alternative makes sense</a> before you move anything.</p>
<h2 id="railway-pricing-faq" tabindex="-1"><a class="header-anchor" href="#railway-pricing-faq">Railway pricing FAQ</a></h2>
<h3 id="is-railway-free" tabindex="-1"><a class="header-anchor" href="#is-railway-free">Is Railway free?</a></h3>
<p>There is a one-time $5 trial for 30 days, then a Free plan with $1 of credit a month. That is enough for
a small service that sleeps most of the time, not for one that runs all month.</p>
<h3 id="does-the-railway-free-tier-need-a-credit-card" tabindex="-1"><a class="header-anchor" href="#does-the-railway-free-tier-need-a-credit-card">Does the Railway free tier need a credit card?</a></h3>
<p>Railway ties the trial to GitHub verification rather than a card. If Railway cannot verify your GitHub
account you get a Limited Trial, and upgrading to Hobby, which needs a card, removes its network limits.</p>
<h3 id="is-railway-pro-20-per-user" tabindex="-1"><a class="header-anchor" href="#is-railway-pro-20-per-user">Is Railway Pro $20 per user?</a></h3>
<p>No. Pro is $20 per workspace and includes $20 of usage. Members are unlimited and do not add a seat fee.</p>
<h3 id="why-is-my-railway-bill-higher-than-5" tabindex="-1"><a class="header-anchor" href="#why-is-my-railway-bill-higher-than-5">Why is my Railway bill higher than $5?</a></h3>
<p>Usage above the included $5: usually memory held by an always-on service, a database volume, or egress.
Railway bills the difference between your usage and the plan fee.</p>
<h3 id="does-railway-charge-gst-in-india" tabindex="-1"><a class="header-anchor" href="#does-railway-charge-gst-in-india">Does Railway charge GST in India?</a></h3>
<p>Railway says it may add sales tax or VAT based on your billing location. GST on a foreign online service
is 18%. If it is not on the invoice and you are GST-registered, you usually pay it under reverse charge.
A <a href="/docs/gst-invoice-cloud-hosting/">GST invoice for cloud hosting</a> explains what a valid Indian
invoice shows. For the same working on a frontend host, see our <a href="/blog/vercel-pricing-india/">Vercel pricing guide</a>.</p>
<h2 id="switching-from-railway-the-paperwork-you-stop-doing" tabindex="-1"><a class="header-anchor" href="#switching-from-railway-the-paperwork-you-stop-doing">Switching from Railway: the paperwork you stop doing</a></h2>
<p>Usage billing in dollars means a different number, and the same reverse-charge step, every month. Voroa bills in
rupees from an Indian entity, with GST on the invoice, so a registered business takes the input tax
credit straight from it. There is no reverse-charge GST to pay in cash first, and no card forex markup.</p>
<p>Moving usually costs a month of two bills. To take that off the table, you can apply for
<a href="/migration-credits/">migration credits</a>: up to Rs 10,000 set against your Voroa bills, granted by hand
after a person reads your application. Applying is not the same as being approved.</p>
<p>The full comparison of a dollar bill and a rupee bill is in
<a href="/docs/cloud-hosting-inr-vs-usd/">INR vs USD cloud hosting</a>.</p>
<h2 id="try-a-fixed-rupee-price-for-your-app" tabindex="-1"><a class="header-anchor" href="#try-a-fixed-rupee-price-for-your-app">Try a fixed rupee price for your app</a></h2>
<p>Deploy a GitHub repository to Voroa on the free size and see the paid sizes in rupees before you
upgrade. <a href="/docs/deploy-from-github/">Deploy your first app from GitHub</a> takes a few minutes.</p>
<p><em>Last checked: 14 October 2026, against Railway’s pricing, free trial, usage limits and regions
documentation. Still comparing? Read our <a href="/blog/railway-alternative-india/">Railway alternative guide for India</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Vercel Pricing in India 2026: Plans and Real Cost in INR</title>
      <link>https://getvoroa.com/blog/vercel-pricing-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/vercel-pricing-india/</guid>
      <pubDate>Wed, 14 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <description><![CDATA[Vercel pricing explained for India: Hobby vs Pro, per-seat and usage costs, what a dollar bill costs in rupees with GST and forex, plus three worked bills.]]></description>
      <content:encoded><![CDATA[<p>Vercel pricing has two headline numbers: $0 for Hobby and $20 a month for Pro. What you pay from
India is built from more parts than that: the Pro platform fee, a fee for every extra developer, usage
beyond a $20 monthly credit, any add-ons, GST on the invoice, and the forex charge your card adds.</p>
<p>This
guide lists every rate that matters, uses the Mumbai region’s prices, and works three bills through to
the rupee amount on your card statement.</p>
<p>All figures are from Vercel’s official <a href="https://vercel.com/docs/pricing">pricing documentation</a>,
checked on 14 October 2026. Vercel changed its plans twice in the last year, so re-check the rates
before you budget.</p>
<h2 id="vercel-pricing-plans-at-a-glance-usd-and-inr" tabindex="-1"><a class="header-anchor" href="#vercel-pricing-plans-at-a-glance-usd-and-inr">Vercel pricing plans at a glance (USD and INR)</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Hobby</th>
<th>Pro</th>
<th>Enterprise</th>
</tr>
</thead>
<tbody>
<tr>
<td>Price</td>
<td>$0</td>
<td>$20 per month platform fee</td>
<td>Custom contract</td>
</tr>
<tr>
<td>In rupees, before tax (Rs 95.20 per $)</td>
<td>Rs 0</td>
<td>Rs 1,904</td>
<td>Custom</td>
</tr>
<tr>
<td>Developer seats</td>
<td>1</td>
<td>1 included, then $20 per month each</td>
<td>Custom</td>
</tr>
<tr>
<td>Viewer seats (read-only)</td>
<td>Not available</td>
<td>Unlimited, free</td>
<td>Custom</td>
</tr>
<tr>
<td>Usage</td>
<td>Fixed free allowances</td>
<td>$20 monthly credit, then pay per use</td>
<td>Custom</td>
</tr>
<tr>
<td>Commercial use</td>
<td>Not allowed</td>
<td>Allowed</td>
<td>Allowed</td>
</tr>
<tr>
<td>Deployments per day</td>
<td>100</td>
<td>6,000</td>
<td>Custom</td>
</tr>
<tr>
<td>Concurrent builds</td>
<td>1</td>
<td>Up to 500 with on-demand concurrency</td>
<td>Custom</td>
</tr>
<tr>
<td>Runtime logs kept</td>
<td>1 hour</td>
<td>1 day</td>
<td>Custom</td>
</tr>
</tbody>
</table></div>
<h2 id="is-the-hobby-plan-free-for-commercial-use" tabindex="-1"><a class="header-anchor" href="#is-the-hobby-plan-free-for-commercial-use">Is the Hobby plan free for commercial use?</a></h2>
<p>No. The Vercel Hobby plan is free, but commercial use is not allowed on it. Two rules catch Indian
teams most often. First, Hobby is for personal, non-commercial use
only. Vercel counts a site as commercial if anyone involved earns from it, including a freelancer paid
to build it, a site that takes payments, or one that shows ads.</p>
<p>Second, a Hobby account cannot deploy
a private repository owned by a GitHub organisation. A college project in your personal account is
fine; a client’s site is not.</p>
<p>Within those rules, Hobby comes with these monthly allowances:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Resource</th>
<th>Hobby allowance per month</th>
</tr>
</thead>
<tbody>
<tr>
<td>Fast Data Transfer (bandwidth to visitors)</td>
<td>100 GB</td>
</tr>
<tr>
<td>CDN requests</td>
<td>1,000,000</td>
</tr>
<tr>
<td>Function invocations</td>
<td>1,000,000</td>
</tr>
<tr>
<td>Active CPU</td>
<td>4 hours</td>
</tr>
<tr>
<td>Provisioned memory</td>
<td>360 GB-hours</td>
</tr>
<tr>
<td>Fast Origin Transfer</td>
<td>10 GB</td>
</tr>
<tr>
<td>Image transformations</td>
<td>5,000</td>
</tr>
<tr>
<td>Blob storage</td>
<td>1 GB</td>
</tr>
</tbody>
</table></div>
<p>Hobby never sends a bill. When you cross a limit, Vercel stops that feature instead. In most cases you
wait until 30 days have passed before you can use it again, or you upgrade. For a portfolio that is a
fair trade. For anything your income depends on, it means the site can go down on a busy day.</p>
<h2 id="vercel-pricing-on-pro-per-seat-fee-and-usage-credit" tabindex="-1"><a class="header-anchor" href="#vercel-pricing-on-pro-per-seat-fee-and-usage-credit">Vercel pricing on Pro: per-seat fee and usage credit</a></h2>
<p>The $20 Pro platform fee buys three things: one deploying seat, a $20 usage credit, and the
included CDN tier. The credit is per team, not per seat. It is spent from the first unit of usage, it
expires at the end of each month, and once it is used up, usage is billed on demand.</p>
<p>Every additional person who deploys or manages projects is a paid seat at $20 a month. So the
Vercel Pro price for a team of four developers is $80 a month before any usage. Viewer seats are free, so a client or a
manager who only checks previews and analytics does not add to the bill.</p>
<h3 id="flat-rate-cdn-included-and-paid-tiers" tabindex="-1"><a class="header-anchor" href="#flat-rate-cdn-included-and-paid-tiers">Flat Rate CDN, included and paid tiers</a></h3>
<p>Since 8 September 2026, new Pro teams are on <strong>Flat Rate CDN</strong> by default. Pro includes the lowest
tier, and you move up a tier only if a whole month’s usage stays above it:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>CDN tier</th>
<th>Price per month</th>
<th>CDN requests</th>
<th>Data transfer</th>
</tr>
</thead>
<tbody>
<tr>
<td>Included with Pro</td>
<td>$0</td>
<td>1 million</td>
<td>1 TB</td>
</tr>
<tr>
<td>Flat Rate 10M</td>
<td>$20</td>
<td>10 million</td>
<td>50 TB</td>
</tr>
<tr>
<td>Flat Rate 50M</td>
<td>$100</td>
<td>50 million</td>
<td>50 TB</td>
</tr>
<tr>
<td>Flat Rate 150M</td>
<td>$300</td>
<td>150 million</td>
<td>50 TB</td>
</tr>
</tbody>
</table></div>
<p>A one-day traffic spike does not change the tier. One thing to watch: the main pricing page still
lists 10 million CDN requests for Pro, while the Flat Rate CDN documentation lists 1 million for the
included tier. Teams created before the change may also still be on the older metered model. Your
team’s <strong>Usage</strong> page shows which one applies to you.</p>
<h3 id="usage-rates-in-the-mumbai-region" tabindex="-1"><a class="header-anchor" href="#usage-rates-in-the-mumbai-region">Usage rates in the Mumbai region</a></h3>
<p>Most usage on Vercel is priced by region, and functions run in Washington, D.C. (<code>iad1</code>) unless you
change the project’s function region. Running near Indian users in Mumbai (<code>bom1</code>) costs a little
more per unit. These are Vercel’s <a href="https://vercel.com/docs/pricing/regional-pricing/bom1">Mumbai regional rates</a>
next to the Washington ones:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Resource</th>
<th>Mumbai (bom1)</th>
<th>Washington (iad1)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Active CPU</td>
<td>$0.14 per hour</td>
<td>$0.128 per hour</td>
</tr>
<tr>
<td>Provisioned memory</td>
<td>$0.0116 per GB-hour</td>
<td>$0.0106 per GB-hour</td>
</tr>
<tr>
<td>Function invocations</td>
<td>$0.60 per million</td>
<td>$0.60 per million</td>
</tr>
<tr>
<td>Fast Origin Transfer</td>
<td>$0.25 per GB</td>
<td>$0.06 per GB</td>
</tr>
<tr>
<td>Fast Data Transfer (outside a flat tier)</td>
<td>$0.20 per GB</td>
<td>$0.15 per GB</td>
</tr>
<tr>
<td>CDN requests (outside a flat tier)</td>
<td>$2.20 per million</td>
<td>$2.00 per million</td>
</tr>
<tr>
<td>ISR writes</td>
<td>$4.40 per million</td>
<td>$4.00 per million</td>
</tr>
<tr>
<td>Image transformations</td>
<td>$0.0527 per 1,000</td>
<td>$0.05 per 1,000</td>
</tr>
<tr>
<td>Blob storage</td>
<td>$0.025 per GB-month</td>
<td>$0.023 per GB-month</td>
</tr>
</tbody>
</table></div>
<p>The row that surprises people is Fast Origin Transfer: the data your functions send to Vercel’s CDN.
It has no included amount on Pro, and in Mumbai it costs about four times the Washington rate. An app
that renders most pages in a function pays it on every uncached response.</p>
<p>Functions are billed in two parts. Active CPU counts only the time your code is actually computing,
not the time it waits for a database or an API. Provisioned memory is billed for as long as an
instance is alive, waiting included. The default instance is 1 vCPU with 2 GB of memory.</p>
<h3 id="build-minutes-the-line-most-teams-never-check" tabindex="-1"><a class="header-anchor" href="#build-minutes-the-line-most-teams-never-check">Build minutes, the line most teams never check</a></h3>
<p>Hobby builds on a basic machine for free. On Pro, builds are billed: new teams use <strong>Elastic</strong> build
machines by default, charged at $0.0035 per vCPU per minute. A 4-vCPU Standard machine therefore costs
$0.014 a minute, an 8-vCPU Enhanced machine $0.028, and a 30-vCPU Turbo machine $0.105.</p>
<p>A team that pushes 300 two-minute builds a month on a Standard machine spends about $8.40, inside the credit.</p>
<p>Several teams on Vercel’s community forum have reported invoices of $180 to $660 from build minutes on
large machines they did not expect to be using, so set the machine type yourself if your builds are
small.</p>
<h3 id="add-ons-that-are-billed-on-top" tabindex="-1"><a class="header-anchor" href="#add-ons-that-are-billed-on-top">Add-ons that are billed on top</a></h3>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Add-on</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>Extra developer seat</td>
<td>$20 per month</td>
</tr>
<tr>
<td>Password Protection</td>
<td>$20 per month per protected project</td>
</tr>
<tr>
<td>Static IPs</td>
<td>$100 per month per project, plus private data transfer</td>
</tr>
<tr>
<td>SAML single sign-on</td>
<td>$300 per month</td>
</tr>
<tr>
<td>HIPAA BAA</td>
<td>$350 per month</td>
</tr>
<tr>
<td>Observability Plus</td>
<td>$1.20 per million events</td>
</tr>
<tr>
<td>Web Analytics Plus</td>
<td>$10 per month</td>
</tr>
<tr>
<td>Preview Deployment Suffix</td>
<td>$100 per month</td>
</tr>
</tbody>
</table></div>
<p>Password Protection changed in September 2026 from a $150 team add-on to $20 per project. A team
protecting eight or more client previews now pays more than before.</p>
<h3 id="spend-management-alerts-by-default-pausing-only-if-you-ask" tabindex="-1"><a class="header-anchor" href="#spend-management-alerts-by-default-pausing-only-if-you-ask">Spend Management: alerts by default, pausing only if you ask</a></h3>
<p>New Pro teams get spend alerts at $200 of on-demand usage per billing cycle. You can change the amount
and, optionally, turn on <strong>Pause Production Deployments</strong>.</p>
<p>Two limits apply. The spend amount counts
only metered usage above the credit, not seats or add-ons. And Vercel checks spend every few minutes,
so usage continues for a short while after you cross the line.</p>
<p>With pausing on, visitors see a 503 page
until you resume each project by hand.</p>
<h2 id="vercel-pricing-on-an-indian-card-gst-and-forex" tabindex="-1"><a class="header-anchor" href="#vercel-pricing-on-an-indian-card-gst-and-forex">Vercel pricing on an Indian card: GST and forex</a></h2>
<p>Vercel prices and bills in US dollars, and its prices exclude GST. Its
<a href="https://vercel.com/docs/pricing/taxes">tax page</a> says it began collecting VAT, GST or the local
equivalent from international customers on invoices issued from 1 April 2026, as a separate line,
where it is registered to collect. Three things then decide the rupee amount:</p>
<ul>
<li><strong>GST.</strong> Indian GST on an online service from abroad is 18%. If your Vercel invoice carries it, add
18%. If you are a GST-registered business and the invoice does not, you usually pay the 18% yourself
under reverse charge and claim it back as input tax credit. The steps are in our guide to
<a href="/docs/reverse-charge-foreign-cloud-hosting/">reverse charge on foreign hosting</a>; check your own case
with your accountant.</li>
<li><strong>The exchange rate</strong> on the day the card is charged. We use Rs 95.20 to the dollar, the mid-market
rate on 14 October 2026.</li>
<li><strong>The card’s forex markup.</strong> Many Indian cards charge 3.5% on foreign-currency spends, plus 18% GST
on that fee: about 4.13% in total. Some cards now advertise zero markup, so check yours.</li>
</ul>
<p>Two more points that only apply in India:</p>
<ol>
<li><strong>Recurring card payments above Rs 15,000.</strong> Under the RBI’s Digital Payments - E-mandate Framework, 2026
(issued 21 April 2026), an automatic card payment above Rs 15,000 needs your approval each time. Bill 3 below crosses that line, so it
can wait for an OTP, or fail if nobody approves it.</li>
<li><strong>Declined Indian cards.</strong> Since late 2024, Indian users have reported on Vercel’s community forum
that recurring charges on Indian cards are declined by their banks. Vercel staff suggested paying
the invoice manually from billing settings. Make sure your card is enabled for international and
recurring payments before an invoice is due.</li>
</ol>
<h2 id="vercel-pricing-in-practice-three-worked-monthly-bills" tabindex="-1"><a class="header-anchor" href="#vercel-pricing-in-practice-three-worked-monthly-bills">Vercel pricing in practice: three worked monthly bills</a></h2>
<p><img src="https://media.getvoroa.com/img/85b90ed56276201f.png" alt="Diagram: a Vercel Pro bill is the $20 platform fee, plus $20 for each extra developer, plus usage above the $20 credit, plus a higher CDN tier and add-ons, then GST on the invoice and the forex markup your card adds" width="700" loading="eager" decoding="async"></p>
<p>The monthly Pro bill, before tax, is:</p>
<ol>
<li><strong>$20</strong> platform fee</li>
<li><strong>+ $20</strong> for each developer seat after the first</li>
<li><strong>+ usage</strong> at the rates above, <strong>minus the $20 credit</strong></li>
<li><strong>+ a Flat Rate CDN tier</strong> if a month’s traffic stays above 1 million requests or 1 TB</li>
<li><strong>+ add-ons</strong> you have switched on</li>
</ol>
<p>The usage figures below are assumptions for illustration, with functions in Mumbai. Your own numbers
are on the Usage page.</p>
<p><strong>Bill 1: a freelancer’s client site.</strong> One developer, light traffic, usage inside the credit.
Total: <strong>$20.00</strong>.</p>
<p><strong>Bill 2: a three-person startup.</strong> Usage: 50 hours of active CPU ($7.00), 600 GB-hours of memory
($6.96), 3 million invocations ($1.80), 20 GB of Fast Origin Transfer ($5.00) and 2,400 build CPU
minutes ($8.40). That is $29.16 of usage, or $9.16 after the credit.
Total: 20 + 40 + 9.16 = <strong>$69.16</strong>.</p>
<p><strong>Bill 3: a growing product.</strong> Three developers, about 4 million requests a month (so the $20 CDN
tier), and one password-protected staging project. Usage: 200 hours of active CPU ($28.00),
2,400 GB-hours of memory ($27.84), 10 million invocations ($6.00), 100 GB of Fast Origin Transfer
($25.00) and 6,000 build CPU minutes ($21.00): $107.84, or $87.84 after the credit.
Total: 20 + 40 + 20 + 87.84 + 20 = <strong>$187.84</strong>.</p>
<p>In rupees, using the GST and forex steps above, the three bills come to:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Bill</th>
<th>At Rs 95.20</th>
<th>Plus 18% GST</th>
<th>Plus 4.13% card charges</th>
<th>Approx. on your card</th>
</tr>
</thead>
<tbody>
<tr>
<td>$20.00</td>
<td>Rs 1,904</td>
<td>Rs 2,247</td>
<td>Rs 93</td>
<td>Rs 2,340</td>
</tr>
<tr>
<td>$69.16</td>
<td>Rs 6,584</td>
<td>Rs 7,769</td>
<td>Rs 321</td>
<td>Rs 8,090</td>
</tr>
<tr>
<td>$187.84</td>
<td>Rs 17,882</td>
<td>Rs 21,101</td>
<td>Rs 871</td>
<td>Rs 21,973</td>
</tr>
</tbody>
</table></div>
<p>If your invoice has no Indian GST and you are not registered, leave out the GST column: the first
bill then comes to about Rs 1,983.</p>
<h2 id="when-a-long-running-server-is-cheaper-than-vercel" tabindex="-1"><a class="header-anchor" href="#when-a-long-running-server-is-cheaper-than-vercel">When a long-running server is cheaper than Vercel</a></h2>
<p>Vercel is built for frontends and functions. If what you run is a Node.js server, an API or a
full-stack app that should stay running, compare it with a host that bills in rupees. If
Railway is on your list, our <a href="/blog/railway-pricing-india/">Railway pricing guide</a> works the same
bills through to rupees.</p>
<p><a href="/web-services/">Voroa’s web services</a> run in Mumbai as long-running containers. A bill has two parts:
a workspace plan (Free, or Growth at Rs 499 per month) and a size for each service, with the smallest
always-on size at Rs 298 a month.</p>
<p>Prices exclude 18% GST, and every payment gets a GST invoice, paid by
UPI, an Indian card or netbanking. There is no dollar conversion and no forex markup.</p>
<p>The Free plan has one member and Growth allows up to five, so for a team of three, Growth plus one
Starter service comes to Rs 797 before GST, or about Rs 940 with
it.</p>
<p>That is not a like-for-like swap: Voroa does not run Vercel’s edge functions, ISR or image
optimisation, and bandwidth allowances differ by plan. The full list is on the <a href="/pricing/">pricing page</a>.
Read <a href="/blog/vercel-alternative-india/">when a Vercel alternative makes sense</a> before you move anything.</p>
<h2 id="vercel-pricing-faq" tabindex="-1"><a class="header-anchor" href="#vercel-pricing-faq">Vercel pricing FAQ</a></h2>
<h3 id="is-vercel-free-for-commercial-use" tabindex="-1"><a class="header-anchor" href="#is-vercel-free-for-commercial-use">Is Vercel free for commercial use?</a></h3>
<p>Hobby is free for personal, non-commercial projects, within fixed monthly allowances. Any commercial
site, including one built for a paying client, needs Pro or Enterprise.</p>
<h3 id="is-vercel-pro-20-per-user" tabindex="-1"><a class="header-anchor" href="#is-vercel-pro-20-per-user">Is Vercel Pro $20 per user?</a></h3>
<p>The $20 platform fee includes one developer seat and a $20 usage credit for the whole team. Each extra
developer is $20 a month and adds no extra credit. Viewer seats are free.</p>
<h3 id="does-vercel-charge-gst-in-india" tabindex="-1"><a class="header-anchor" href="#does-vercel-charge-gst-in-india">Does Vercel charge GST in India?</a></h3>
<p>Vercel’s prices exclude tax. Since 1 April 2026 it adds VAT, GST or the equivalent as a separate
invoice line where it is registered to collect it. A GST-registered business that is not charged
usually pays it under reverse charge. A <a href="/docs/gst-invoice-cloud-hosting/">GST invoice for cloud hosting</a>
explains what a valid Indian invoice must show.</p>
<h3 id="why-is-my-vercel-bill-more-than-20" tabindex="-1"><a class="header-anchor" href="#why-is-my-vercel-bill-more-than-20">Why is my Vercel bill more than $20?</a></h3>
<p>Usually one of these: extra developer seats, build minutes on a large machine, Fast Origin Transfer
from functions, a higher Flat Rate CDN tier, or an add-on such as Password Protection. Your invoice
lists each one.</p>
<h3 id="what-is-vercel-pricing-in-rupees" tabindex="-1"><a class="header-anchor" href="#what-is-vercel-pricing-in-rupees">What is Vercel pricing in rupees?</a></h3>
<p>Vercel bills in US dollars, not rupees. The $20 Pro fee is about Rs 1,904 at Rs 95.20 to the
dollar, and about Rs 2,340 once 18% GST and a typical card markup are added. If paying
in rupees with a GST invoice matters to you, our <a href="/blog/supabase-pricing-india/">Supabase pricing</a>
guide shows the same dollar-to-rupee working for a database bill.</p>
<h2 id="leaving-vercel-for-a-rupee-bill" tabindex="-1"><a class="header-anchor" href="#leaving-vercel-for-a-rupee-bill">Leaving Vercel for a rupee bill</a></h2>
<p>A Vercel Pro team in India pays in dollars every month, and the GST and forex steps above come with it. Voroa bills in
rupees from an Indian entity, with GST on the invoice, so a registered business takes the input tax
credit straight from it. There is no reverse-charge GST to pay in cash first, and no card forex markup.</p>
<p>Moving usually costs a month of two bills. To take that off the table, you can apply for
<a href="/migration-credits/">migration credits</a>: up to Rs 10,000 set against your Voroa bills, granted by hand
after a person reads your application. Applying is not the same as being approved.</p>
<p>The full comparison of a dollar bill and a rupee bill is in
<a href="/docs/cloud-hosting-inr-vs-usd/">INR vs USD cloud hosting</a>. Paying Netlify instead? The same sums for its
credit plans are in <a href="/blog/netlify-pricing-india/">Netlify pricing in India</a>.</p>
<h2 id="get-a-rupee-price-for-your-app" tabindex="-1"><a class="header-anchor" href="#get-a-rupee-price-for-your-app">Get a rupee price for your app</a></h2>
<p>You can deploy a GitHub repository to Voroa on the free size and see what the paid sizes cost in
rupees before you upgrade. <a href="/docs/deploy-from-github/">Deploy your first app from GitHub</a> takes a few
minutes, and every size is listed on <a href="/pricing/">Voroa pricing in INR</a>.</p>
<p><em>Last checked: 14 October 2026, against Vercel’s pricing, regional pricing and tax documentation. Still
deciding? Read our <a href="/blog/vercel-alternative-india/">Vercel alternative guide for India</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Postgres Create User: Logins, Grants and Read-Only Access</title>
      <link>https://getvoroa.com/blog/postgres-create-user/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/postgres-create-user/</guid>
      <pubDate>Sun, 11 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Security</category>
      <description><![CDATA[Postgres create user, step by step: the SQL for a login, the grants it needs, a read-only user, a login per app, and why the owner skips row-level security.]]></description>
      <content:encoded><![CDATA[<p>Postgres create user is only half the job: a new login also needs the grants that decide what it can touch.</p>
<p>Most guides stop after <code>CREATE USER</code>, and the new login then fails with “permission denied” on its first query.</p>
<p>This guide covers the full path - the login, the grants, a read-only user for a reporting tool, a separate login per application, and the one detail that catches teams who turn on row-level security: the database owner is not limited by it.</p>
<p>Every command here is plain PostgreSQL and is described in the official <a href="https://www.postgresql.org/docs/current/sql-createrole.html">CREATE ROLE reference</a>.</p>
<h2 id="a-user-is-a-role-that-can-log-in" tabindex="-1"><a class="header-anchor" href="#a-user-is-a-role-that-can-log-in">A user is a role that can log in</a></h2>
<p>PostgreSQL has one object for both users and groups: the role.</p>
<p><code>CREATE USER</code> is <code>CREATE ROLE</code> with <code>LOGIN</code> switched on, so these two lines do the same thing:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">CREATE USER reporting WITH PASSWORD 'a-long-random-password';
CREATE ROLE reporting WITH LOGIN PASSWORD 'a-long-random-password';
</code></pre>
</div>
<p>Use a generated password, not one you type.</p>
<p>Since PostgreSQL 14 the default storage is SCRAM, which never keeps the password itself - only a verifier.</p>
<p>Avoid <code>SUPERUSER</code> and <code>CREATEROLE</code> for anything an application or a tool uses: a superuser bypasses every permission check, including the ones this guide sets up.</p>
<h2 id="the-grants-a-new-login-needs-before-it-can-read-anything" tabindex="-1"><a class="header-anchor" href="#the-grants-a-new-login-needs-before-it-can-read-anything">The grants a new login needs before it can read anything</a></h2>
<p>A fresh login can connect to a database only if <code>PUBLIC</code> still has <code>CONNECT</code>, and it can read nothing.</p>
<p>Privileges are layered - database, then schema, then table - and a missing layer gives the same “permission denied” error as a missing table grant:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">GRANT CONNECT ON DATABASE appdb TO reporting;
GRANT USAGE ON SCHEMA public TO reporting;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO reporting;
</code></pre>
</div>
<p>The third line covers tables that exist today and nothing created tomorrow.</p>
<p>For future tables, add a default privilege, run as the role that creates the tables:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO reporting;
</code></pre>
</div>
<p>The full list of object types and privileges is in the <a href="https://www.postgresql.org/docs/current/sql-grant.html">GRANT reference</a>.</p>
<p><img src="https://media.getvoroa.com/img/386ba68686807456.png" alt="Diagram: a new Postgres user needs three grant layers - CONNECT on the database, USAGE on the schema, then SELECT or write privileges on the tables" width="700" loading="eager" decoding="async"></p>
<h2 id="a-read-only-login-for-a-dashboard-or-an-analyst" tabindex="-1"><a class="header-anchor" href="#a-read-only-login-for-a-dashboard-or-an-analyst">A read-only login for a dashboard or an analyst</a></h2>
<p>The three grants above give <code>reporting</code> <code>SELECT</code> and nothing else, so an <code>UPDATE</code> or <code>DROP</code> from a dashboard fails.</p>
<p>On PostgreSQL 14 and older there is one gap: every role can create tables in the <code>public</code> schema by default.</p>
<p>PostgreSQL 15 removed that default.</p>
<p>On 14, close it once:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">REVOKE CREATE ON SCHEMA public FROM PUBLIC;
</code></pre>
</div>
<p>Two more habits keep it read-only.</p>
<p>Do not grant it membership in your application’s role, because membership carries every privilege of that role.</p>
<p>And give it a short <code>statement_timeout</code>, so one slow chart cannot hold locks for minutes:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">ALTER ROLE reporting SET statement_timeout = '30s';
</code></pre>
</div>
<h2 id="one-login-per-application-not-one-shared-password" tabindex="-1"><a class="header-anchor" href="#one-login-per-application-not-one-shared-password">One login per application, not one shared password</a></h2>
<p>When three services and a laptop share the owner login, you cannot revoke one of them, you cannot see which one ran a query, and rotating the password means redeploying everything at once.</p>
<p>A login per application fixes all three.</p>
<p>It also gives you a rotation path with no downtime: create <code>orders_v2</code> with the same grants and default privileges as <code>orders</code>, move the service to it, confirm it works, then drop <code>orders</code>.</p>
<p>Both logins work during the switch, so nothing is locked out.</p>
<p>To remove a login that owns tables, hand its objects over first, or <code>DROP ROLE</code> refuses:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">REASSIGN OWNED BY orders TO app_owner;
DROP OWNED BY orders;
DROP ROLE orders;
</code></pre>
</div>
<h2 id="row-level-security-does-not-apply-to-the-owner" tabindex="-1"><a class="header-anchor" href="#row-level-security-does-not-apply-to-the-owner">Row-level security does not apply to the owner</a></h2>
<p>This is the step that surprises teams building multi-tenant apps.</p>
<p>Row-level security policies apply to ordinary roles, but the table owner skips them unless the table is set to <code>FORCE ROW LEVEL SECURITY</code>, and a superuser always skips them.</p>
<p>If your application connects as the role that created the tables, your policies silently do nothing.</p>
<p>The fix is a separate login for the application that owns nothing, with the tenant passed inside each transaction so it cannot leak to the next request on a pooled connection:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">BEGIN;
SELECT set_config('app.tenant', 'acme', true);  -- true = this transaction only
SELECT * FROM invoices;                         -- the policy filters by app.tenant
COMMIT;
</code></pre>
</div>
<p>The PostgreSQL <a href="https://www.postgresql.org/docs/current/ddl-rowsecurity.html">row security guide</a> covers policy syntax in detail.</p>
<h2 id="when-a-pooler-refuses-the-new-login" tabindex="-1"><a class="header-anchor" href="#when-a-pooler-refuses-the-new-login">When a pooler refuses the new login</a></h2>
<p>A connection pooler such as PgBouncer authenticates clients itself before PostgreSQL sees them.</p>
<p>If it reads users only from its own file, a login you just created fails at the pooler with “no such user”, while the owner keeps working - and nothing appears in the PostgreSQL log, because the connection never got that far.</p>
<p>The pooler has to look users up in PostgreSQL instead.</p>
<p>If you see that error right after creating a login, check the pooler before you check your grants.</p>
<h2 id="adding-a-user-on-voroa" tabindex="-1"><a class="header-anchor" href="#adding-a-user-on-voroa">Adding a user on Voroa</a></h2>
<p>On a <a href="/managed-postgresql-india/">managed PostgreSQL database on Voroa</a> you can add a login from the dashboard and get its connection URL once, or run <code>CREATE USER</code> yourself.</p>
<p>A login added from the dashboard can read and write all data and create tables. For a read-only login, create it with SQL as shown above.</p>
<p>A login you create with SQL connects through the same address as the main user when it has a SCRAM password (the default since PostgreSQL 14), is not a replication role, and its name does not start with <code>voroa_</code>.</p>
<p>That connection shares server connections between transactions, so use <code>SET LOCAL</code> inside a transaction rather than a session-wide <code>SET</code>.</p>
<p>The <a href="/docs/postgres-database-users/">database users guide</a> explains both, including which logins the dashboard lists.</p>
<p>Your <a href="/docs/postgresql-creating-connecting/">connection details</a> show the address to use, and <a href="/docs/connect-postgres-externally/">connecting from outside</a> covers tools on your own machine.</p>
<p>If you are moving from another provider, our guide to <a href="/blog/supabase-backup-guide/">backups on Supabase</a> and our comparison of <a href="/blog/neon-vs-supabase-india/">Neon and Supabase for teams in India</a> cover what to check before you switch.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<p><strong>What is the difference between CREATE USER and CREATE ROLE?</strong>
Only the default: <code>CREATE USER</code> adds <code>LOGIN</code>.</p>
<p>Everything else is the same command.</p>
<p><strong>Why does my new user get “permission denied”?</strong>
It is usually a missing <code>USAGE ON SCHEMA</code>, not a missing table grant.</p>
<p>Check all three layers.</p>
<p><strong>Can I list users?</strong>
Yes.</p>
<p>In <code>psql</code>, run <code>\du</code>, or query <code>pg_roles</code>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Firebase Alternatives in India: Where to Go and How to Move</title>
      <link>https://getvoroa.com/blog/firebase-alternatives-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/firebase-alternatives-india/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Migration</category>
      <description><![CDATA[Firebase alternatives for Indian developers: why teams leave, which option fits which app, and how to move Firestore data and Firebase Auth users to PostgreSQL.]]></description>
      <content:encoded><![CDATA[<p>Developers look at Firebase alternatives once their app grows past the prototype stage. Firebase is
fast to start with, but its document database, pay-per-operation billing and Google-only stack start
to pinch as the data becomes relational and the bill becomes real. This guide gives the common
reasons to leave, with dates and sources, compares the main alternatives honestly, and shows how a
move works step by step, including a halfway option where you keep part of Firebase.</p>
<h2 id="four-reasons-teams-outgrow-firebase" tabindex="-1"><a class="header-anchor" href="#four-reasons-teams-outgrow-firebase">Four reasons teams outgrow Firebase</a></h2>
<ol>
<li><strong>The data became relational.</strong> Firestore has no joins. When users, orders and payments start to
reference each other, you either run many queries or copy data into many documents and keep every
copy in sync.</li>
<li><strong>The bill is hard to predict.</strong> The Blaze plan charges per read, write and gigabyte, and has no
hard spending cap. Budget alerts notify you; they do not stop charges.</li>
<li><strong>Storage moved behind billing.</strong> Firebase’s own
<a href="https://firebase.google.com/docs/storage/faqs-storage-changes-announced-sept-2024">Cloud Storage FAQ</a>
says that from February 2026, keeping access to your default storage bucket requires the paid Blaze
plan, even if your usage stays inside the free allowance.</li>
<li><strong>Products get retired.</strong> According to the
<a href="https://firebase.google.com/support/dynamic-links-faq">Dynamic Links FAQ</a>, Firebase Dynamic Links
shut down on 25 August 2025, and every link they served started returning a 404. Apps that used them
for referrals and deep links had to rebuild those flows.</li>
</ol>
<h2 id="the-main-alternatives-compared" tabindex="-1"><a class="header-anchor" href="#the-main-alternatives-compared">The main alternatives, compared</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Database</th>
<th>Built-in auth</th>
<th>Self-host</th>
<th>Billing</th>
</tr>
</thead>
<tbody>
<tr>
<td>Supabase</td>
<td>PostgreSQL</td>
<td>Yes</td>
<td>Yes</td>
<td>USD</td>
</tr>
<tr>
<td>Appwrite</td>
<td>Several, including PostgreSQL on dedicated compute</td>
<td>Yes</td>
<td>Yes</td>
<td>USD</td>
</tr>
<tr>
<td>PocketBase</td>
<td>SQLite, in one file</td>
<td>Yes</td>
<td>Only self-hosted</td>
<td>Free software; you pay for the server</td>
</tr>
<tr>
<td>Neon</td>
<td>Serverless PostgreSQL</td>
<td>Newer, limited</td>
<td>Open source, but not packaged for self-hosting</td>
<td>USD</td>
</tr>
<tr>
<td>Nhost</td>
<td>PostgreSQL with a GraphQL API</td>
<td>Yes</td>
<td>Yes</td>
<td>USD</td>
</tr>
<tr>
<td>Convex</td>
<td>Its own reactive document database</td>
<td>Convex Auth or integrations</td>
<td>Yes, open-source backend</td>
<td>USD</td>
</tr>
<tr>
<td>AWS Amplify</td>
<td>DynamoDB by default</td>
<td>Yes, through Cognito</td>
<td>No</td>
<td>INR for Indian accounts, through AWS India</td>
</tr>
<tr>
<td>Managed PostgreSQL with your own backend</td>
<td>PostgreSQL</td>
<td>Your code or a library</td>
<td>Depends on host</td>
<td>Depends on host; INR on Voroa</td>
</tr>
</tbody>
</table></div>
<h3 id="supabase" tabindex="-1"><a class="header-anchor" href="#supabase">Supabase</a></h3>
<p>The closest like-for-like replacement: a database, auth, storage, realtime and functions in one
platform, but on PostgreSQL. It suits apps whose data is relational and that want to keep a
“no backend” setup. Its trade-offs from India are covered in our
<a href="/blog/supabase-vs-firebase-india/">Supabase vs Firebase comparison</a>.</p>
<h3 id="appwrite" tabindex="-1"><a class="header-anchor" href="#appwrite">Appwrite</a></h3>
<p>An open-source backend with auth, databases, storage and functions, which you can self-host or use as a
cloud service. Its cloud defaults to its own TablesDB, and its dedicated compute tiers offer other database types, including PostgreSQL. A good fit
if you want Firebase’s all-in-one feel without depending on Google.</p>
<h3 id="pocketbase" tabindex="-1"><a class="header-anchor" href="#pocketbase">PocketBase</a></h3>
<p>A single Go program with an embedded SQLite database, auth, files and an admin screen. It is excellent
for small apps and internal tools. Its own documentation warns that it is not yet recommended for
production-critical apps before version 1.0, and that backward compatibility is not guaranteed until
then, so plan for manual upgrades.</p>
<h3 id="neon" tabindex="-1"><a class="header-anchor" href="#neon">Neon</a></h3>
<p>Serverless PostgreSQL with branching and scale-to-zero. It replaces the database only; you bring your
own backend. We compare it in <a href="/blog/neon-vs-supabase-india/">Neon vs Supabase</a>.</p>
<h3 id="managed-postgresql-and-your-own-backend" tabindex="-1"><a class="header-anchor" href="#managed-postgresql-and-your-own-backend">Managed PostgreSQL and your own backend</a></h3>
<p>If you are comfortable writing a Node.js, Django or FastAPI server, a plain PostgreSQL database plus
your own API gives the most control and the least lock-in. This is the path the rest of this guide
uses for the migration steps.</p>
<h2 id="a-halfway-option-keep-firebase-auth-and-push" tabindex="-1"><a class="header-anchor" href="#a-halfway-option-keep-firebase-auth-and-push">A halfway option: keep Firebase Auth and push</a></h2>
<p>You do not have to leave Firebase all at once. A common, low-risk pattern is:</p>
<ul>
<li>keep <strong>Firebase Auth</strong> for login, and <strong>Cloud Messaging</strong> for push notifications;</li>
<li>move the <strong>data</strong> to PostgreSQL behind your own API;</li>
<li>in your API, check each request’s Firebase ID token with the Admin SDK, as described in Google’s
guide to <a href="https://firebase.google.com/docs/auth/admin/verify-id-tokens">verifying ID tokens</a>, and use
the user ID it returns in your SQL.</li>
</ul>
<p>Users keep logging in exactly as before, and you remove the part of Firebase that was causing the pain.
Later, you can move auth too, or never.</p>
<h2 id="how-to-move-firestore-data-to-postgresql" tabindex="-1"><a class="header-anchor" href="#how-to-move-firestore-data-to-postgresql">How to move Firestore data to PostgreSQL</a></h2>
<p><img src="https://media.getvoroa.com/img/7eeac5dc82319169.png" alt="Diagram: export Firestore to JSON, design tables and foreign keys, transform and load the data, then point the app at PostgreSQL" width="700" loading="eager" decoding="async"></p>
<p>Moving a document database to SQL is a redesign, not a copy:</p>
<ol>
<li><strong>Export with the Admin SDK.</strong> Firestore’s managed export writes files to Cloud Storage that only
Firestore and BigQuery can read back, and it needs the Blaze plan. For PostgreSQL, a short script
with the Admin SDK that reads each collection and writes JSON is usually simpler.</li>
<li><strong>Design the tables.</strong> Each collection usually becomes a table. Nested objects become their own
tables with a foreign key. Arrays of references become join tables. Fields you copied into many
documents go back to living in one place.</li>
<li><strong>Transform and load.</strong> Reshape the JSON to the new tables and insert it in batches. Load parent
tables first so foreign keys resolve, and keep Firestore document IDs in a column so you can trace
every row back.</li>
<li><strong>Switch reads, then writes.</strong> Point reads at PostgreSQL first and compare results with Firestore
for a few days. Then move writes, and only then turn Firestore off.</li>
</ol>
<p>A small example of step 2. A Firestore <code>orders</code> document that embeds the customer’s name and a list of
items becomes three tables:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">CREATE TABLE customers (id text PRIMARY KEY, name text NOT NULL);
CREATE TABLE orders (id text PRIMARY KEY, customer_id text REFERENCES customers(id), created_at timestamptz);
CREATE TABLE order_items (order_id text REFERENCES orders(id), product text, qty int);
</code></pre>
</div>
<h2 id="moving-firebase-auth-users-if-you-do" tabindex="-1"><a class="header-anchor" href="#moving-firebase-auth-users-if-you-do">Moving Firebase Auth users, if you do</a></h2>
<p>Firebase can export users, including password hashes, with the <code>firebase auth:export</code> command. Most
projects hash passwords with a modified scrypt, and the console shows the hash parameters you need to
check those passwords elsewhere. Your new auth system must support that algorithm, or users will need
to reset their passwords once. Social logins move more easily, because they only need the provider’s
user ID.</p>
<h2 id="running-the-postgresql-side-in-india" tabindex="-1"><a class="header-anchor" href="#running-the-postgresql-side-in-india">Running the PostgreSQL side in India</a></h2>
<p>Once your data is relational, you need a PostgreSQL database and a place for the API that talks to it.
On Voroa both run in Mumbai: <a href="/managed-postgresql-india/">managed PostgreSQL</a> for the data, and your
Node.js or Django server deployed from GitHub, as in <a href="/docs/deploy-nodejs/">deploying a Node.js app</a>
or <a href="/docs/deploy-python-django/">deploying Django</a>. Billing is in rupees with a GST invoice, and the
database is reached by your server, not the browser.</p>
<p>If you are deciding between Supabase and plain PostgreSQL, read
<a href="/blog/supabase-alternative-india/">when a Supabase alternative makes sense</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="what-is-the-best-firebase-alternative" tabindex="-1"><a class="header-anchor" href="#what-is-the-best-firebase-alternative">What is the best Firebase alternative?</a></h3>
<p>It depends on the data and how much backend you want to write. For relational data with built-in auth,
Supabase. For an open-source all-in-one you can self-host, Appwrite. For a small self-hosted app,
PocketBase. For full control, PostgreSQL with your own API. For an offline-first mobile app, Firebase is
often still the right tool.</p>
<h3 id="can-i-keep-firebase-auth-and-use-postgresql" tabindex="-1"><a class="header-anchor" href="#can-i-keep-firebase-auth-and-use-postgresql">Can I keep Firebase Auth and use PostgreSQL?</a></h3>
<p>Yes. Keep Firebase Auth on the client, verify the ID token in your API with the Admin SDK, and store
your data in PostgreSQL. Push notifications through Cloud Messaging keep working too.</p>
<h3 id="does-firebase-bill-in-rupees" tabindex="-1"><a class="header-anchor" href="#does-firebase-bill-in-rupees">Does Firebase bill in rupees?</a></h3>
<p>Yes. Firebase uses Google Cloud billing, and accounts with an Indian billing address are billed in INR
by Google Cloud India, with GST on the invoice. If you move to PostgreSQL and want the same, Voroa bills
in rupees with a GST invoice and runs in Mumbai; it does not replace Firebase Auth or push notifications.</p>
<h3 id="how-long-does-a-firebase-to-postgresql-migration-take" tabindex="-1"><a class="header-anchor" href="#how-long-does-a-firebase-to-postgresql-migration-take">How long does a Firebase to PostgreSQL migration take?</a></h3>
<p>It depends mostly on how much data you copied between documents. The schema design takes longer than
moving the data, so plan for it first, and run reads in parallel before switching writes.</p>
<h2 id="start-the-postgresql-side-with-voroa" tabindex="-1"><a class="header-anchor" href="#start-the-postgresql-side-with-voroa">Start the PostgreSQL side with Voroa</a></h2>
<p>Create a free PostgreSQL database in Mumbai and load your exported data into it. If your data already
lives in another Postgres database, the import is covered in
<a href="/docs/import-existing-database/">importing an existing database</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Neon vs Supabase: Which Postgres Fits an App Built in India?</title>
      <link>https://getvoroa.com/blog/neon-vs-supabase-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/neon-vs-supabase-india/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <description><![CDATA[Neon vs Supabase compared for India: serverless scale-to-zero against a full backend, free plans, pricing, cold starts and the nearest region to your users.]]></description>
      <content:encoded><![CDATA[<p>Neon vs Supabase is a common question once you have decided on PostgreSQL. Both run real Postgres,
both have generous free plans, and both are popular with startups. They are built for different jobs:
Neon is a serverless database, Supabase is a backend platform with a database inside. This guide
compares them on pricing models with worked numbers, branching, backups, connections, idle behaviour,
and the question that matters most from India: where the database physically runs.</p>
<p>Figures are from Neon’s official <a href="https://neon.com/pricing">pricing page</a> and
<a href="https://neon.com/docs/introduction/regions">regions documentation</a>, and from Supabase’s pricing and
documentation, checked on 7 October 2026.</p>
<h2 id="two-different-products" tabindex="-1"><a class="header-anchor" href="#two-different-products">Two different products</a></h2>
<p><strong>Neon</strong> is PostgreSQL rebuilt so that storage and compute are separate. Compute can scale to zero when
nobody uses the database and start again on the next query, and it can grow and shrink with load. You
can branch a database the way you branch code. You bring your own backend.</p>
<p><strong>Supabase</strong> gives you one always-on PostgreSQL database per project, plus an auto-generated API, auth,
file storage and realtime. It replaces a backend for many apps. We explain those layers in
<a href="/blog/supabase-database-postgres-explained/">the Supabase database explained</a>.</p>
<h2 id="side-by-side" tabindex="-1"><a class="header-anchor" href="#side-by-side">Side by side</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Neon</th>
<th>Supabase</th>
</tr>
</thead>
<tbody>
<tr>
<td>What you get</td>
<td>Serverless PostgreSQL</td>
<td>PostgreSQL plus API, auth, storage, realtime</td>
</tr>
<tr>
<td>Free plan storage</td>
<td>1 GB per project, 20 GB per account</td>
<td>500 MB database (<a href="/blog/supabase-free-tier-limits/">free plan limits</a>)</td>
</tr>
<tr>
<td>Free plan compute</td>
<td>100 compute-unit hours per project, up to 2 CU</td>
<td>Shared CPU, 500 MB RAM, always on</td>
</tr>
<tr>
<td>Free plan when idle</td>
<td>Sleeps after 5 minutes, wakes on the next query</td>
<td>Paused after 1 week, resumed by hand</td>
</tr>
<tr>
<td>Free projects</td>
<td>100</td>
<td>2 active</td>
</tr>
<tr>
<td>Paid pricing model</td>
<td>Per compute-unit hour plus storage, no monthly minimum</td>
<td>$25 per month plan plus compute per project (<a href="/blog/supabase-pricing-india/">details</a>)</td>
</tr>
<tr>
<td>Branching</td>
<td>Built in, with data, 10 branches per project</td>
<td>Paid, billed per branch hour, starts without data</td>
</tr>
<tr>
<td>Restore to a past moment</td>
<td>6 hours free, up to 7 days on Launch, 30 on Scale</td>
<td>Point-in-time recovery add-on, $100 per month per 7 days</td>
</tr>
<tr>
<td>Connection pooling</td>
<td>PgBouncer, up to 10,000 clients</td>
<td>Supavisor, by compute size</td>
</tr>
<tr>
<td>Nearest region to India</td>
<td>Singapore</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Billing currency</td>
<td>USD</td>
<td>USD</td>
</tr>
</tbody>
</table></div>
<h2 id="how-neon-bills-with-numbers" tabindex="-1"><a class="header-anchor" href="#how-neon-bills-with-numbers">How Neon bills, with numbers</a></h2>
<p>Neon measures compute in compute units (CU); one CU is about 4 GB of RAM with matching CPU. On the
Launch plan you pay $0.106 per CU-hour, only while compute is running, plus $0.35 per GB-month of
storage. There is no monthly minimum.</p>
<p>That model rewards idle time. Two examples with a small 0.25 CU compute and 5 GB of data:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Workload</th>
<th>Compute hours in a month</th>
<th>Compute</th>
<th>Storage</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>Busy only 4 hours a day</td>
<td>0.25 x 4 x 30 = 30 CU-hours</td>
<td>$3.18</td>
<td>$1.75</td>
<td>about $4.90</td>
</tr>
<tr>
<td>Always on, all month</td>
<td>0.25 x 730 = 182.5 CU-hours</td>
<td>$19.35</td>
<td>$1.75</td>
<td>about $21.10</td>
</tr>
</tbody>
</table></div>
<p>On Supabase Pro the same small always-on database costs $25 a month, with one Micro compute instance
covered by the included credit and 8 GB of disk included, plus auth and storage that Neon does not
provide. For a database that is idle most of the day, Neon is clearly cheaper. For one that is busy all
day, the two land close together.</p>
<h2 id="branching-neons-strongest-feature" tabindex="-1"><a class="header-anchor" href="#branching-neons-strongest-feature">Branching: Neon’s strongest feature</a></h2>
<p>A Neon branch is an instant copy of the database, data included, made by copy-on-write so it costs
almost nothing until it changes. Teams create one per pull request, run migrations and tests against
real data, and delete it afterwards. Ten branches per project are included; on paid plans, extra ones are $1.50 per
branch-month.</p>
<p>A Supabase branch runs your migrations and seed file on a fresh database; by default it does not copy
production data. It is billed per branch per hour on paid plans.</p>
<p>If preview databases with realistic data matter to your team, Neon is ahead.</p>
<h2 id="cold-starts-and-pauses-are-not-the-same-thing" tabindex="-1"><a class="header-anchor" href="#cold-starts-and-pauses-are-not-the-same-thing">Cold starts and pauses are not the same thing</a></h2>
<p>Both free plans stop running when idle, but the effect on your app is very different.</p>
<p>On Neon, compute sleeps after five minutes without queries. The next query wakes it, typically in a
few hundred milliseconds, sometimes a few seconds. Your app keeps working; the first request is just
slower. Paid Neon plans can switch scale-to-zero off.</p>
<p>On Supabase, a free project that sees too little activity for a week is paused. It does not wake on the
next request; someone has to open the dashboard and resume it. Paid projects are never paused.</p>
<h2 id="the-region-question-for-indian-users" tabindex="-1"><a class="header-anchor" href="#the-region-question-for-indian-users">The region question for Indian users</a></h2>
<p><img src="https://media.getvoroa.com/img/01ca74bb8d0da12b.png" alt="Diagram: the nearest Neon region to India is Singapore, Supabase offers Mumbai, and Voroa runs in Mumbai" width="700" loading="eager" decoding="async"></p>
<p>Neon has no India region; its nearest is Singapore. Supabase offers Mumbai.</p>
<p>This matters most for the link between your app server and the database. A web page often runs several
queries per request, and each one crosses the distance between the two, so the delay is paid many
times per page. It adds up further on Neon, where a woken compute may also add a cold start. If your
users and your server are in India, a database in India is the simpler choice, and it is also the
answer some customers want when they ask where their data is stored.</p>
<h2 id="which-to-choose" tabindex="-1"><a class="header-anchor" href="#which-to-choose">Which to choose</a></h2>
<ul>
<li><strong>Choose Neon</strong> if your workload is spiky or idle much of the day, you want a branch with real data
for every pull request, and a Singapore region is acceptable.</li>
<li><strong>Choose Supabase</strong> if you want auth, storage and an API without writing a backend, with your data in
Mumbai.</li>
<li><strong>Choose a plain managed PostgreSQL in India</strong> if you have your own backend, want the database always
on in the same city as your app, and want a rupee bill.</li>
</ul>
<p>For the third case, <a href="/managed-postgresql-india/">Voroa’s managed PostgreSQL</a> runs in Mumbai next to the
service you deploy from GitHub, priced per size in rupees with a GST invoice. It does not scale to zero
and does not branch. Moving data in from either provider uses
<a href="/docs/import-existing-database/">the database import</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-neon-cheaper-than-supabase" tabindex="-1"><a class="header-anchor" href="#is-neon-cheaper-than-supabase">Is Neon cheaper than Supabase?</a></h3>
<p>For a database that is idle much of the day, yes, often by a wide margin, because Neon bills compute by
the hour of use. For a database busy all day, the gap narrows, and Supabase’s price also covers auth and
storage.</p>
<h3 id="can-i-use-neon-from-india" tabindex="-1"><a class="header-anchor" href="#can-i-use-neon-from-india">Can I use Neon from India?</a></h3>
<p>Yes. It works from anywhere, but the nearest region is Singapore, so each query from a server in India
travels further than it would to a Mumbai database.</p>
<h3 id="does-neon-have-auth-like-supabase" tabindex="-1"><a class="header-anchor" href="#does-neon-have-auth-like-supabase">Does Neon have auth like Supabase?</a></h3>
<p>Neon offers Neon Auth, which is newer and narrower than Supabase Auth. Most Neon users pair it with
their own backend or a separate auth service.</p>
<h3 id="which-is-better-for-preview-environments" tabindex="-1"><a class="header-anchor" href="#which-is-better-for-preview-environments">Which is better for preview environments?</a></h3>
<p>Neon. Its branches include data and are created instantly, which makes a database per pull request
cheap and realistic.</p>
<h2 id="keep-the-database-next-to-your-app" tabindex="-1"><a class="header-anchor" href="#keep-the-database-next-to-your-app">Keep the database next to your app</a></h2>
<p>If your server runs in India, start a free PostgreSQL database in Mumbai and connect it with one
variable. We compare the wider options in <a href="/blog/supabase-alternative-india/">our Supabase alternative guide</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>What Is pgvector? The Postgres Vector Database, Explained</title>
      <link>https://getvoroa.com/blog/postgres-vector-database-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/postgres-vector-database-india/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <description><![CDATA[What is pgvector? A free extension that makes a Postgres vector database: embeddings and similarity search in SQL, its uses, indexes, limits and a benchmark.]]></description>
      <content:encoded><![CDATA[<p>pgvector is the simplest way to build a Postgres vector database: one free extension adds a <code>vector</code>
column type and similarity search to PostgreSQL, so embeddings live in the same table as the rows they
describe and obey the same filters and permissions. This guide answers what pgvector is and what it is
used for, then shows how to install it, which index to pick, how much memory it needs, and what it
actually does on a small machine - we loaded 100,000 vectors and timed every query.</p>
<p>Facts are from the official <a href="https://github.com/pgvector/pgvector">pgvector repository</a> and its
changelog: the current release is 0.8.7, published on 1 October 2026, and this page was last checked on
14 November 2026. Benchmark numbers are our own, measured on 16 October 2026.</p>
<h2 id="what-is-pgvector" tabindex="-1"><a class="header-anchor" href="#what-is-pgvector">What is pgvector?</a></h2>
<p>pgvector (also written “pg vector”) is a free, open-source PostgreSQL extension that stores AI embeddings
in a <code>vector</code> column and finds the most similar ones with an ordinary SQL query. It does exact and
approximate nearest-neighbour search, with HNSW and IVFFlat indexes and six distance functions, on
Postgres 13 and later. You turn it on with <code>CREATE EXTENSION vector;</code>.</p>
<p><img src="https://media.getvoroa.com/img/46a3acdb5c5b5dc9.png" alt="Diagram: text or an image goes to an embedding model, the result is stored in a vector column with an HNSW index, and one SQL query returns similar rows with normal filters" width="700" loading="eager" decoding="async"></p>
<p>In one line each:</p>
<ul>
<li><strong>What it stores:</strong> embeddings - lists of numbers an AI model produces from text, images or audio,
where similar meanings sit close together.</li>
<li><strong>What it adds to Postgres:</strong> four column types (<code>vector</code>, <code>halfvec</code>, <code>bit</code>, <code>sparsevec</code>), distance
operators such as <code>&lt;=&gt;</code> for cosine, and two index types that make search fast.</li>
<li><strong>What it does not do:</strong> create embeddings. You get those from a model, such as an embedding API or an
open-source model you run yourself, and insert them like any other value.</li>
<li><strong>Licence and cost:</strong> the PostgreSQL License. The extension is free; you pay only for the database
machine.</li>
</ul>
<h2 id="what-is-pgvector-used-for" tabindex="-1"><a class="header-anchor" href="#what-is-pgvector-used-for">What is pgvector used for?</a></h2>
<p>Anything that needs “find the rows most like this one”, next to data you already keep in Postgres:</p>
<ol>
<li><strong>Retrieval for AI chatbots (RAG).</strong> Find the five document chunks closest to a user’s question and
pass them to a language model as context.</li>
<li><strong>Semantic search.</strong> Search help articles, products or tickets by meaning, so “refund not received”
also finds “money not credited back”.</li>
<li><strong>Recommendations.</strong> “Customers who viewed this also viewed” from product or content embeddings.</li>
<li><strong>Duplicate and near-duplicate detection.</strong> Flag support tickets, listings or uploads that say the
same thing in different words.</li>
<li><strong>Image similarity.</strong> Find visually similar product photos from image embeddings.</li>
<li><strong>Classification and anomaly checks.</strong> Label a new item by its nearest labelled neighbours, or flag
one that is far from everything seen before.</li>
</ol>
<p>The reason to do these in Postgres rather than in a separate vector database is the join: one query can
filter by tenant, permissions and metadata and rank by similarity at the same time, and deleting a row
deletes its embedding in the same transaction.</p>
<h2 id="does-postgres-support-vector-databases" tabindex="-1"><a class="header-anchor" href="#does-postgres-support-vector-databases">Does Postgres support vector databases?</a></h2>
<p>Not on its own. Core PostgreSQL (version 18 is current) has no vector type or similarity index: you can keep
numbers in a <code>real[]</code> array, but there is no distance operator or index to search them. pgvector is the extension that
adds them, and it is what most managed Postgres services mean when they say they support vector search.
Once it is enabled, Postgres works as a vector database for most application workloads; the
<a href="#pgvector-vs-pinecone-and-other-vector-databases">comparison further down</a> covers where a dedicated
system starts to make sense.</p>
<p><strong>On this page</strong></p>
<ul>
<li><a href="#how-pgvector-works-types-and-operators">How pgvector works: types and operators</a></li>
<li><a href="#how-to-install-pgvector">How to install pgvector</a></li>
<li><a href="#hnsw-or-ivfflat">HNSW or IVFFlat</a></li>
<li><a href="#pgvector-benchmark-100000-vectors-on-a-small-instance">pgvector benchmark: 100,000 vectors on a small instance</a></li>
<li><a href="#how-much-memory-pgvector-needs">How much memory pgvector needs</a></li>
<li><a href="#pgvector-vs-pinecone-and-other-vector-databases">pgvector vs Pinecone and other vector databases</a></li>
<li><a href="#enable-pgvector-on-voroa-step-by-step">Enable pgvector on Voroa, step by step</a></li>
<li><a href="#faq">FAQ</a></li>
</ul>
<h2 id="how-pgvector-works-types-and-operators" tabindex="-1"><a class="header-anchor" href="#how-pgvector-works-types-and-operators">How pgvector works: types and operators</a></h2>
<p>An embedding model turns text or an image into a list of numbers; similar meanings produce lists that
are close together. pgvector stores the list in a column and finds the nearest ones:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE chunks (
  id bigserial PRIMARY KEY,
  document_id bigint NOT NULL,
  tenant_id bigint NOT NULL,
  content text NOT NULL,
  embedding vector(1536)
);

CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);
</code></pre>
</div>
<h3 id="distance-operators" tabindex="-1"><a class="header-anchor" href="#distance-operators">Distance operators</a></h3>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Operator</th>
<th>Distance</th>
<th>Typical use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>&lt;=&gt;</code></td>
<td>Cosine</td>
<td>Text embeddings; the usual default</td>
</tr>
<tr>
<td><code>&lt;-&gt;</code></td>
<td>Euclidean (L2)</td>
<td>Image and other embeddings trained for L2</td>
</tr>
<tr>
<td><code>&lt;#&gt;</code></td>
<td>Negative inner product</td>
<td>Normalised vectors, slightly faster than cosine</td>
</tr>
<tr>
<td><code>&lt;+&gt;</code></td>
<td>Manhattan (L1)</td>
<td>Less common</td>
</tr>
<tr>
<td><code>&lt;~&gt;</code> and <code>&lt;%&gt;</code></td>
<td>Hamming and Jaccard</td>
<td>Binary vectors</td>
</tr>
</tbody>
</table></div>
<p>Match the operator to the index’s operator class, such as <code>vector_cosine_ops</code> for <code>&lt;=&gt;</code>, or the index
will not be used.</p>
<h3 id="which-vector-type-to-use" tabindex="-1"><a class="header-anchor" href="#which-vector-type-to-use">Which vector type to use</a></h3>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Type</th>
<th>Bytes per number</th>
<th>Most dimensions you can store</th>
<th>Most dimensions you can index</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>vector</code></td>
<td>4</td>
<td>16,000</td>
<td>2,000</td>
</tr>
<tr>
<td><code>halfvec</code></td>
<td>2</td>
<td>16,000</td>
<td>4,000</td>
</tr>
<tr>
<td><code>bit</code></td>
<td>1 bit</td>
<td>not stated</td>
<td>64,000</td>
</tr>
<tr>
<td><code>sparsevec</code></td>
<td>only non-zero values</td>
<td>16,000 non-zero</td>
<td>1,000 non-zero</td>
</tr>
</tbody>
</table></div>
<p>This decides a practical question. OpenAI’s
<a href="https://developers.openai.com/api/docs/guides/embeddings">embeddings guide</a> lists
<code>text-embedding-3-small</code> at 1,536 dimensions, which fits <code>vector</code>, and <code>text-embedding-3-large</code> at
3,072, which is too many to index as <code>vector</code>. For the large model, either store <code>halfvec(3072)</code>, or ask
the API for fewer dimensions with its <code>dimensions</code> parameter, for example 1,024, at a small cost in
accuracy.</p>
<h2 id="how-to-install-pgvector" tabindex="-1"><a class="header-anchor" href="#how-to-install-pgvector">How to install pgvector</a></h2>
<p>Most people do not compile pgvector. Pick the line that matches where your database runs.</p>
<p><strong>Docker.</strong> The official image already contains it. Tags follow <code>pg&lt;version&gt;-&lt;debian release&gt;</code>:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">docker run -d --name pgvector -e POSTGRES_PASSWORD=secret -p 5432:5432 pgvector/pgvector:pg17
</code></pre>
</div>
<p><strong>Linux, from source.</strong> You need the Postgres server headers (<code>postgresql-server-dev-17</code> on Debian
and Ubuntu):</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">cd /tmp
git clone --branch v0.8.7 https://github.com/pgvector/pgvector.git
cd pgvector
make
make install   # may need sudo
</code></pre>
</div>
<p><strong>Windows.</strong> Open the “x64 Native Tools Command Prompt for VS” as administrator, point <code>PGROOT</code> at your
install and build with <code>nmake</code>:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bat</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bat">set &quot;PGROOT=C:\Program Files\PostgreSQL\17&quot;
cd %TEMP%
git clone --branch v0.8.7 https://github.com/pgvector/pgvector.git
cd pgvector
nmake /F Makefile.win
nmake /F Makefile.win install
</code></pre>
</div>
<p><strong>Managed Postgres.</strong> Nothing to install: enable it from the provider’s console, then run
<code>CREATE EXTENSION vector;</code> if the console does not do that for you. The Voroa steps, with screenshots,
are <a href="#enable-pgvector-on-voroa-step-by-step">further down</a>.</p>
<p>Then check it worked:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">SELECT extversion FROM pg_extension WHERE extname = 'vector';
</code></pre>
</div>
<h2 id="hnsw-or-ivfflat" tabindex="-1"><a class="header-anchor" href="#hnsw-or-ivfflat">HNSW or IVFFlat</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>HNSW</th>
<th>IVFFlat</th>
</tr>
</thead>
<tbody>
<tr>
<td>Search speed and recall</td>
<td>Better</td>
<td>Good, depends on tuning</td>
</tr>
<tr>
<td>Build time</td>
<td>Slower</td>
<td>Much faster</td>
</tr>
<tr>
<td>Memory while building</td>
<td>Higher</td>
<td>Lower</td>
</tr>
<tr>
<td>Can be created on an empty table</td>
<td>Yes</td>
<td>No; build after loading data</td>
</tr>
<tr>
<td>Main tuning settings</td>
<td><code>m</code> (default 16), <code>ef_construction</code> (default 64), <code>hnsw.ef_search</code> (default 40)</td>
<td><code>lists</code> when building, <code>ivfflat.probes</code> (default 1) when searching</td>
</tr>
</tbody>
</table></div>
<p>HNSW builds a layered graph in which every vector links to its closest neighbours. A search enters at
the top layer, jumps towards the query, and drops a layer each time it gets close, so it touches only a
small part of the data. That is why it is fast and accurate, and also why it uses more memory and
takes longer to build: every insert has to find its place in the graph.</p>
<p>IVFFlat splits the vectors into groups around centre points and searches only the groups nearest to
the query. It builds in a fraction of the time and needs less memory, which makes it a fair choice
for a large table you rebuild often or a machine with little RAM, as long as you accept tuning
<code>probes</code> to get recall up.</p>
<p>Start with HNSW. Raise <code>hnsw.ef_search</code> per query when you need better recall, at some cost in speed:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">SET hnsw.ef_search = 100;
</code></pre>
</div>
<p>For IVFFlat, pgvector’s guidance is <code>lists</code> of about rows / 1,000 up to a million rows, and the square
root of the row count above that; then raise <code>ivfflat.probes</code> until recall is good enough.</p>
<h2 id="pgvector-benchmark-100000-vectors-on-a-small-instance" tabindex="-1"><a class="header-anchor" href="#pgvector-benchmark-100000-vectors-on-a-small-instance">pgvector benchmark: 100,000 vectors on a small instance</a></h2>
<p>Most guides stop at “HNSW is faster”. We wanted the numbers on the kind of machine a small team starts
with, so we created a free Postgres 17 database with pgvector 0.8.0 on a 0.25 compute-unit instance
(Neon’s free tier, with 64 MB of <code>maintenance_work_mem</code>), loaded 100,000 vectors of 384 dimensions -
the size of common open-source text embedding models - and timed top-10 cosine queries.</p>
<p>Times are
server-side (<code>EXPLAIN ANALYZE</code>), so the network is not in them. Recall is checked against an exact scan
for 50 queries.</p>
<p><img src="https://media.getvoroa.com/img/f981e859fbd68b18.png" alt="Bar chart: top-10 query over 100,000 vectors - 61.3 ms with no index, 0.34 ms with HNSW at ef_search 40 (91.2% recall), 0.53 ms at ef_search 100 (99.2%), 1.83 ms at ef_search 200 (99.2%)" width="700" loading="lazy" decoding="async"></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>What we measured</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Table size, 100,000 x 384-dimension <code>vector</code></td>
<td>158 MB</td>
</tr>
<tr>
<td>Exact search, no index</td>
<td>61.3 ms median</td>
</tr>
<tr>
<td>HNSW index build (defaults, <code>m</code> 16, <code>ef_construction</code> 64)</td>
<td>89 seconds</td>
</tr>
<tr>
<td>HNSW index size</td>
<td>195 MB</td>
</tr>
<tr>
<td>HNSW, <code>ef_search</code> 40 (default)</td>
<td>0.34 ms, 91.2% recall</td>
</tr>
<tr>
<td>HNSW, <code>ef_search</code> 100</td>
<td>0.53 ms, 99.2% recall</td>
</tr>
<tr>
<td>HNSW, <code>ef_search</code> 200</td>
<td>1.83 ms, 99.2% recall</td>
</tr>
<tr>
<td>Same vectors as <code>halfvec</code>: index size</td>
<td>112 MB, 89.6% recall at <code>ef_search</code> 40</td>
</tr>
</tbody>
</table></div>
<p><strong>The index is the whole game.</strong> The same query went from 61 ms to a third of a millisecond, about
180 times faster. Without an index Postgres compares the query with every row; with HNSW it walks a
graph and looks at a few hundred candidates instead of a hundred thousand.</p>
<p><strong>Raise <code>ef_search</code> to 100 for production.</strong> The default of 40 returned 91% of the true nearest
neighbours. At 100 it returned 99% and still answered in about half a millisecond. Past 100 we paid
three times the time for no extra recall, so 100 is a sensible starting point for most apps.</p>
<p><strong><code>halfvec</code> cut the index by 43%</strong> for a 1.6-point recall loss. On a small machine that is often the
difference between an index that stays in memory and one that keeps being read from disk, which is
where the slow queries come from.</p>
<p><strong>Your data changes everything, so test on it.</strong> We first ran the same benchmark on purely random
vectors. The build took 386 seconds instead of 89, and recall at the default setting was 7.6%. Random
vectors have no real neighbours, while real embeddings cluster around topics. Benchmark with a sample
of your own embeddings, not with synthetic noise, or the result will mislead you in either direction.</p>
<p>Our vectors were synthetic but clustered the way embeddings are (1,000 groups of related points), and
the queries were new points near those groups. A different dataset will give different absolute
numbers; the ratios are what to take away.</p>
<h2 id="filtered-search-and-the-catch" tabindex="-1"><a class="header-anchor" href="#filtered-search-and-the-catch">Filtered search, and the catch</a></h2>
<p>Real queries filter: only this customer’s documents, only this category. With an approximate index,
Postgres first finds the nearest candidates, then applies the filter, so a selective filter can return
fewer rows than you asked for. pgvector 0.8 added iterative index scans, which keep searching until
enough rows pass the filter:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">SET hnsw.iterative_scan = relaxed_order;
</code></pre>
</div>
<p>For filters that remove most rows, such as one tenant out of thousands, an ordinary B-tree index on the
filter column, or a partial index per large tenant, often helps more.</p>
<h2 id="a-retrieval-query-for-a-chatbot" tabindex="-1"><a class="header-anchor" href="#a-retrieval-query-for-a-chatbot">A retrieval query for a chatbot</a></h2>
<p>The core of retrieval-augmented generation is one query: the closest chunks the user is allowed to see.</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">SELECT c.content, d.title
FROM chunks c
JOIN documents d ON d.id = c.document_id
WHERE c.tenant_id = $2
ORDER BY c.embedding &lt;=&gt; $1
LIMIT 5;
</code></pre>
</div>
<p>The join and the <code>tenant_id</code> filter are why keeping vectors in Postgres is attractive: permissions,
metadata and similarity are answered together, with no second system to keep in sync.</p>
<p>From Python, the <code>pgvector</code> package registers the type with psycopg, SQLAlchemy and Django, and
LangChain’s Postgres vector store uses pgvector underneath, so the same table works from a framework.</p>
<h2 id="common-pgvector-mistakes" tabindex="-1"><a class="header-anchor" href="#common-pgvector-mistakes">Common pgvector mistakes</a></h2>
<p><strong>The query does not use the index.</strong> The <code>ORDER BY</code> has to be the bare distance expression with the
same operator as the index, followed by a <code>LIMIT</code>. An index built with <code>vector_cosine_ops</code> is ignored
by a query that orders by <code>&lt;-&gt;</code>, and so is one that sorts by <code>1 - (embedding &lt;=&gt; $1)</code>. Run
<code>EXPLAIN</code> once on every new query and look for the index name in the plan.</p>
<p><strong>Statistics were never collected.</strong> After a bulk load, run <code>ANALYZE</code> on the table. Without it the
planner can misjudge how many rows a filter keeps and choose a full scan for a query that should have
used the index, which looks like pgvector being slow when it is the plan that is wrong.</p>
<p><strong>The dimension does not match.</strong> A <code>vector(1536)</code> column rejects a 1,024-number embedding with an
error, and switching embedding models later means re-embedding every row. Record the model and its
dimension next to the data, and treat a model change as a migration rather than a config change.</p>
<p><strong>Inner product on unnormalised vectors.</strong> The <code>&lt;#&gt;</code> operator is only a stand-in for cosine when every
vector has length 1. Most hosted embedding APIs return normalised vectors; many local models do not.
If you are unsure, use cosine.</p>
<p><strong>Building the index on a busy table.</strong> A plain <code>CREATE INDEX</code> blocks writes until it finishes, which
on a large table can be many minutes. Use <code>CREATE INDEX CONCURRENTLY</code> in production, and load data
before building an IVFFlat index, because it learns its groups from the rows that exist at build time.</p>
<h2 id="how-much-memory-pgvector-needs" tabindex="-1"><a class="header-anchor" href="#how-much-memory-pgvector-needs">How much memory pgvector needs</a></h2>
<p>Vector search is fast only when the index sits in memory. In our benchmark the HNSW index was 1.3
times the raw vector data, and table plus index came to about 350 MB per 100,000 vectors of 384
dimensions. Raw vector sizes for common embedding sizes:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Vectors</th>
<th>1,536 dimensions as <code>vector</code></th>
<th>1,536 as <code>halfvec</code></th>
<th>3,072 as <code>halfvec</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>100,000</td>
<td>about 0.6 GB</td>
<td>about 0.3 GB</td>
<td>about 0.6 GB</td>
</tr>
<tr>
<td>1,000,000</td>
<td>about 6.1 GB</td>
<td>about 3.1 GB</td>
<td>about 6.1 GB</td>
</tr>
</tbody>
</table></div>
<p>Add roughly the same again for the HNSW index, then leave room for the rest of your database. As a
starting point on Voroa’s sizes:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Size</th>
<th>RAM</th>
<th>Comfortable for (HNSW, <code>vector</code>)</th>
</tr>
</thead>
<tbody>
<tr>
<td>DB Starter</td>
<td>1 GB</td>
<td>about 200,000 vectors of 384 dimensions</td>
</tr>
<tr>
<td>DB Standard</td>
<td>2 GB</td>
<td>about 100,000 vectors of 1,536 dimensions</td>
</tr>
<tr>
<td>DB Pro</td>
<td>4 GB</td>
<td>about 200,000 vectors of 1,536, or twice that as <code>halfvec</code></td>
</tr>
<tr>
<td>DB Pro Max</td>
<td>8 GB</td>
<td>about 400,000 vectors of 1,536, or twice that as <code>halfvec</code></td>
</tr>
</tbody>
</table></div>
<p>These are estimates scaled from our measurement; measure your own index with
<code>SELECT pg_size_pretty(pg_relation_size('your_index'));</code> before you size up. HNSW builds are much faster
when the graph fits in <code>maintenance_work_mem</code> - Postgres prints a notice when it does not - and
<code>CREATE INDEX CONCURRENTLY</code> keeps a live table writable while it builds.</p>
<h2 id="pgvector-vs-pinecone-and-other-vector-databases" tabindex="-1"><a class="header-anchor" href="#pgvector-vs-pinecone-and-other-vector-databases">pgvector vs Pinecone and other vector databases</a></h2>
<p>pgvector covers what most teams build: documentation search, support ticket matching, product
recommendations and chatbot retrieval. A dedicated vector database such as Pinecone, Milvus or
Weaviate starts to earn its place in three situations: when you store tens of millions of vectors and
the index no longer fits in one machine’s memory, when search must stay equally fast while the data
changes constantly at high volume, or when you need vector search with no relational data at all.</p>
<p>Below that, a second system mainly adds another bill, another network hop on every query and another
copy of your data to keep in sync with the rows it describes. With pgvector, deleting a document
deletes its embedding in the same transaction; with two systems, that is a job you have to build and
watch.</p>
<h2 id="enable-pgvector-on-voroa-step-by-step" tabindex="-1"><a class="header-anchor" href="#enable-pgvector-on-voroa-step-by-step">Enable pgvector on Voroa, step by step</a></h2>
<p><a href="/managed-postgresql-india/">Voroa’s managed PostgreSQL</a> runs in Mumbai and offers pgvector as
<strong>Vector Search</strong> on paid sizes. Every new database is built to support it. Only some older
databases first created on the free plan are not, and upgrading alone does not change that; if Vector
Search is not listed on your paid database, contact support. The full rules are in
<a href="/docs/postgres-extensions/">PostgreSQL extensions on Voroa</a>, and sizes are on the
<a href="/pricing/">pricing page</a>.</p>
<p><strong>Step 1. Create a Postgres database on a paid size.</strong> In the dashboard choose <strong>New</strong>, then
<strong>Postgres</strong>, and pick a paid instance type with enough RAM for your index (the table above).</p>
<p><strong>Step 2. Open Settings and find Extensions.</strong> On the database, go to the <strong>Settings</strong> tab and scroll to
<strong>Extensions</strong>. Vector Search is listed with the other extensions.</p>
<p><img src="https://media.getvoroa.com/img/1d345a4dc0a7d9b1.png" alt="Extensions card on a Voroa Postgres database's Settings tab, listing UUID Generation, Case-insensitive Text, Trigram Search, Cryptographic Functions, Key-Value Store and Vector Search, each Available with an Enable button" width="700" loading="lazy" decoding="async"></p>
<p><strong>Step 3. Select Enable next to Vector Search.</strong> The status changes to <strong>Pending</strong> while Voroa installs
it, with no database restart, and to <strong>Enabled</strong> once it has checked the extension is really there.</p>
<p><img src="https://media.getvoroa.com/img/937b59464a7a926a.png" alt="Extensions card after selecting Enable: Vector Search now shows Pending" width="700" loading="lazy" decoding="async"></p>
<p><strong>Step 4. Create your table and index.</strong> Connect with the <code>DATABASE_URL</code> from the <strong>Connect</strong> tab (the
details are in <a href="/docs/postgresql-creating-connecting/">PostgreSQL connection details</a>) and run the
<code>CREATE TABLE</code> and <code>CREATE INDEX</code> statements from the top of this page.</p>
<p>Coming from Supabase, which also ships pgvector? See <a href="/blog/supabase-alternative-india/">moving off Supabase</a>;
existing data, vector columns included, comes in through <a href="/docs/import-existing-database/">the database import</a>.
Just trying things out first? Our comparison of <a href="/blog/free-postgres-database/">free Postgres databases</a>
covers the free tiers, including the limits that matter once an index is in memory.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-pgvector-free" tabindex="-1"><a class="header-anchor" href="#is-pgvector-free">Is pgvector free?</a></h3>
<p>Yes. pgvector is open source under the PostgreSQL License. You pay only for the machine your database
runs on.</p>
<h3 id="can-pgvector-handle-10-million-vectors" tabindex="-1"><a class="header-anchor" href="#can-pgvector-handle-10-million-vectors">Can pgvector handle 10 million vectors?</a></h3>
<p>Yes, if the machine has the memory. Ten million 1,536-dimension embeddings are about 61 GB as <code>vector</code>
or 31 GB as <code>halfvec</code> before the index, and an HNSW index adds roughly the same again, so plan for a
large instance or shorter embeddings (10 million at 384 dimensions is about 15 GB). Build the index with
plenty of <code>maintenance_work_mem</code>, use <code>CREATE INDEX CONCURRENTLY</code>, and test recall with your filters.
When one machine’s memory is no longer enough, a dedicated vector database is worth comparing.</p>
<h3 id="is-pgvector-the-same-as-pg_vector-or-pg-vector" tabindex="-1"><a class="header-anchor" href="#is-pgvector-the-same-as-pg_vector-or-pg-vector">Is pgvector the same as pg_vector or pg vector?</a></h3>
<p>Yes. The project is called pgvector, people often write it “pg vector” or “pg_vector”, and the
extension you create inside Postgres is named <code>vector</code>.</p>
<h3 id="is-pgvector-good-enough-for-production" tabindex="-1"><a class="header-anchor" href="#is-pgvector-good-enough-for-production">Is pgvector good enough for production?</a></h3>
<p>Yes, for most AI features. Plan the memory, use an HNSW index, set <code>hnsw.ef_search</code> to about 100, and
test recall on your own data with your real filters.</p>
<h3 id="does-pgvector-work-with-prisma-or-django" tabindex="-1"><a class="header-anchor" href="#does-pgvector-work-with-prisma-or-django">Does pgvector work with Prisma or Django?</a></h3>
<p>Yes. Prisma treats vector columns as an unsupported type and queries them with raw SQL. Django has the
official <code>pgvector</code> Python package, with a field type and distance functions.</p>
<h3 id="what-embedding-size-should-i-use" tabindex="-1"><a class="header-anchor" href="#what-embedding-size-should-i-use">What embedding size should I use?</a></h3>
<p>The smallest that is accurate enough on your data. Smaller embeddings need less memory and search
faster; test a shortened embedding before paying for the largest.</p>
<h3 id="why-does-my-filtered-vector-query-return-fewer-results-than-limit" tabindex="-1"><a class="header-anchor" href="#why-does-my-filtered-vector-query-return-fewer-results-than-limit">Why does my filtered vector query return fewer results than LIMIT?</a></h3>
<p>The index found the nearest candidates before the filter removed most of them. Turn on iterative index
scans, or add an index on the filter column.</p>
<h2 id="add-vector-search-next-to-your-data" tabindex="-1"><a class="header-anchor" href="#add-vector-search-next-to-your-data">Add vector search next to your data</a></h2>
<p>Create a paid PostgreSQL database in Mumbai, turn on Vector Search, and keep your embeddings in the same
database as your app. For the wider choice of Postgres hosts, read
<a href="/blog/neon-vs-supabase-india/">Neon vs Supabase</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>PostgreSQL vs MongoDB: Which Database Should Your App Use?</title>
      <link>https://getvoroa.com/blog/postgresql-vs-mongodb-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/postgresql-vs-mongodb-india/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <description><![CDATA[PostgreSQL vs MongoDB explained simply: tables against documents, joins, transactions, JSON support, limits, licences, real costs and hosting from India.]]></description>
      <content:encoded><![CDATA[<p>PostgreSQL vs MongoDB is one of the first choices in a new project, and the MERN stack taught many
developers in India to reach for MongoDB by default. Both are mature, widely used and easy to host. They
store data in different shapes, and that shape decides how easy your app is to build and change a year
from now. This guide compares them on the points that matter in practice, with the real limits,
licences and costs, and shows the same query written for each.</p>
<h2 id="tables-or-documents" tabindex="-1"><a class="header-anchor" href="#tables-or-documents">Tables or documents</a></h2>
<p><img src="https://media.getvoroa.com/img/bc340a6f0f0c6ccc.png" alt="Diagram: PostgreSQL stores one order as rows in users, orders and order_items tables joined by foreign keys; MongoDB stores it as one order document with the items inside" width="700" loading="eager" decoding="async"></p>
<p>PostgreSQL stores data in tables with a defined set of columns. An order lives in an <code>orders</code> table, its
items in an <code>order_items</code> table, and foreign keys link them. One SQL query joins them back together.</p>
<p>MongoDB stores JSON-like documents in collections. An order and its items can sit in one document, and
two documents in the same collection may have different fields, so you can change the shape without a
migration.</p>
<p>Neither is better in general. Documents are natural when you always read and write the whole thing
together. Tables are natural when the same data is used from many directions, such as an order seen
from the customer, the product and the warehouse.</p>
<h2 id="the-two-engines-compared-point-by-point" tabindex="-1"><a class="header-anchor" href="#the-two-engines-compared-point-by-point">The two engines compared point by point</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>PostgreSQL</th>
<th>MongoDB</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data model</td>
<td>Tables, rows, foreign keys</td>
<td>Collections of JSON-like documents</td>
</tr>
<tr>
<td>Schema</td>
<td>Defined up front, changed with migrations</td>
<td>Flexible; optional validation rules</td>
</tr>
<tr>
<td>Joins</td>
<td>Native, any number of tables</td>
<td><code>$lookup</code> stage; most designs avoid it</td>
</tr>
<tr>
<td>Transactions</td>
<td>Full ACID across tables</td>
<td>Single-document atomic; multi-document since 4.0, 60-second default limit</td>
</tr>
<tr>
<td>Largest single record</td>
<td>A row can hold up to 1 GB in a field</td>
<td>16 MB per document</td>
</tr>
<tr>
<td>JSON</td>
<td><code>jsonb</code> columns with indexes</td>
<td>Native</td>
</tr>
<tr>
<td>Scaling out</td>
<td>Read replicas, partitioning, extensions for sharding</td>
<td>Sharding built in</td>
</tr>
<tr>
<td>Licence</td>
<td>PostgreSQL Licence, OSI-approved open source</td>
<td>Server Side Public License, not OSI-approved</td>
</tr>
</tbody>
</table></div>
<p>The MongoDB figures are from its official
<a href="https://www.mongodb.com/docs/manual/reference/limits/">limits and thresholds reference</a>.</p>
<h2 id="the-same-question-asked-of-each" tabindex="-1"><a class="header-anchor" href="#the-same-question-asked-of-each">The same question, asked of each</a></h2>
<p>“The ten biggest orders this week, with the customer’s name”:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">SELECT o.id, c.name, o.total
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at &gt; now() - interval '7 days'
ORDER BY o.total DESC
LIMIT 10;
</code></pre>
</div>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">javascript</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-javascript">db.orders.aggregate([
  { $match: { createdAt: { $gt: new Date(Date.now() - 7 * 864e5) } } },
  { $sort: { total: -1 } },
  { $limit: 10 },
  { $lookup: { from: &quot;customers&quot;, localField: &quot;customerId&quot;, foreignField: &quot;_id&quot;, as: &quot;customer&quot; } },
  { $project: { total: 1, &quot;customer.name&quot;: 1 } }
]);
</code></pre>
</div>
<p>Both work. The SQL version stays short as the question grows; the pipeline grows a stage for every
new relationship. Many MongoDB designs avoid <code>$lookup</code> by copying the customer’s name into each order,
which makes this read fast and makes renaming a customer a write to every order they ever placed.</p>
<h2 id="transactions-and-correctness" tabindex="-1"><a class="header-anchor" href="#transactions-and-correctness">Transactions and correctness</a></h2>
<p>PostgreSQL has had full ACID transactions across any number of tables since its early versions. Money,
inventory and bookings, where two writes must succeed or fail together, fit it naturally.</p>
<p>MongoDB has always made a single document update atomic, and has supported multi-document transactions
since version 4.0. They work, but they were added to a design built around single documents: they cost
more, and by default a transaction that runs longer than 60 seconds is aborted. Many MongoDB apps avoid
them by putting everything that changes together into one document.</p>
<h2 id="json-in-postgresql" tabindex="-1"><a class="header-anchor" href="#json-in-postgresql">JSON in PostgreSQL</a></h2>
<p>You do not have to give up documents to use PostgreSQL. Its <code>jsonb</code> type stores JSON in a column, can be
indexed, and can be queried in the same statement as normal columns:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">CREATE TABLE products (
  id bigserial PRIMARY KEY,
  name text NOT NULL,
  price numeric NOT NULL,
  attributes jsonb
);
CREATE INDEX ON products USING gin (attributes);

SELECT name, price FROM products WHERE attributes @&gt; '{&quot;colour&quot;: &quot;black&quot;}';
</code></pre>
</div>
<p>A common pattern is typed columns for what every row has, plus a <code>jsonb</code> column for optional fields.
PostgreSQL’s own <a href="https://www.postgresql.org/docs/current/datatype-json.html">JSON types documentation</a>
covers the operators. For heavy, deeply nested documents that change shape constantly, MongoDB is still
more comfortable.</p>
<h2 id="licences-why-some-companies-care" tabindex="-1"><a class="header-anchor" href="#licences-why-some-companies-care">Licences: why some companies care</a></h2>
<p>PostgreSQL uses a short, permissive licence approved by the Open Source Initiative. You can use, change
and offer it as a service with almost no conditions.</p>
<p>MongoDB moved to the Server Side Public License in 2018. It is free to use in your own app, but it adds
strict conditions if you offer MongoDB itself as a service, and it is not OSI-approved. Some Linux
distributions and projects dropped MongoDB over this. For most app developers it changes nothing; for a
company building a database product, it matters.</p>
<h2 id="popularity" tabindex="-1"><a class="header-anchor" href="#popularity">Popularity</a></h2>
<p>PostgreSQL was the most used database in the 2023, 2024 and 2025 Stack Overflow Developer
Surveys. That shows up in practice: more hosting options, more ORM support, and more answers when you get
stuck.</p>
<h2 id="where-supabase-fits" tabindex="-1"><a class="header-anchor" href="#where-supabase-fits">Where Supabase fits</a></h2>
<p>Supabase is PostgreSQL, so “Supabase vs MongoDB” is the same choice as PostgreSQL vs MongoDB, plus
Supabase’s auth and API on top. We explain what Supabase adds in
<a href="/blog/supabase-database-postgres-explained/">the Supabase database explained</a>.</p>
<h2 id="free-tiers-and-costs-from-india" tabindex="-1"><a class="header-anchor" href="#free-tiers-and-costs-from-india">Free tiers and costs from India</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>MongoDB Atlas</th>
<th>PostgreSQL on Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Free size</td>
<td>M0: 512 MB, shared</td>
<td>DB Nano: 1 GB</td>
</tr>
<tr>
<td>Smallest paid step</td>
<td>Flex: about $8 to $30 per month, 5 GB</td>
<td>DB Starter: Rs 299 per month, plus GST</td>
</tr>
<tr>
<td>First dedicated size</td>
<td>M10: about $57 per month (default region; Mumbai can differ), 2 vCPU, 2 GB RAM</td>
<td>DB Standard: Rs 999 per month, 2 GB RAM, 10 GB</td>
</tr>
<tr>
<td>India region</td>
<td>Mumbai available</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Billed in</td>
<td>USD</td>
<td>INR, with a GST invoice</td>
</tr>
</tbody>
</table></div>
<p>Atlas figures are from MongoDB’s official pricing page, checked on 7 October 2026. Compare like with
like: an Atlas dedicated cluster runs as a three-node replica set, which is part of why it costs more.
For PostgreSQL, <a href="/managed-postgresql-india/">Voroa’s managed PostgreSQL</a> runs in Mumbai next to the app
you deploy from GitHub; all sizes are on the <a href="/pricing/">pricing page</a>.</p>
<h2 id="how-to-choose-in-one-minute" tabindex="-1"><a class="header-anchor" href="#how-to-choose-in-one-minute">How to choose in one minute</a></h2>
<ul>
<li>Your data has clear relationships, you need reports, or money is involved: <strong>PostgreSQL</strong>.</li>
<li>Your data is self-contained documents of changing shape, read and written whole: <strong>MongoDB</strong>.</li>
<li>You are unsure: start with <strong>PostgreSQL</strong> and use <code>jsonb</code> for the flexible parts. Moving from
PostgreSQL to documents later is easier than the reverse.</li>
</ul>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-postgresql-faster-than-mongodb" tabindex="-1"><a class="header-anchor" href="#is-postgresql-faster-than-mongodb">Is PostgreSQL faster than MongoDB?</a></h3>
<p>It depends on the query. MongoDB is often faster for reading one nested document; PostgreSQL is usually
faster for joins, grouping and reports. For most apps, indexes matter far more than the engine.</p>
<h3 id="can-i-use-postgresql-with-nodejs-and-express" tabindex="-1"><a class="header-anchor" href="#can-i-use-postgresql-with-nodejs-and-express">Can I use PostgreSQL with Node.js and Express?</a></h3>
<p>Yes. Use Prisma, Drizzle, Sequelize or the <code>pg</code> driver; a MERN app becomes a “PERN” app. Deploying it
is covered in <a href="/docs/deploy-nodejs/">deploying a Node.js app</a>.</p>
<h3 id="should-a-mern-app-switch-to-postgresql" tabindex="-1"><a class="header-anchor" href="#should-a-mern-app-switch-to-postgresql">Should a MERN app switch to PostgreSQL?</a></h3>
<p>Only if relationships or transactions are causing real pain. A working app on MongoDB does not need to
move. A new app with relational data is easier to start on PostgreSQL.</p>
<h3 id="is-mongodb-still-open-source" tabindex="-1"><a class="header-anchor" href="#is-mongodb-still-open-source">Is MongoDB still open source?</a></h3>
<p>It is source-available under the SSPL, which the Open Source Initiative does not recognise as an open
source licence. You can still use it freely inside your own application.</p>
<h2 id="start-with-postgresql-in-mumbai" tabindex="-1"><a class="header-anchor" href="#start-with-postgresql-in-mumbai">Start with PostgreSQL in Mumbai</a></h2>
<p>Create a free PostgreSQL database, connect it with one <code>DATABASE_URL</code>, and keep flexible fields in
<code>jsonb</code>. The steps are in <a href="/docs/postgresql-creating-connecting/">creating and connecting a database</a>,
and the hosted options are compared in <a href="/blog/supabase-alternative-india/">our Supabase alternative guide</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Supabase Alternative in India: Postgres Billed in INR</title>
      <link>https://getvoroa.com/blog/supabase-alternative-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/supabase-alternative-india/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[Looking for a Supabase alternative in India? See when plain managed PostgreSQL in Mumbai, billed in INR with a GST invoice, can replace your Supabase project.]]></description>
      <content:encoded><![CDATA[<p>If you are looking for a Supabase alternative in India, start with one question: which parts of
Supabase does your app actually use? This guide is for developers and small teams who started on
Supabase and now want a bill in rupees with a GST invoice, a database only their server can reach,
or fewer free tier limits. It explains
what Supabase gives you, what it costs, and when a plain managed PostgreSQL database is enough.</p>
<h2 id="why-developers-in-india-look-for-a-supabase-alternative" tabindex="-1"><a class="header-anchor" href="#why-developers-in-india-look-for-a-supabase-alternative">Why developers in India look for a Supabase alternative</a></h2>
<p>We went through developer forums, news reports, Supabase’s own docs and its status page. The same
reasons come up again and again, and most teams have more than one.</p>
<h3 id="the-february-2026-access-block" tabindex="-1"><a class="header-anchor" href="#the-february-2026-access-block">The February 2026 access block</a></h3>
<p>On 24 February 2026, according to news reports, a blocking order under Section 69A of the IT Act reached Indian internet
providers. Users on Jio, Airtel and ACT Fibernet could no longer reach <code>*.supabase.co</code>, the address
every Supabase app calls for its data and logins. Supabase’s
<a href="https://status.supabase.com/incidents/xmgq69x4brfk">status page</a> logged it as an India-only DNS
problem, and access came back on 5 March 2026.</p>
<p>For about ten days, apps that called Supabase from the browser showed empty screens and failed logins
to anyone on those networks. Developers could not fix it from their side, because you cannot ask
thousands of users to change their DNS. The workarounds were a VPN, a proxy, or a paid plan with a
custom domain. Many teams came out of those ten days wanting fewer outside services between their users
and their data.</p>
<h3 id="free-projects-that-go-to-sleep" tabindex="-1"><a class="header-anchor" href="#free-projects-that-go-to-sleep">Free projects that go to sleep</a></h3>
<p>A free Supabase project is paused after one week with too little database activity. That is fine for
experiments. It hurts a college project, a portfolio demo or an early product with few visitors,
because the first visitor after a quiet week finds the app broken. A free organisation that goes over
its usage quota can also be restricted until the next billing cycle.</p>
<h3 id="the-bill-is-more-than-the-25-headline" tabindex="-1"><a class="header-anchor" href="#the-bill-is-more-than-the-25-headline">The bill is more than the $25 headline</a></h3>
<p>The Pro plan starts at $25 a month per organisation, and that covers one small compute instance.
Every extra project, such as a separate staging copy, adds its own compute. Bandwidth past the
included amount, a dedicated IPv4 address and a custom domain are paid add-ons. None of this is
hidden, but the monthly total is often well above the first number people see.</p>
<h3 id="paying-in-dollars-from-india" tabindex="-1"><a class="header-anchor" href="#paying-in-dollars-from-india">Paying in dollars from India</a></h3>
<p>Supabase prices and invoices in US dollars. An Indian card adds foreign transaction charges, the rupee
amount changes every month, and some Indian cards decline international recurring payments under RBI’s
e-mandate rules. A GST-registered business can still claim credit, but it has to pay the GST itself
under reverse charge. With an Indian supplier, the GST is already on the invoice.</p>
<h3 id="a-database-the-browser-can-reach" tabindex="-1"><a class="header-anchor" href="#a-database-the-browser-can-reach">A database the browser can reach</a></h3>
<p>In the usual Supabase setup, the browser talks to the database through Supabase’s API, and row level
security policies decide what each user can see. When those policies are right, this works well.
When row level security is turned off on a table the API exposes, anyone with the public key in the
page can read it, and often change it. Teams
that write their own backend often prefer a database that only their server can reach.</p>
<h3 id="they-only-use-the-database" tabindex="-1"><a class="header-anchor" href="#they-only-use-the-database">They only use the database</a></h3>
<p>Many apps never use Supabase auth, storage or realtime. They connect through an ORM and use Supabase
as a hosted PostgreSQL. For them, the rest of the platform is extra cost and complexity.</p>
<h2 id="what-supabase-gives-you-beyond-the-database" tabindex="-1"><a class="header-anchor" href="#what-supabase-gives-you-beyond-the-database">What Supabase gives you beyond the database</a></h2>
<p>Supabase is a backend-as-a-service built on PostgreSQL. Next to the database it adds:</p>
<ul>
<li><strong>Auth</strong>: sign-up, login and social providers</li>
<li><strong>Storage</strong>: file uploads with access rules</li>
<li><strong>Realtime</strong>: table changes pushed to the browser</li>
<li><strong>Auto-generated APIs</strong>: REST and GraphQL over your tables</li>
<li><strong>Edge functions</strong>: small server functions</li>
</ul>
<p>If your app uses several of these, Supabase is doing real work for you. Moving off it means replacing
each piece you use, so be honest about that list before you compare prices.</p>
<h2 id="what-supabase-costs-in-short" tabindex="-1"><a class="header-anchor" href="#what-supabase-costs-in-short">What Supabase costs, in short</a></h2>
<p>These figures are from the official <a href="https://supabase.com/pricing">Supabase pricing page</a>, checked on
7 October 2026.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Free plan</th>
<th>Pro plan</th>
</tr>
</thead>
<tbody>
<tr>
<td>Price</td>
<td>$0</td>
<td>from $25 per month</td>
</tr>
<tr>
<td>Database size</td>
<td>500 MB</td>
<td>8 GB disk per project included</td>
</tr>
<tr>
<td>Active projects</td>
<td>2</td>
<td>Billed per project</td>
</tr>
<tr>
<td>When unused</td>
<td>Paused after 1 week of inactivity</td>
<td>Not paused</td>
</tr>
<tr>
<td>Monthly active users</td>
<td>50,000</td>
<td>100,000</td>
</tr>
<tr>
<td>File storage</td>
<td>1 GB</td>
<td>100 GB</td>
</tr>
<tr>
<td>Egress</td>
<td>5 GB</td>
<td>250 GB</td>
</tr>
<tr>
<td>Automatic backups</td>
<td>Not included</td>
<td>Included</td>
</tr>
<tr>
<td>Currency</td>
<td>USD</td>
<td>USD</td>
</tr>
</tbody>
</table></div>
<p>For a team in India, two rows matter most. A paused free project means a demo goes offline when
nobody opens it for a week. And a dollar bill on an Indian card changes in rupees every month and can
carry foreign transaction charges. The full breakdown is in
<a href="/blog/supabase-pricing-india/">Supabase pricing in India</a>.</p>
<h2 id="when-plain-managed-postgresql-is-enough" tabindex="-1"><a class="header-anchor" href="#when-plain-managed-postgresql-is-enough">When plain managed PostgreSQL is enough</a></h2>
<p>Many apps use Supabase only as a PostgreSQL database. They connect through an ORM such as Prisma,
Drizzle, SQLAlchemy or the Django ORM, and handle login and file uploads in their own backend code.</p>
<p>For those apps the only thing that matters is a reliable <code>DATABASE_URL</code>. A managed PostgreSQL service
is then a direct swap: the same engine, the same schema, the same queries. Nothing in your code needs
to know the database moved.</p>
<p>Signs you are in this group:</p>
<ol>
<li>Your backend, not the browser, talks to the database.</li>
<li>You do not call <code>supabase.auth</code> or <code>supabase.storage</code> anywhere.</li>
<li>You would be happy if the Supabase dashboard were just a SQL console.</li>
</ol>
<h2 id="voroa-as-a-supabase-alternative" tabindex="-1"><a class="header-anchor" href="#voroa-as-a-supabase-alternative">Voroa as a Supabase alternative</a></h2>
<p><a href="/managed-postgresql-india/">Voroa’s managed PostgreSQL in Mumbai</a> runs next to the service you deploy
from GitHub, on one account and one bill. Sizes are priced in rupees, plus GST:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Size</th>
<th>Price per month</th>
<th>Memory</th>
<th>Storage included</th>
</tr>
</thead>
<tbody>
<tr>
<td>DB Nano</td>
<td>Free, no card</td>
<td>256 MB</td>
<td>1 GB</td>
</tr>
<tr>
<td>DB Starter</td>
<td>Rs 299</td>
<td>1 GB</td>
<td>1 GB</td>
</tr>
<tr>
<td>DB Basic</td>
<td>Rs 499</td>
<td>1 GB</td>
<td>5 GB</td>
</tr>
<tr>
<td>DB Standard</td>
<td>Rs 999</td>
<td>2 GB</td>
<td>10 GB</td>
</tr>
</tbody>
</table></div>
<p>Bigger sizes go up to 8 GB of RAM, and a paid size can add storage at any time without recreating the
database. Current prices are always on the <a href="/pricing/">Voroa pricing page</a>.</p>
<p><img src="https://media.getvoroa.com/img/62f0379813753b5a.png" alt="The Connect tab of a Voroa PostgreSQL database in Mumbai, showing the DATABASE_URL to use from an app on Voroa" width="720" loading="eager" decoding="async"></p>
<p>Every size runs real PostgreSQL 18 (17 or 16 on the free size) with a SQL console in the dashboard.
Extensions such as trigram search and UUID generation are one click each; the full list is in
<a href="/docs/postgres-extensions/">supported PostgreSQL extensions</a>. Paid sizes can take nightly backups,
which you turn on with one switch, and a database created on a paid size also offers vector search
for embeddings.</p>
<h2 id="supabase-vs-voroa-side-by-side" tabindex="-1"><a class="header-anchor" href="#supabase-vs-voroa-side-by-side">Supabase vs Voroa side by side</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Supabase</th>
<th>Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>PostgreSQL database</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Free database size</td>
<td>500 MB</td>
<td>1 GB</td>
</tr>
<tr>
<td>Free project when unused</td>
<td>Paused after 1 week</td>
<td>Not paused today</td>
</tr>
<tr>
<td>First paid plan</td>
<td>from $25 per month</td>
<td>Rs 299 per month, plus GST</td>
</tr>
<tr>
<td>Billed in</td>
<td>USD</td>
<td>INR, with a GST invoice</td>
</tr>
<tr>
<td>Backups</td>
<td>Pro plan and above</td>
<td>Nightly on paid sizes, once you turn them on</td>
</tr>
<tr>
<td>Auth, file storage, realtime</td>
<td>Built in</td>
<td>Not included</td>
</tr>
<tr>
<td>Auto-generated REST and GraphQL APIs</td>
<td>Built in</td>
<td>Not included</td>
</tr>
<tr>
<td>Deploy your app from GitHub</td>
<td>No, edge functions only</td>
<td>Yes, web services and workers</td>
</tr>
<tr>
<td>Region</td>
<td>Chosen per project, Mumbai included</td>
<td>Mumbai</td>
</tr>
</tbody>
</table></div>
<h2 id="what-voroa-does-not-replace" tabindex="-1"><a class="header-anchor" href="#what-voroa-does-not-replace">What Voroa does not replace</a></h2>
<p>Voroa does not include Supabase’s auth, file storage, realtime or auto-generated APIs. If your app
depends on them, you would need a library or service for each, and staying on Supabase may be the
simpler choice. Voroa fits best when you want plain PostgreSQL, your own backend code, data in India
and an INR bill with a GST invoice.</p>
<h2 id="how-to-move-your-data-off-supabase" tabindex="-1"><a class="header-anchor" href="#how-to-move-your-data-off-supabase">How to move your data off Supabase</a></h2>
<p><img src="https://media.getvoroa.com/img/a366d3ea043933e1.png" alt="Diagram: copy the Supabase connection string, paste it in the Voroa Import tab, check and start the import, then point DATABASE_URL at Voroa" width="700" loading="lazy" decoding="async"></p>
<p>You do not need to create dump files. Voroa’s import copies the database on its own servers:</p>
<ol>
<li>Create an empty PostgreSQL database on Voroa, on PostgreSQL 17 or 18. New Supabase projects run 17,
and an import only goes into the same version or a newer one. The guide to
<a href="/docs/postgresql-creating-connecting/">creating and connecting a database</a> covers this step.</li>
<li>In Supabase, open <strong>Connect</strong> and copy the <strong>Session pooler</strong> connection string (port 5432). The
direct connection string only works over IPv6 unless you buy Supabase’s IPv4 add-on, and Voroa’s
import connects over IPv4.</li>
<li>Open the database’s <strong>Import</strong> tab, paste the string, select <strong>Check</strong>, then <strong>Continue</strong>.</li>
<li>When it finishes, point <code>DATABASE_URL</code> in your app at the Voroa database and redeploy.</li>
</ol>
<p><img src="https://media.getvoroa.com/img/ca2ec2868091a69a.png" alt="The Import tab of a Voroa PostgreSQL database, with a field for the existing connection string and a Check button" width="720" loading="lazy" decoding="async"></p>
<p>Your own schemas, tables, data and indexes come across. Supabase’s internal <code>auth</code>, <code>storage</code> and
<code>vault</code> schemas do not, because they belong to Supabase and cannot run anywhere else. Check two things
before you import: tables with a foreign key to <code>auth.users</code>, and row level security policies that call
<code>auth.uid()</code>. Both point at Supabase’s auth, so change them to your own users table first, or drop them
if your backend already checks who can see what. The full
walkthrough is in <a href="/docs/import-existing-database/">importing an existing database</a>. If you prefer
<code>pg_dump</code> and <code>pg_restore</code>, PostgreSQL’s own guide to
<a href="https://www.postgresql.org/docs/current/backup-dump.html">SQL dump and restore</a> works against Voroa too.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-supabase-free-forever" tabindex="-1"><a class="header-anchor" href="#is-supabase-free-forever">Is Supabase free forever?</a></h3>
<p>The free plan has no time limit, but it is capped at 500 MB of database and two active projects, and
a free project is paused after a week without activity. Every limit is listed in
<a href="/blog/supabase-free-tier-limits/">Supabase free tier limits</a>.</p>
<h3 id="can-i-keep-using-prisma-or-drizzle-after-moving" tabindex="-1"><a class="header-anchor" href="#can-i-keep-using-prisma-or-drizzle-after-moving">Can I keep using Prisma or Drizzle after moving?</a></h3>
<p>Yes. Both talk to PostgreSQL through a connection string, so you change <code>DATABASE_URL</code> and keep your
schema and migrations as they are.</p>
<h3 id="is-there-a-free-supabase-alternative-in-india" tabindex="-1"><a class="header-anchor" href="#is-there-a-free-supabase-alternative-in-india">Is there a free Supabase alternative in India?</a></h3>
<p>Yes. Voroa’s DB Nano size is free, needs no card, and includes 1 GB of storage in Mumbai. It suits a
student project, a demo or an early product, and you can move to a paid size later without recreating
the database.</p>
<h3 id="is-my-data-stored-in-india-on-voroa" tabindex="-1"><a class="header-anchor" href="#is-my-data-stored-in-india-on-voroa">Is my data stored in India on Voroa?</a></h3>
<p>Yes. Voroa runs in Mumbai, and the database sits in the same region as the service that uses it.</p>
<h3 id="i-am-also-moving-my-app-not-just-the-database-where-do-i-start" tabindex="-1"><a class="header-anchor" href="#i-am-also-moving-my-app-not-just-the-database-where-do-i-start">I am also moving my app, not just the database. Where do I start?</a></h3>
<p>The steps are similar to any platform move. Our guide on
<a href="/blog/railway-alternative-india/">moving off Railway</a> walks through deploying the app side.</p>
<h2 id="other-supabase-alternatives" tabindex="-1"><a class="header-anchor" href="#other-supabase-alternatives">Other Supabase alternatives</a></h2>
<p>Voroa is not the only option, and the right one depends on what you used Supabase for:</p>
<ul>
<li><strong>Firebase</strong>: Google’s backend with auth and a document database. A fit for mobile-first apps, but it
is not PostgreSQL, so moving relational data means remodelling it.</li>
<li><strong>Appwrite</strong>: an open-source backend with auth, database and storage that you can self-host.</li>
<li><strong>Neon</strong>: serverless PostgreSQL with branching and scale-to-zero. Database only, billed in USD.</li>
<li><strong>PocketBase</strong>: a single-file backend built on SQLite, good for small apps you host yourself.</li>
</ul>
<p>Pick Voroa when you want plain PostgreSQL next to your app in Mumbai, billed in INR with a GST invoice.</p>
<h2 id="move-your-database-to-voroa" tabindex="-1"><a class="header-anchor" href="#move-your-database-to-voroa">Move your database to Voroa</a></h2>
<p>If Supabase is only your database, you can move it in one import and keep everything else the same.
<a href="/managed-postgresql-india/#db-pricing">Create a free PostgreSQL database</a>, paste your connection
string, and your data is in Mumbai on a rupee bill.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Supabase Backup Guide: Daily Backups, PITR and Your Own Copy</title>
      <link>https://getvoroa.com/blog/supabase-backup-guide/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/supabase-backup-guide/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Migration</category>
      <description><![CDATA[How Supabase backup works on each plan, what it leaves out, how PITR is priced, and how to make and restore a dump you own, including your users and files.]]></description>
      <content:encoded><![CDATA[<p>A Supabase backup protects you from a dropped table or a bad migration, but only if you know what your
plan keeps, and what it leaves out. The free plan keeps nothing. The paid plans keep database backups
inside Supabase, but those backups do not include your uploaded files, and the CLI’s default dump does
not include your users. This guide explains each plan, point-in-time recovery and its price, what is
missing from every backup, and how to make and restore a complete copy you control.</p>
<p>Facts are from Supabase’s <a href="https://supabase.com/docs/guides/platform/backups">backups documentation</a>,
checked on 7 October 2026.</p>
<h2 id="what-each-plan-backs-up" tabindex="-1"><a class="header-anchor" href="#what-each-plan-backs-up">What each plan backs up</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Plan</th>
<th>Automatic backups</th>
<th>Kept for</th>
<th>Point-in-time recovery</th>
</tr>
</thead>
<tbody>
<tr>
<td>Free</td>
<td>None</td>
<td>-</td>
<td>Not available</td>
</tr>
<tr>
<td>Pro</td>
<td>Daily</td>
<td>7 days</td>
<td>Add-on</td>
</tr>
<tr>
<td>Team</td>
<td>Daily</td>
<td>14 days</td>
<td>Add-on</td>
</tr>
<tr>
<td>Enterprise</td>
<td>Daily</td>
<td>Up to 30 days</td>
<td>Add-on</td>
</tr>
</tbody>
</table></div>
<p>The free plan has no automatic backup at all, so a mistake on a free project cannot be undone by
Supabase. On paid plans you restore daily backups from the dashboard into the same project.</p>
<h2 id="what-a-supabase-backup-leaves-out" tabindex="-1"><a class="header-anchor" href="#what-a-supabase-backup-leaves-out">What a Supabase backup leaves out</a></h2>
<p>Three gaps catch teams out:</p>
<ol>
<li><strong>Uploaded files.</strong> Database backups do not include objects stored through Supabase Storage. A
restore brings back the rows that point at files, not the files. A file deleted last week stays
deleted.</li>
<li><strong>Passwords of custom roles.</strong> Daily backups do not store the passwords of database roles you
created yourself; set them again after a restore.</li>
<li><strong>Your users, in your own dump.</strong> The CLI’s <code>db dump</code> leaves out Supabase-managed schemas, including
<code>auth</code> and <code>storage</code>, by default. A dump made with defaults has your tables but not your sign-ups.</li>
</ol>
<h2 id="daily-backups-or-point-in-time-recovery" tabindex="-1"><a class="header-anchor" href="#daily-backups-or-point-in-time-recovery">Daily backups or point-in-time recovery</a></h2>
<p>A daily backup takes you back to the start of a day. If a bad migration ran at 4 pm, restoring the
morning’s backup also throws away every order and sign-up since then.</p>
<p>Point-in-time recovery (PITR) archives the database’s write-ahead log every two minutes, so you can
restore to almost any moment, with at most about two minutes of data lost. It needs at least the Small
compute size, and turning it on replaces daily backups.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>PITR retention</th>
<th>Price per month</th>
</tr>
</thead>
<tbody>
<tr>
<td>7 days</td>
<td>about $100</td>
</tr>
<tr>
<td>14 days</td>
<td>about $200</td>
</tr>
<tr>
<td>28 days</td>
<td>about $400</td>
</tr>
</tbody>
</table></div>
<p>Before choosing, compare the PITR price with the compute size it requires. Our
<a href="/blog/supabase-pricing-india/">Supabase pricing guide</a> has both.</p>
<h2 id="what-a-restore-actually-does" tabindex="-1"><a class="header-anchor" href="#what-a-restore-actually-does">What a restore actually does</a></h2>
<p>A restore replaces the project’s database. The project is unavailable while it runs, and the time grows
with database size. Logical replication slots and subscriptions are not carried over and must be
recreated. If you want to inspect old data without touching production, restore into a new project
instead, using Supabase’s project duplication.</p>
<h2 id="why-you-should-keep-your-own-copy" tabindex="-1"><a class="header-anchor" href="#why-you-should-keep-your-own-copy">Why you should keep your own copy</a></h2>
<p>A backup that only the provider can restore does not help if you cannot reach the provider. Indian
developers saw this in February 2026, when <code>supabase.co</code> was unreachable on several Indian networks for
about ten days. A copy you hold also lets you:</p>
<ul>
<li>restore into any PostgreSQL host if you decide to move</li>
<li>keep a monthly copy for longer than the plan’s retention</li>
<li>test a risky migration on a real copy of production</li>
</ul>
<h2 id="find-the-right-connection-string" tabindex="-1"><a class="header-anchor" href="#find-the-right-connection-string">Find the right connection string</a></h2>
<p>Use the <strong>Session pooler</strong> connection string from the <strong>Connect</strong> panel, on port 5432. The direct
connection works only over IPv6 unless you pay for the IPv4 add-on, and the transaction pooler on port
6543 is not suitable for dumps. Keep the string in an environment variable so the password does not
end up in your shell history.</p>
<h2 id="make-a-backup-you-own" tabindex="-1"><a class="header-anchor" href="#make-a-backup-you-own">Make a backup you own</a></h2>
<p><img src="https://media.getvoroa.com/img/0119dda84f7fdcc8.png" alt="Diagram: a backup you own is three files - roles.sql, schema.sql and data.sql - restored with psql in that order" width="700" loading="eager" decoding="async"></p>
<p>The Supabase CLI wraps <code>pg_dump</code> and leaves out Supabase’s internal schemas, which makes the files easy
to restore elsewhere. Make three files:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">supabase db dump --db-url &quot;$SESSION_POOLER_URL&quot; -f roles.sql --role-only
supabase db dump --db-url &quot;$SESSION_POOLER_URL&quot; -f schema.sql
supabase db dump --db-url &quot;$SESSION_POOLER_URL&quot; -f data.sql --use-copy --data-only
</code></pre>
</div>
<h3 id="include-your-users" tabindex="-1"><a class="header-anchor" href="#include-your-users">Include your users</a></h3>
<p>If you use Supabase Auth and want your users in the copy, dump the <code>auth</code> schema’s data as well, with
plain <code>pg_dump</code>:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">pg_dump &quot;$SESSION_POOLER_URL&quot; --data-only --schema=auth --exclude-table=auth.schema_migrations -f auth_data.sql
</code></pre>
</div>
<p>Restore it only into another Supabase project, where the <code>auth</code> tables already exist. The migrations
table is left out because the target project has its own; on plain
PostgreSQL there is nothing to restore it into.</p>
<h3 id="include-your-files" tabindex="-1"><a class="header-anchor" href="#include-your-files">Include your files</a></h3>
<p>Storage files need their own copy. Supabase Storage offers an S3-compatible endpoint, so a tool such as
<code>rclone</code> or the AWS CLI can copy every bucket to storage you control. Schedule it alongside the database
dump so the rows and the files match.</p>
<h2 id="restore-it-on-any-postgres" tabindex="-1"><a class="header-anchor" href="#restore-it-on-any-postgres">Restore it on any Postgres</a></h2>
<p>Restore the database files in order, stopping at the first error. The last command sets
<code>session_replication_role</code>, which needs a superuser on most managed hosts; where your user is not one,
restore the data without it and load parent tables first.</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">psql &quot;$TARGET_URL&quot; -v ON_ERROR_STOP=1 -f roles.sql
psql &quot;$TARGET_URL&quot; -v ON_ERROR_STOP=1 -f schema.sql
psql &quot;$TARGET_URL&quot; -v ON_ERROR_STOP=1 -c &quot;SET session_replication_role = replica&quot; -f data.sql
</code></pre>
</div>
<p>On a non-Supabase target, three things may fail and need editing first: grants and policies that name
Supabase’s <code>anon</code>, <code>authenticated</code> and <code>service_role</code> roles (create those roles first, or remove those
lines), foreign keys to <code>auth.users</code>, and row level security policies that call <code>auth.uid()</code>. All three
belong to Supabase. We cover them in <a href="/blog/supabase-alternative-india/">moving off Supabase</a>.</p>
<h2 id="a-simple-backup-routine" tabindex="-1"><a class="header-anchor" href="#a-simple-backup-routine">A simple backup routine</a></h2>
<ol>
<li>Before every schema change: make your own dump.</li>
<li>Weekly: dump the database and copy the storage buckets, and keep at least four weeks.</li>
<li>Monthly: restore the latest dump into a scratch database and run a few queries. A backup you have
never restored is a guess.</li>
</ol>
<h2 id="backups-on-a-managed-postgres-in-india" tabindex="-1"><a class="header-anchor" href="#backups-on-a-managed-postgres-in-india">Backups on a managed Postgres in India</a></h2>
<p>If you move your database, check the backup terms before you rely on them. On
<a href="/managed-postgresql-india/">Voroa’s managed PostgreSQL</a>, paid sizes have nightly backups that you turn
on with one switch, plus an on-demand <strong>Back up now</strong>; the free size is not backed up. Details are in
<a href="/docs/managed-databases/">managed databases</a>. To bring an existing database across without making dump
files, use <a href="/docs/import-existing-database/">the database import</a>, which copies from a connection
string.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="does-supabase-back-up-free-projects" tabindex="-1"><a class="header-anchor" href="#does-supabase-back-up-free-projects">Does Supabase back up free projects?</a></h3>
<p>No. The free plan has no automatic backups. Make your own dump regularly, especially before changing
the schema. Other free-plan gaps are in <a href="/blog/supabase-free-tier-limits/">Supabase free tier limits</a>.</p>
<h3 id="does-a-supabase-backup-include-storage-files" tabindex="-1"><a class="header-anchor" href="#does-a-supabase-backup-include-storage-files">Does a Supabase backup include Storage files?</a></h3>
<p>No. Database backups cover the database only. Copy your buckets separately through the S3-compatible
endpoint.</p>
<h3 id="can-i-restore-a-supabase-backup-to-a-different-provider" tabindex="-1"><a class="header-anchor" href="#can-i-restore-a-supabase-backup-to-a-different-provider">Can I restore a Supabase backup to a different provider?</a></h3>
<p>The built-in backups are restored from the Supabase dashboard into a Supabase project. A dump you make
yourself, as above, restores into any PostgreSQL database.</p>
<h3 id="how-much-data-can-i-lose-with-pitr" tabindex="-1"><a class="header-anchor" href="#how-much-data-can-i-lose-with-pitr">How much data can I lose with PITR?</a></h3>
<p>Supabase archives the write-ahead log every two minutes, so in the worst case about two minutes of
writes.</p>
<h2 id="keep-your-database-and-its-backups-in-india" tabindex="-1"><a class="header-anchor" href="#keep-your-database-and-its-backups-in-india">Keep your database and its backups in India</a></h2>
<p>Create a PostgreSQL database in Mumbai, turn on nightly backups on a paid size, and keep your own dump
alongside. Steps for connecting are in <a href="/docs/postgresql-creating-connecting/">creating and connecting a database</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Is Supabase Postgres? Yes - and Here Is What It Adds</title>
      <link>https://getvoroa.com/blog/supabase-database-postgres-explained/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/supabase-database-postgres-explained/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <description><![CDATA[Is Supabase Postgres? Yes. Every project is a full PostgreSQL database, with an API, auth, realtime and storage around it. How it works, limits and roles.]]></description>
      <content:encoded><![CDATA[<p>Is Supabase Postgres? Yes - the Supabase database is a real PostgreSQL database, not a custom engine.
What makes Supabase feel different is the set of services built around it, and the way security moves
into the database itself. This guide is for developers who want to know exactly which part is “just
Postgres” and which part is Supabase: how a request flows, the schemas and roles it creates, the four
ways to connect, the limits that apply, and when the database on its own is all you need. Every
Supabase fact here was checked against Supabase’s own docs on 14 November 2026.</p>
<h2 id="is-supabase-postgres" tabindex="-1"><a class="header-anchor" href="#is-supabase-postgres">Is Supabase Postgres?</a></h2>
<p>Yes. Every Supabase project gets its own full PostgreSQL database, and Supabase’s changelog says new
projects default to PostgreSQL 17. It is not a fork or a look-alike engine: you connect with <code>psql</code>, pgAdmin or
any Postgres driver. Supabase adds services around that database - an auto-generated API, Auth,
Realtime and Storage - and they all read and write the same Postgres.</p>
<p>Supabase’s docs put it plainly: every project gets “a full Postgres database, not a Postgres
abstraction”, and “we do not abstract the Postgres database - you can access it and use it with full
privileges.” Its architecture guide also says Supabase prefers to get missing features added to
upstream Postgres rather than run its own fork.</p>
<h2 id="what-type-of-database-is-supabase" tabindex="-1"><a class="header-anchor" href="#what-type-of-database-is-supabase">What type of database is Supabase?</a></h2>
<p>Supabase is a relational, SQL database: tables, rows, foreign keys, joins and transactions, exactly as
in any PostgreSQL server. It is not a document or NoSQL store, even though the client library lets a
browser query it in a way that can feel like one. Extensions such as <code>pgvector</code> (for embeddings),
PostGIS and <code>pg_cron</code> add to that same database rather than replacing it.</p>
<p>Supabase as a product is more than the database. It is a backend platform: an auto-generated API,
login, file storage, realtime updates and functions, all built on that one Postgres. You can connect to
it with Prisma, Drizzle, SQLAlchemy or any other Postgres client, exactly as you would to any other
Postgres server.</p>
<h2 id="how-supabase-works" tabindex="-1"><a class="header-anchor" href="#how-supabase-works">How Supabase works</a></h2>
<p>A typical request from a browser or mobile app goes like this:</p>
<ol>
<li>The app calls your project’s URL with the public API key, and the signed-in user’s token if there
is one.</li>
<li>An API gateway routes the call to the right service: the REST API, Auth, Realtime or Storage.</li>
<li>That service talks to Postgres. For data requests, it switches to the <code>anon</code> or <code>authenticated</code>
role for that one request.</li>
<li>Row level security policies in Postgres decide which rows the request may read or change.</li>
<li>When a row changes, Realtime can stream the change back to every subscribed browser.</li>
</ol>
<p>Your own server can skip all of this and connect straight to Postgres with a connection string, like
any other database.</p>
<h2 id="the-services-around-the-database" tabindex="-1"><a class="header-anchor" href="#the-services-around-the-database">The services around the database</a></h2>
<p><img src="https://media.getvoroa.com/img/4f0235c47540e1f2.png" alt="Diagram: requests from the browser or app pass through an API gateway to the auto REST API, auth and realtime services, which all use the PostgreSQL database" width="700" loading="eager" decoding="async"></p>
<p>According to Supabase’s <a href="https://supabase.com/docs/guides/getting-started/architecture">architecture guide</a>,
a project combines several open-source pieces:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Layer</th>
<th>What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td>PostgREST</td>
<td>Turns your tables into a REST API the browser can call</td>
</tr>
<tr>
<td>Auth</td>
<td>Signs users up and in, and issues tokens the database can check</td>
</tr>
<tr>
<td>Realtime</td>
<td>Streams table changes to browsers over websockets</td>
</tr>
<tr>
<td>Storage</td>
<td>Stores files, with their permissions kept in the database</td>
</tr>
<tr>
<td>Edge Functions</td>
<td>Runs your own server-side TypeScript and JavaScript code</td>
</tr>
<tr>
<td>Supavisor</td>
<td>Pools connections so many clients can share the database</td>
</tr>
<tr>
<td>API gateway</td>
<td>The single public entry point that routes traffic to the services above</td>
</tr>
</tbody>
</table></div>
<h2 id="where-the-database-runs" tabindex="-1"><a class="header-anchor" href="#where-the-database-runs">Where the database runs</a></h2>
<p>Supabase lists 17 regions, including South Asia (Mumbai), <code>ap-south-1</code>. Its regions page says your
Postgres database, Auth service and Storage objects stay in the region you choose. If you pick a broad
general area instead of a specific region, Supabase places the project in an available region within
that area, so pick Mumbai by name if you need the data in India.</p>
<h2 id="the-schemas-supabase-creates" tabindex="-1"><a class="header-anchor" href="#the-schemas-supabase-creates">The schemas Supabase creates</a></h2>
<p>Open a new project’s database and you will find more than your own tables:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Schema</th>
<th>What lives there</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>public</code></td>
<td>Your application tables. These are what the REST API exposes</td>
</tr>
<tr>
<td><code>auth</code></td>
<td>Users, sessions and identities, managed by Supabase Auth</td>
</tr>
<tr>
<td><code>storage</code></td>
<td>File and bucket records, managed by Supabase Storage</td>
</tr>
<tr>
<td><code>realtime</code></td>
<td>Realtime configuration and messages</td>
</tr>
<tr>
<td><code>extensions</code></td>
<td>Where extensions such as <code>pgcrypto</code> are installed</td>
</tr>
<tr>
<td><code>vault</code></td>
<td>Encrypted secrets, through the Vault extension</td>
</tr>
<tr>
<td><code>graphql</code></td>
<td>The GraphQL API, through the <code>pg_graphql</code> extension</td>
</tr>
</tbody>
</table></div>
<p>Treat <code>auth</code>, <code>storage</code> and <code>realtime</code> as Supabase’s own. Reading from them is fine; changing their
structure can break the services that own them.</p>
<h2 id="the-roles-and-how-security-moves-into-the-database" tabindex="-1"><a class="header-anchor" href="#the-roles-and-how-security-moves-into-the-database">The roles, and how security moves into the database</a></h2>
<p>Supabase creates Postgres roles that the API switches between for each request:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Role</th>
<th>Used for</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>anon</code></td>
<td>Requests from users who are not signed in</td>
</tr>
<tr>
<td><code>authenticated</code></td>
<td>Requests from signed-in users</td>
</tr>
<tr>
<td><code>service_role</code></td>
<td>Trusted server code; bypasses row level security</td>
</tr>
<tr>
<td><code>authenticator</code></td>
<td>The API’s own login, which checks the token and switches role</td>
</tr>
<tr>
<td><code>postgres</code></td>
<td>The admin role you use from the dashboard and migrations</td>
</tr>
</tbody>
</table></div>
<p>When the browser calls the REST API, the request runs as <code>anon</code> or <code>authenticated</code>, and row level
security policies in Postgres decide which rows it may touch:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY &quot;users read their own orders&quot;
ON orders FOR SELECT
TO authenticated
USING (user_id = auth.uid());
</code></pre>
</div>
<p><code>auth.uid()</code> returns the signed-in user’s ID from the request’s token. This is the heart of the
Supabase model: your SQL is your access control. It also means a table exposed by the API with row
level security turned off is readable, and often writable, by anyone with the public key in the page.</p>
<h2 id="four-ways-to-connect" tabindex="-1"><a class="header-anchor" href="#four-ways-to-connect">Four ways to connect</a></h2>
<p>From Supabase’s <a href="https://supabase.com/docs/guides/database/connecting-to-postgres">connection guide</a>:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Mode</th>
<th>Host and port</th>
<th>Network</th>
<th>Best for</th>
</tr>
</thead>
<tbody>
<tr>
<td>Direct</td>
<td><code>db.&lt;project-ref&gt;.supabase.co:5432</code></td>
<td>IPv6, or IPv4 with the paid add-on</td>
<td>Long-running servers on IPv6</td>
</tr>
<tr>
<td>Session pooler</td>
<td>pooler host, port 5432</td>
<td>IPv4</td>
<td>Long-running servers on IPv4, migrations, dumps</td>
</tr>
<tr>
<td>Transaction pooler</td>
<td>pooler host, port 6543</td>
<td>IPv4</td>
<td>Serverless functions with many short connections</td>
</tr>
<tr>
<td>Dedicated pooler</td>
<td><code>db.&lt;project-ref&gt;.supabase.co:6543</code></td>
<td>IPv6, or IPv4 with the add-on</td>
<td>High-traffic paid projects; transaction mode only</td>
</tr>
</tbody>
</table></div>
<p>Two details cause most connection errors. On the shared pooler, the username is
<code>postgres.&lt;project-ref&gt;</code>, not just <code>postgres</code>. And the shared pooler does not support prepared
statements in transaction mode, so ORMs such as Drizzle need <code>prepare: false</code>, and Prisma migrations
need a separate direct or session URL.</p>
<h2 id="connection-limits-by-size" tabindex="-1"><a class="header-anchor" href="#connection-limits-by-size">Connection limits by size</a></h2>
<p>Each compute size allows a fixed number of direct connections, and more through the pooler:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Size</th>
<th>Direct connections</th>
<th>Pooler clients</th>
</tr>
</thead>
<tbody>
<tr>
<td>Nano and Micro</td>
<td>60</td>
<td>200</td>
</tr>
<tr>
<td>Small</td>
<td>90</td>
<td>400</td>
</tr>
<tr>
<td>Medium</td>
<td>120</td>
<td>600</td>
</tr>
<tr>
<td>Large</td>
<td>160</td>
<td>800</td>
</tr>
<tr>
<td>XL</td>
<td>240</td>
<td>1,000</td>
</tr>
</tbody>
</table></div>
<p>Supabase describes these as recommended values that can be changed. A busy app should connect through
a pool rather than opening a connection per request.</p>
<h2 id="database-size-disk-size-and-read-only-mode" tabindex="-1"><a class="header-anchor" href="#database-size-disk-size-and-read-only-mode">Database size, disk size and read-only mode</a></h2>
<p>Supabase separates two numbers. <strong>Database size</strong> is your data, indexes and materialised views.
<strong>Disk size</strong> adds the write-ahead log and other files Postgres needs.</p>
<p>On the free plan, 500 MB of database size is a billing quota, not the read-only trigger. The free disk
is a fixed 2 GB, and the database goes read-only when disk usage reaches 95%. On paid plans the disk
grows by itself: at 90% full it expands by 50%, up to 200 GB at a time and four times in a rolling 24
hours. If it still reaches 95% after those four resizes, the project goes read-only. Supabase explains
this in its <a href="https://supabase.com/docs/guides/platform/database-size">database size guide</a>.</p>
<h2 id="what-is-portable-and-what-is-not" tabindex="-1"><a class="header-anchor" href="#what-is-portable-and-what-is-not">What is portable, and what is not</a></h2>
<p>Your tables, rows, indexes, views and functions are standard PostgreSQL, and a dump of them restores on
any Postgres host. Supabase itself points to standard tools such as <code>pg_dump</code> for moving in and out.
These parts only make sense inside Supabase:</p>
<ul>
<li>the <code>auth</code>, <code>storage</code> and <code>realtime</code> schemas</li>
<li>policies that call <code>auth.uid()</code> and foreign keys to <code>auth.users</code></li>
<li>the <code>anon</code>, <code>authenticated</code> and <code>service_role</code> roles</li>
</ul>
<p>If you ever move, these are the parts to rewrite. How to make a copy you own is covered in our
<a href="/blog/supabase-backup-guide/">Supabase backup guide</a>.</p>
<h2 id="when-you-only-need-the-postgres" tabindex="-1"><a class="header-anchor" href="#when-you-only-need-the-postgres">When you only need the Postgres</a></h2>
<p>If your backend is your own server, written in Node.js, Python or Go, and it handles login itself,
the services around the Supabase database are unused. Your server needs one thing: a reliable
<code>DATABASE_URL</code>.</p>
<p>For that case, a managed PostgreSQL service is simpler. <a href="/managed-postgresql-india/">Voroa’s managed PostgreSQL in
Mumbai</a> runs next to your app, is billed in rupees, and is reached by your
server rather than the browser. The extensions it supports are in
<a href="/docs/postgres-extensions/">PostgreSQL extensions on Voroa</a>, and pooling is covered in
<a href="/docs/postgresql-connection-pooling/">PostgreSQL connection pooling</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="does-supabase-use-postgresql-or-mysql" tabindex="-1"><a class="header-anchor" href="#does-supabase-use-postgresql-or-mysql">Does Supabase use PostgreSQL or MySQL?</a></h3>
<p>PostgreSQL. Every Supabase project is a Postgres database, and Supabase’s docs describe no MySQL option.</p>
<h3 id="is-the-supabase-database-free" tabindex="-1"><a class="header-anchor" href="#is-the-supabase-database-free">Is the Supabase database free?</a></h3>
<p>The free plan includes a 500 MB database in up to two active projects, and a free project is paused
after a week of inactivity. See <a href="/blog/supabase-pricing-india/">Supabase pricing in India</a> for paid
plans.</p>
<h3 id="can-i-use-the-supabase-database-without-the-supabase-client-library" tabindex="-1"><a class="header-anchor" href="#can-i-use-the-supabase-database-without-the-supabase-client-library">Can I use the Supabase database without the Supabase client library?</a></h3>
<p>Yes. Use any Postgres driver or ORM with a connection string. The client library is only needed for the
REST API, auth, storage and realtime.</p>
<h3 id="can-a-supabase-database-run-in-india" tabindex="-1"><a class="header-anchor" href="#can-a-supabase-database-run-in-india">Can a Supabase database run in India?</a></h3>
<p>Yes. Choose South Asia (Mumbai), <code>ap-south-1</code>, when you create the project. Your database, Auth and
Storage then stay in that region.</p>
<h3 id="why-can-my-server-not-reach-the-direct-connection" tabindex="-1"><a class="header-anchor" href="#why-can-my-server-not-reach-the-direct-connection">Why can my server not reach the direct connection?</a></h3>
<p>Usually because your network or hosting platform has no IPv6. The direct host only resolves to an IPv6
address unless you buy the IPv4 add-on, so you see errors such as “Network is unreachable” or “could
not translate host name”. Use the session pooler string instead, or buy the add-on.</p>
<h3 id="which-postgres-version-does-supabase-use" tabindex="-1"><a class="header-anchor" href="#which-postgres-version-does-supabase-use">Which Postgres version does Supabase use?</a></h3>
<p>Supabase’s changelog says PostgreSQL 17 is the current default for new projects. Older projects on PostgreSQL 15 can be
upgraded from the dashboard; the project is offline while the upgrade runs.</p>
<h2 id="use-plain-postgresql-with-voroa" tabindex="-1"><a class="header-anchor" href="#use-plain-postgresql-with-voroa">Use plain PostgreSQL with Voroa</a></h2>
<p>If you only use the Supabase database and not the rest, start a free PostgreSQL database in Mumbai and
connect your server with one variable. The wider comparison is in
<a href="/blog/supabase-alternative-india/">choosing a Supabase alternative</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Supabase Free Tier Limits: What Happens When You Hit Each</title>
      <link>https://getvoroa.com/blog/supabase-free-tier-limits/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/supabase-free-tier-limits/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Free tier</category>
      <description><![CDATA[Every Supabase free tier limit for 2026 - database size, pausing, egress, auth emails - and exactly what happens to your app when it crosses each, with fixes.]]></description>
      <content:encoded><![CDATA[<p>The Supabase free tier is one of the most generous free backends available, which is why so many
student projects and early products in India start on it. Its limits are clear on paper, but what
happens when you cross one is less obvious, and it usually shows up at the worst moment, such as the
morning of a demo or a viva. This guide lists every limit, explains what each one does to a live app,
and gives the exact fix where there is one.</p>
<p>All figures are from Supabase’s official <a href="https://supabase.com/pricing">pricing page</a> and platform
documentation, checked on 7 October 2026.</p>
<h2 id="every-limit-on-the-free-plan" tabindex="-1"><a class="header-anchor" href="#every-limit-on-the-free-plan">Every limit on the free plan</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Resource</th>
<th>Free plan limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>Active projects</td>
<td>2 per organisation</td>
</tr>
<tr>
<td>Database size</td>
<td>500 MB per project</td>
</tr>
<tr>
<td>Compute</td>
<td>Shared CPU, 500 MB RAM</td>
</tr>
<tr>
<td>Egress (data sent out)</td>
<td>5 GB, plus 5 GB of cached egress</td>
</tr>
<tr>
<td>File storage</td>
<td>1 GB, with a 50 MB maximum per file</td>
</tr>
<tr>
<td>Monthly active users</td>
<td>50,000</td>
</tr>
<tr>
<td>Edge function invocations</td>
<td>500,000 per month</td>
</tr>
<tr>
<td>Realtime</td>
<td>200 concurrent connections, 2 million messages per month</td>
</tr>
<tr>
<td>Image transformations</td>
<td>Not included</td>
</tr>
<tr>
<td>Automatic backups</td>
<td>Not included</td>
</tr>
<tr>
<td>Logs kept</td>
<td>1 hour for auth, 1 day for API and database</td>
</tr>
<tr>
<td>Custom domain, branching, read replicas</td>
<td>Not included</td>
</tr>
<tr>
<td>Support</td>
<td>Community only</td>
</tr>
<tr>
<td>Inactivity</td>
<td>Paused after 1 week</td>
</tr>
</tbody>
</table></div>
<h2 id="what-happens-at-each-limit" tabindex="-1"><a class="header-anchor" href="#what-happens-at-each-limit">What happens at each limit</a></h2>
<p><img src="https://media.getvoroa.com/img/85ea7902ad467307.png" alt="Diagram: a quiet week pauses the project; data over 500 MB makes it read-only; going over the usage quota restricts it; a project paused for over a year can no longer be restored from the dashboard" width="700" loading="eager" decoding="async"></p>
<h3 id="a-quiet-week-pauses-the-project" tabindex="-1"><a class="header-anchor" href="#a-quiet-week-pauses-the-project">A quiet week pauses the project</a></h3>
<p>A free project is paused when it “does not receive sufficient user database activity over the past
week”, in Supabase’s words; a few real requests a day usually keeps it running. You get a warning
email about a week before the pause and a second email when it happens.</p>
<p>A paused project does not answer at all, so your app shows errors until someone opens the dashboard
and selects <strong>Resume project</strong>. It comes back with its data and settings. According to Supabase’s
<a href="https://supabase.com/docs/guides/platform/free-project-pausing">project pausing guide</a>, you can
restore it from the dashboard for up to a year.</p>
<h3 id="data-over-500-mb-makes-the-database-read-only" tabindex="-1"><a class="header-anchor" href="#data-over-500-mb-makes-the-database-read-only">Data over 500 MB makes the database read-only</a></h3>
<p>The database keeps serving reads, but every insert and update fails with an error that it cannot
execute the write in a read-only transaction. Sign-ups, orders and form submissions stop while pages
that only read data look normal, which makes this easy to miss.</p>
<p>Check your size with:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">SELECT pg_size_pretty(sum(pg_database_size(datname))) FROM pg_database;
</code></pre>
</div>
<p>To get out of read-only mode, Supabase’s
<a href="https://supabase.com/docs/guides/platform/database-size">database size guide</a> gives these steps:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">SET SESSION CHARACTERISTICS AS TRANSACTION READ WRITE;
-- delete the rows or tables you no longer need, then:
VACUUM;
SET default_transaction_read_only = 'off';
</code></pre>
</div>
<p>The first line applies only to your current session, so run all of it in one SQL editor session. If
you cannot free enough space, the other way out is upgrading to Pro.</p>
<h3 id="going-over-the-usage-quota-restricts-the-organisation" tabindex="-1"><a class="header-anchor" href="#going-over-the-usage-quota-restricts-the-organisation">Going over the usage quota restricts the organisation</a></h3>
<p>Egress, storage, function calls and other usage are counted across the whole free organisation. When
usage passes the quota, Supabase emails the billing address and starts a grace period. If usage stays
over, it can restrict the organisation: API requests start failing and new projects cannot be created.
Restrictions lift when the next billing cycle starts, or straight away if you upgrade.</p>
<h3 id="auth-sends-only-2-emails-an-hour" tabindex="-1"><a class="header-anchor" href="#auth-sends-only-2-emails-an-hour">Auth sends only 2 emails an hour</a></h3>
<p>This limit is not on the pricing page, and it catches many first launches. With Supabase’s built-in
email sender, auth emails such as sign-up confirmations, magic links and password resets are limited
to 2 per hour. A launch that brings 20 sign-ups in an hour leaves most of them waiting for an email that
never arrives.</p>
<p>The fix is to connect your own SMTP provider in the auth settings, which is allowed on the free plan
and lifts the limit. Phone OTPs and other auth endpoints have their own, higher limits.</p>
<h3 id="paused-for-over-a-year-no-dashboard-restore" tabindex="-1"><a class="header-anchor" href="#paused-for-over-a-year-no-dashboard-restore">Paused for over a year: no dashboard restore</a></h3>
<p>After the restore window, the <strong>Resume project</strong> button is gone, and Supabase’s pausing guide does not
promise another way back. Treat a project paused for over a year as lost unless you kept your own dump.</p>
<h2 id="how-to-stay-within-the-free-tier" tabindex="-1"><a class="header-anchor" href="#how-to-stay-within-the-free-tier">How to stay within the free tier</a></h2>
<ol>
<li><strong>Watch database size, not disk size.</strong> The 500 MB limit counts your data and indexes. Large text
columns, logs and old rows grow it faster than you expect; run the size query above weekly.</li>
<li><strong>Resize images before upload.</strong> Full-size photos use egress every time someone views them, and
image transformations are not on the free plan.</li>
<li><strong>Set up your own SMTP before launch.</strong> Do it before your first real users arrive, not after the
complaints.</li>
<li><strong>Export your data yourself.</strong> There are no automatic backups, so run a dump before any schema
change. Our <a href="/blog/supabase-backup-guide/">Supabase backup guide</a> shows how.</li>
</ol>
<h2 id="when-the-free-tier-is-the-wrong-choice" tabindex="-1"><a class="header-anchor" href="#when-the-free-tier-is-the-wrong-choice">When the free tier is the wrong choice</a></h2>
<p>The free tier fits learning, hackathons and prototypes. It is the wrong choice for an app that must
answer at any hour: a live client project, a store, or anything people log in to daily. The weekly
pause alone makes it unsuitable, and there is no backup if something goes wrong. Costs above the free
plan are in <a href="/blog/supabase-pricing-india/">Supabase pricing in India</a>.</p>
<h2 id="a-free-postgresql-database-in-mumbai" tabindex="-1"><a class="header-anchor" href="#a-free-postgresql-database-in-mumbai">A free PostgreSQL database in Mumbai</a></h2>
<p>If you only need PostgreSQL, <a href="/managed-postgresql-india/">Voroa’s managed PostgreSQL</a> has a free size,
DB Nano, with 1 GB of storage in Mumbai and no card needed. Free databases on Voroa are not paused
today; the policy that would apply in future is written down in advance. As storage fills you get
emails at 80% and 90%, and the database switches to read-only only near full, as described in
<a href="/docs/database-storage-limits/">database storage limits</a>. Free web services are covered in
<a href="/docs/free-instances/">free instances</a>.</p>
<p>Voroa does not include auth, file storage or realtime. If your app uses those, compare carefully using
<a href="/blog/supabase-alternative-india/">our Supabase alternative guide</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-the-supabase-free-tier-enough-for-production" tabindex="-1"><a class="header-anchor" href="#is-the-supabase-free-tier-enough-for-production">Is the Supabase free tier enough for production?</a></h3>
<p>For a small internal tool that someone opens every day, it can be. For anything customers rely on, the
weekly pause, the 2-emails-an-hour auth limit and the lack of backups are real risks.</p>
<h3 id="does-supabase-delete-my-data-when-a-project-is-paused" tabindex="-1"><a class="header-anchor" href="#does-supabase-delete-my-data-when-a-project-is-paused">Does Supabase delete my data when a project is paused?</a></h3>
<p>No. A paused project keeps its data and can be restored from the dashboard for up to a year. After
that, Supabase does not promise a way back, so keep your own dump.</p>
<h3 id="how-many-free-supabase-projects-can-i-have" tabindex="-1"><a class="header-anchor" href="#how-many-free-supabase-projects-can-i-have">How many free Supabase projects can I have?</a></h3>
<p>Two active projects per free organisation.</p>
<h3 id="why-are-my-supabase-sign-up-emails-not-arriving" tabindex="-1"><a class="header-anchor" href="#why-are-my-supabase-sign-up-emails-not-arriving">Why are my Supabase sign-up emails not arriving?</a></h3>
<p>Most often the built-in sender’s limit of 2 emails an hour. Connect your own SMTP provider in the auth
settings.</p>
<h2 id="start-a-free-postgresql-database-in-mumbai" tabindex="-1"><a class="header-anchor" href="#start-a-free-postgresql-database-in-mumbai">Start a free PostgreSQL database in Mumbai</a></h2>
<p>Create a free PostgreSQL database in Mumbai, connect it with one <code>DATABASE_URL</code>, and move to a paid
size only when your app needs more. The steps are in
<a href="/docs/postgresql-creating-connecting/">creating and connecting a database</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Supabase Pricing in India: Plans and the Real Cost in INR</title>
      <link>https://getvoroa.com/blog/supabase-pricing-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/supabase-pricing-india/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <description><![CDATA[Supabase pricing explained for India: every plan, compute size, usage rate and add-on, four worked bills, and what a dollar bill really costs on an Indian card.]]></description>
      <content:encoded><![CDATA[<p>Supabase pricing looks simple: a free plan, and a Pro plan from $25 a month. The bill you actually
pay is built from a plan fee, compute for every project, usage beyond the included amounts, and any
add-ons you switch on. From India it then arrives in US dollars on an Indian card. This guide puts
every rate in one place, works through four realistic bills step by step, and converts them to
rupees, so you can estimate your own number before you commit.</p>
<p>All figures are from Supabase’s official pricing and billing documentation, checked on
7 October 2026. Prices change, so treat the method as the lasting part and re-check the rates.</p>
<h2 id="the-four-supabase-plans" tabindex="-1"><a class="header-anchor" href="#the-four-supabase-plans">The four Supabase plans</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Free</th>
<th>Pro</th>
<th>Team</th>
<th>Enterprise</th>
</tr>
</thead>
<tbody>
<tr>
<td>Base price</td>
<td>$0</td>
<td>from $25 per month</td>
<td>from $599 per month</td>
<td>Custom</td>
</tr>
<tr>
<td>Billed per</td>
<td>Organisation</td>
<td>Organisation</td>
<td>Organisation</td>
<td>Contract</td>
</tr>
<tr>
<td>Compute credit</td>
<td>-</td>
<td>$10 per month</td>
<td>$10 per month</td>
<td>Custom</td>
</tr>
<tr>
<td>Database disk included</td>
<td>500 MB</td>
<td>8 GB per project</td>
<td>8 GB per project</td>
<td>Custom</td>
</tr>
<tr>
<td>Egress included</td>
<td>5 GB</td>
<td>250 GB</td>
<td>250 GB</td>
<td>Custom</td>
</tr>
<tr>
<td>Monthly active users</td>
<td>50,000</td>
<td>100,000</td>
<td>100,000</td>
<td>Custom</td>
</tr>
<tr>
<td>Daily backups kept</td>
<td>None</td>
<td>7 days</td>
<td>14 days</td>
<td>Up to 30 days</td>
</tr>
<tr>
<td>Inactive projects</td>
<td>Paused after 1 week</td>
<td>Not paused</td>
<td>Not paused</td>
<td>Not paused</td>
</tr>
</tbody>
</table></div>
<p>Team does not buy more capacity than Pro. It buys compliance and control: SSO for the dashboard,
longer backup retention, log retention of 28 days instead of 7, and support with SLAs. Most early-stage
teams do not need it. The free plan has its own limits, covered in detail in
<a href="/blog/supabase-free-tier-limits/">Supabase free tier limits</a>.</p>
<h2 id="compute-the-line-most-people-miss" tabindex="-1"><a class="header-anchor" href="#compute-the-line-most-people-miss">Compute: the line most people miss</a></h2>
<p>Every project runs on its own compute instance, billed by the hour whether or not anyone is using it.
The Pro plan’s $10 credit covers one Micro instance. Each additional project, such as a staging copy,
adds its own compute line. Sizes from Supabase’s
<a href="https://supabase.com/docs/guides/platform/compute-and-disk">compute and disk guide</a>:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Size</th>
<th>Per month (approx.)</th>
<th>CPU</th>
<th>RAM</th>
<th>Direct connections</th>
</tr>
</thead>
<tbody>
<tr>
<td>Nano (free plan only)</td>
<td>$0</td>
<td>Shared</td>
<td>0.5 GB</td>
<td>60</td>
</tr>
<tr>
<td>Micro</td>
<td>$10</td>
<td>Shared</td>
<td>1 GB</td>
<td>60</td>
</tr>
<tr>
<td>Small</td>
<td>$15</td>
<td>Shared</td>
<td>2 GB</td>
<td>90</td>
</tr>
<tr>
<td>Medium</td>
<td>$60</td>
<td>Shared</td>
<td>4 GB</td>
<td>120</td>
</tr>
<tr>
<td>Large</td>
<td>$110</td>
<td>2 dedicated</td>
<td>8 GB</td>
<td>160</td>
</tr>
<tr>
<td>XL</td>
<td>$210</td>
<td>4 dedicated</td>
<td>16 GB</td>
<td>240</td>
</tr>
<tr>
<td>2XL</td>
<td>$410</td>
<td>8 dedicated</td>
<td>32 GB</td>
<td>380</td>
</tr>
</tbody>
</table></div>
<p>Larger sizes go up to 64 dedicated CPUs and 256 GB of RAM. Note the jump from Small to Medium: the
price quadruples while the RAM doubles, and the CPU is still shared until Large.</p>
<h2 id="usage-rates-on-pro" tabindex="-1"><a class="header-anchor" href="#usage-rates-on-pro">Usage rates on Pro</a></h2>
<p>Usage above the included amount is billed at these rates:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Item</th>
<th>Included on Pro</th>
<th>Rate beyond that</th>
</tr>
</thead>
<tbody>
<tr>
<td>Database disk (general purpose)</td>
<td>8 GB per project</td>
<td>$0.125 per GB per month</td>
</tr>
<tr>
<td>Egress, uncached</td>
<td>250 GB</td>
<td>$0.09 per GB</td>
</tr>
<tr>
<td>Egress, cached</td>
<td>250 GB</td>
<td>$0.03 per GB</td>
</tr>
<tr>
<td>File storage</td>
<td>100 GB</td>
<td>$0.0213 per GB per month</td>
</tr>
<tr>
<td>Monthly active users</td>
<td>100,000</td>
<td>$0.00325 per user</td>
</tr>
<tr>
<td>Edge function calls</td>
<td>2 million</td>
<td>$2 per million</td>
</tr>
<tr>
<td>Realtime messages</td>
<td>5 million</td>
<td>$2.50 per million</td>
</tr>
<tr>
<td>Realtime peak connections</td>
<td>500</td>
<td>$10 per 1,000</td>
</tr>
<tr>
<td>Image transformations</td>
<td>100 origin images</td>
<td>$5 per 1,000</td>
</tr>
</tbody>
</table></div>
<p>Egress is the meter that surprises most teams. It counts data sent out of every Supabase service:
database queries, file downloads, auth and functions. An app that serves full-size images from
Supabase storage reaches 250 GB long before it reaches 100,000 users.</p>
<h2 id="add-ons-and-what-each-costs" tabindex="-1"><a class="header-anchor" href="#add-ons-and-what-each-costs">Add-ons and what each costs</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Add-on</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>Point-in-time recovery</td>
<td>$100 per month for each 7 days of retention</td>
</tr>
<tr>
<td>Custom domain</td>
<td>$10 per month per project</td>
</tr>
<tr>
<td>Dedicated IPv4 address</td>
<td>$4 per month per database, and per read replica</td>
</tr>
<tr>
<td>Branching (preview databases)</td>
<td>$0.01344 per branch per hour, about $10 a month if left running</td>
</tr>
<tr>
<td>Log drain</td>
<td>$60 per drain per month, plus events and egress</td>
</tr>
<tr>
<td>Phone MFA</td>
<td>$75 per month for the first project</td>
</tr>
<tr>
<td>Read replica</td>
<td>Billed at its own compute size, plus IPv4 if that is on</td>
</tr>
</tbody>
</table></div>
<p>Two of these matter more in India than the price suggests. The IPv4 add-on is the only way to use the
direct database connection from networks without IPv6, which includes many hosting platforms. And a
custom domain was the workaround Indian teams used when <code>supabase.co</code> was blocked on Jio, Airtel and
ACT Fibernet in February 2026.</p>
<h2 id="what-the-spend-cap-protects-and-what-it-does-not" tabindex="-1"><a class="header-anchor" href="#what-the-spend-cap-protects-and-what-it-does-not">What the spend cap protects, and what it does not</a></h2>
<p>Pro has a spend cap, switched on by default. With it on, usage beyond the quota is restricted instead
of billed. According to Supabase’s <a href="https://supabase.com/docs/guides/platform/cost-control">cost control guide</a>,
it covers disk size, egress, function calls, monthly active users, realtime, image transformations,
storage and logs.</p>
<p>It does <strong>not</strong> cover compute, branching, read replicas, custom domains, extra disk IOPS or
throughput, IPv4, log drains, phone MFA or point-in-time recovery. Those are billed in full whatever
the cap says, because you switch them on yourself. The cap prevents usage surprises; it is not a
monthly budget.</p>
<h2 id="a-supabase-pricing-calculator-you-can-do-by-hand" tabindex="-1"><a class="header-anchor" href="#a-supabase-pricing-calculator-you-can-do-by-hand">A Supabase pricing calculator you can do by hand</a></h2>
<p><img src="https://media.getvoroa.com/img/95d0e39c11f97d2f.png" alt="Diagram: a Supabase Pro bill is the $25 plan, plus compute per project minus a $10 credit, plus disk and egress over the included amounts, plus add-ons, plus the forex markup your card adds" width="700" loading="eager" decoding="async"></p>
<p>The monthly bill on Pro is:</p>
<ol>
<li><strong>$25</strong> for the organisation</li>
<li><strong>+ compute</strong> for every project, from the table above</li>
<li><strong>- $10</strong> compute credit</li>
<li><strong>+ usage</strong> above the included amounts, from the usage table</li>
<li><strong>+ add-ons</strong> you have switched on</li>
</ol>
<p>Four worked examples, in US dollars:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Setup</th>
<th>Calculation</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>Side project: one Micro project, inside all quotas</td>
<td>25 + 10 - 10</td>
<td>$25.00</td>
</tr>
<tr>
<td>Production plus staging, both Micro</td>
<td>25 + 10 + 10 - 10</td>
<td>$35.00</td>
</tr>
<tr>
<td>Startup with 10,000 users: Small, 20 GB of data, egress inside quota, custom domain, IPv4</td>
<td>25 + 15 - 10 + (12 x 0.125) + 10 + 4</td>
<td>$45.50</td>
</tr>
<tr>
<td>Growing app: Medium, 50 GB of data, 400 GB uncached egress, 7-day PITR, custom domain</td>
<td>25 + 60 - 10 + (42 x 0.125) + (150 x 0.09) + 100 + 10</td>
<td>$203.75</td>
</tr>
</tbody>
</table></div>
<p>The last example shows where bills grow: point-in-time recovery and compute are most of it, and
neither is stopped by the spend cap.</p>
<h2 id="what-it-costs-in-rupees" tabindex="-1"><a class="header-anchor" href="#what-it-costs-in-rupees">What it costs in rupees</a></h2>
<p>Supabase bills in dollars, so three things decide the rupee amount on your card:</p>
<ul>
<li><strong>The exchange rate</strong> on the day the card is charged, which moves every month.</li>
<li><strong>The forex markup.</strong> Many Indian credit cards charge about 3.5% on international transactions.</li>
<li><strong>18% GST on that markup</strong>, which takes the extra cost to about 4.1% of the bill.</li>
</ul>
<p>At an example rate of Rs 90 to the dollar:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Bill in dollars</th>
<th>At the exchange rate</th>
<th>Markup and GST on it</th>
<th>Approx. on your card</th>
</tr>
</thead>
<tbody>
<tr>
<td>$25.00</td>
<td>Rs 2,250</td>
<td>Rs 93</td>
<td>Rs 2,343</td>
</tr>
<tr>
<td>$35.00</td>
<td>Rs 3,150</td>
<td>Rs 130</td>
<td>Rs 3,280</td>
</tr>
<tr>
<td>$45.50</td>
<td>Rs 4,095</td>
<td>Rs 169</td>
<td>Rs 4,264</td>
</tr>
<tr>
<td>$203.75</td>
<td>Rs 18,338</td>
<td>Rs 757</td>
<td>Rs 19,095</td>
</tr>
</tbody>
</table></div>
<p>Swap in today’s rate to get your own number. Two more India-specific points:</p>
<ol>
<li><strong>Recurring payments above Rs 15,000.</strong> If your card runs the charge as an Indian e-mandate, RBI
rules need your approval for each payment above Rs 15,000. The last example crosses that line, so a
charge can wait for an OTP, or fail if nobody approves it.</li>
<li><strong>GST for registered businesses.</strong> A business buying a foreign service usually pays the GST itself
under reverse charge and then claims the credit; the steps are in our guide to
<a href="/docs/reverse-charge-foreign-cloud-hosting/">reverse charge on foreign hosting</a>. Check the treatment
with your accountant.</li>
</ol>
<h2 id="supabase-pricing-compared-with-a-rupee-billed-database" tabindex="-1"><a class="header-anchor" href="#supabase-pricing-compared-with-a-rupee-billed-database">Supabase pricing compared with a rupee-billed database</a></h2>
<p>If you use Supabase mainly as a PostgreSQL database, compare it with a database priced in INR.
<a href="/managed-postgresql-india/">Voroa’s managed PostgreSQL</a> runs in Mumbai and is priced per size, plus
GST, with a GST invoice:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Size</th>
<th>Price per month</th>
<th>Memory</th>
<th>Storage included</th>
</tr>
</thead>
<tbody>
<tr>
<td>DB Nano</td>
<td>Free, no card</td>
<td>256 MB</td>
<td>1 GB</td>
</tr>
<tr>
<td>DB Starter</td>
<td>Rs 299</td>
<td>1 GB</td>
<td>1 GB</td>
</tr>
<tr>
<td>DB Basic</td>
<td>Rs 499</td>
<td>1 GB</td>
<td>5 GB</td>
</tr>
<tr>
<td>DB Standard</td>
<td>Rs 999</td>
<td>2 GB</td>
<td>10 GB</td>
</tr>
</tbody>
</table></div>
<p>Extra storage on a paid size is Rs 21 per GB per month; how it works is in
<a href="/docs/database-storage-limits/">database storage limits</a>. There is no forex markup and no reverse
charge. Current prices are on the <a href="/pricing/">pricing page</a>.</p>
<p>This is not a like-for-like swap. Voroa does not include auth, file storage or realtime, so the
comparison only holds if Supabase is your database and nothing else. Read
<a href="/blog/supabase-alternative-india/">when a Supabase alternative makes sense</a> before you decide.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-the-supabase-pro-plan-25-per-project" tabindex="-1"><a class="header-anchor" href="#is-the-supabase-pro-plan-25-per-project">Is the Supabase Pro plan $25 per project?</a></h3>
<p>No. The $25 is per organisation. Compute is billed per project, and the $10 monthly credit covers one
Micro instance, so every additional project adds to the bill.</p>
<h3 id="does-supabase-have-a-pricing-calculator" tabindex="-1"><a class="header-anchor" href="#does-supabase-have-a-pricing-calculator">Does Supabase have a pricing calculator?</a></h3>
<p>Supabase shows your current usage and costs in the dashboard, and third-party calculators exist. The
five-step method above, with the rate tables, gives the same answer and works before you sign up.</p>
<h3 id="why-is-my-supabase-bill-higher-than-25" tabindex="-1"><a class="header-anchor" href="#why-is-my-supabase-bill-higher-than-25">Why is my Supabase bill higher than $25?</a></h3>
<p>Usually one of four things: a second project’s compute, egress above 250 GB, an add-on such as a custom
domain or IPv4, or point-in-time recovery. Check the invoice line by line against the tables above.</p>
<h3 id="can-i-pay-for-supabase-in-rupees" tabindex="-1"><a class="header-anchor" href="#can-i-pay-for-supabase-in-rupees">Can I pay for Supabase in rupees?</a></h3>
<p>Supabase prices and bills in US dollars. Your card converts the amount and adds its forex markup.</p>
<h2 id="moving-a-postgres-database-off-supabase-from-india" tabindex="-1"><a class="header-anchor" href="#moving-a-postgres-database-off-supabase-from-india">Moving a Postgres database off Supabase from India</a></h2>
<p>If you use Supabase mainly for its PostgreSQL database, a move is mostly about the bill. Voroa bills in
rupees from an Indian entity, with GST on the invoice, so a registered business takes the input tax
credit straight from it - no reverse-charge GST paid in cash first, and no card forex markup.</p>
<p>Voroa does not offer Supabase auth, file storage or realtime APIs, so this fits an app that uses
Supabase as a database. You can <a href="/docs/import-existing-database/">import an existing PostgreSQL database</a>
by pasting its connection string.</p>
<p>To cover the month of two bills, you can apply for <a href="/migration-credits/">migration credits</a>: up to
Rs 10,000 against your Voroa bills, granted by hand after a person reads your application.</p>
<h2 id="price-your-database-in-rupees-with-voroa" tabindex="-1"><a class="header-anchor" href="#price-your-database-in-rupees-with-voroa">Price your database in rupees with Voroa</a></h2>
<p>If you only need PostgreSQL, you can start on a free database in Mumbai and see the paid sizes in
rupees before you upgrade. Moving existing data is covered in
<a href="/docs/import-existing-database/">importing an existing database</a>.
<a href="/managed-postgresql-india/#db-pricing">See PostgreSQL sizes and prices</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Supabase vs Firebase: Which Backend Fits Your App in India?</title>
      <link>https://getvoroa.com/blog/supabase-vs-firebase-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/supabase-vs-firebase-india/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <description><![CDATA[Supabase vs Firebase for Indian developers: SQL or documents, features, free plans, a worked cost example, billing risk, offline sync and Mumbai regions.]]></description>
      <content:encoded><![CDATA[<p>Supabase vs Firebase is the first backend decision many Indian developers make, often for a college
project or a first startup MVP. Both give you a database, login, file storage and functions without
running a server. They differ in one thing that decides almost everything else: the shape of your
data. This guide compares them feature by feature, works through what the same app costs on each,
and covers the risks that matter from India.</p>
<p>Pricing figures are from the official <a href="https://firebase.google.com/pricing">Firebase pricing page</a> and
Supabase’s pricing page, checked on 7 October 2026.</p>
<h2 id="the-short-answer" tabindex="-1"><a class="header-anchor" href="#the-short-answer">The short answer</a></h2>
<ul>
<li>Choose <strong>Firebase</strong> if your app is mobile-first, must work offline, and its data is naturally a set of
documents, such as chat messages or user profiles.</li>
<li>Choose <strong>Supabase</strong> if your data is relational, such as users, orders and payments that point at each
other, and you want SQL, reports and joins.</li>
<li>Choose <strong>neither</strong> if you write your own backend and only need a database. That case is at the end.</li>
</ul>
<h2 id="sql-tables-or-json-documents" tabindex="-1"><a class="header-anchor" href="#sql-tables-or-json-documents">SQL tables or JSON documents</a></h2>
<p><img src="https://media.getvoroa.com/img/4451f627b0259502.png" alt="Diagram: relational data such as users, orders and joins fits Supabase or plain PostgreSQL; documents with offline mobile sync fit Firebase Firestore" width="700" loading="eager" decoding="async"></p>
<p>Supabase is built on PostgreSQL. Your data lives in tables with foreign keys, and one query joins them:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">sql</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-sql">SELECT u.name, o.total
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.created_at &gt; now() - interval '7 days';
</code></pre>
</div>
<p>Firebase’s main database, Firestore, stores JSON documents in collections and has no joins. To show
the same list, you either read each order and then each user, or you copy the user’s name into every
order document when you write it. Copying makes reads fast, but every copy must be updated when the
name changes.</p>
<p>A practical test: if you can draw your data as boxes with arrows between them, you want SQL.</p>
<h2 id="feature-by-feature" tabindex="-1"><a class="header-anchor" href="#feature-by-feature">Feature by feature</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Supabase</th>
<th>Firebase</th>
</tr>
</thead>
<tbody>
<tr>
<td>Database</td>
<td>PostgreSQL, relational, SQL</td>
<td>Firestore, documents, no joins</td>
</tr>
<tr>
<td>Auth, free monthly users</td>
<td>50,000</td>
<td>50,000</td>
</tr>
<tr>
<td>File storage</td>
<td>Built in</td>
<td>Built in, needs the paid Blaze plan since February 2026</td>
</tr>
<tr>
<td>Server functions</td>
<td>Edge functions (Deno)</td>
<td>Cloud Functions</td>
</tr>
<tr>
<td>Realtime updates</td>
<td>Table changes over websockets</td>
<td>Live listeners on documents</td>
</tr>
<tr>
<td>Offline sync on mobile</td>
<td>Not built in</td>
<td>Mature, built into the SDKs</td>
</tr>
<tr>
<td>Vector search for AI features</td>
<td>pgvector in PostgreSQL</td>
<td>Firestore vector search</td>
</tr>
<tr>
<td>Push notifications</td>
<td>Not included</td>
<td>Cloud Messaging</td>
</tr>
<tr>
<td>Open source and self-hostable</td>
<td>Yes</td>
<td>No</td>
</tr>
</tbody>
</table></div>
<h2 id="free-plans-compared" tabindex="-1"><a class="header-anchor" href="#free-plans-compared">Free plans compared</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Supabase Free</th>
<th>Firebase Spark</th>
</tr>
</thead>
<tbody>
<tr>
<td>Database</td>
<td>500 MB</td>
<td>1 GiB</td>
</tr>
<tr>
<td>Reads and writes</td>
<td>No per-operation limit</td>
<td>50,000 reads and 20,000 writes per day</td>
</tr>
<tr>
<td>Function calls</td>
<td>500,000 per month</td>
<td>Not on Spark; Cloud Functions need Blaze (2 million calls a month free there)</td>
</tr>
<tr>
<td>Projects</td>
<td>2 active</td>
<td>Limited by a per-account project quota</td>
</tr>
<tr>
<td>When unused</td>
<td>Paused after 1 week</td>
<td>Not paused</td>
</tr>
<tr>
<td>Backups</td>
<td>None</td>
<td>Not included on Spark</td>
</tr>
</tbody>
</table></div>
<p>The biggest practical difference is the pause. A free Supabase project that sees too little activity
for a week stops answering until someone resumes it. A free Firebase project keeps running. Every
Supabase limit is listed in <a href="/blog/supabase-free-tier-limits/">Supabase free tier limits</a>.</p>
<h2 id="what-the-same-app-costs-on-each" tabindex="-1"><a class="header-anchor" href="#what-the-same-app-costs-on-each">What the same app costs on each</a></h2>
<p>Take an app with 10,000 daily users, where each user causes about 30 database reads and 5 writes a
day. That is about 300,000 reads and 50,000 writes daily.</p>
<p><strong>On Firebase Blaze,</strong> the first 50,000 reads and 20,000 writes each day are free. The rest comes to
about 7.5 million reads and 0.9 million writes a month. At the Firestore rates Google listed for its
Mumbai location on 7 October 2026, $0.035 per 100,000 reads and $0.104 per 100,000 writes, that is
roughly $3.50 a month for the database. Check the current per-location rates before relying on it.</p>
<p><strong>On Supabase Pro,</strong> the same app costs $25 a month for the plan, with one Micro compute instance
covered by the included credit, and no per-read charge. Our <a href="/blog/supabase-pricing-india/">Supabase pricing breakdown</a>
shows how that grows with more projects and add-ons.</p>
<p>So for a small, read-light app, Firebase is often cheaper. The picture changes with scale and with
mistakes: Firestore charges every read, so a screen that re-reads a large collection on every render,
or a function stuck in a loop, multiplies the bill.</p>
<h2 id="billing-risk" tabindex="-1"><a class="header-anchor" href="#billing-risk">Billing risk</a></h2>
<p>Supabase Pro has a spend cap, on by default, that restricts most usage overages instead of billing
them. Compute and add-ons are always charged.</p>
<p>Firebase Blaze has no hard spending cap. Google’s own guide to
<a href="https://firebase.google.com/docs/projects/billing/avoid-surprise-bills">avoiding surprise bills</a>
explains that budget alerts send notifications; they do not stop charges. Set alerts on day one, and
add limits in your own code for anything that can loop.</p>
<p>Billing currency differs. Firebase runs on Google Cloud billing, and accounts with an Indian billing
address are billed in rupees by Google Cloud India, with GST on the invoice. Supabase bills in US
dollars, so an Indian card adds a forex markup, and a GST-registered business usually pays the GST
itself under reverse charge.</p>
<h2 id="offline-and-mobile-apps" tabindex="-1"><a class="header-anchor" href="#offline-and-mobile-apps">Offline and mobile apps</a></h2>
<p>Firebase was built for mobile. Its SDKs keep a local copy of the data, let the app work with no
connection, and sync changes when the network returns. For apps used on patchy mobile networks, this
is a real advantage.</p>
<p>Supabase’s realtime works well for web apps with a steady connection. Its offline support on mobile is
not built in, so an offline-first app needs extra libraries and more of your own code.</p>
<h2 id="data-in-india" tabindex="-1"><a class="header-anchor" href="#data-in-india">Data in India</a></h2>
<p>Both offer a Mumbai region. Firestore supports <code>asia-south1</code> (Mumbai), and Supabase offers a Mumbai
region. Choose it when you create the project, because moving later means migrating the data.</p>
<p>Region matters for speed as well as compliance. If your app server runs in one region and your
database in another, every query crosses that distance.</p>
<h2 id="lock-in-and-leaving-later" tabindex="-1"><a class="header-anchor" href="#lock-in-and-leaving-later">Lock-in and leaving later</a></h2>
<p>Leaving Supabase is easier for the data, because it is standard PostgreSQL that any Postgres host can
import. The harder part is the auth, storage and security policies built around it.</p>
<p>Leaving Firebase is harder. Firestore has no standard export into another database, so moving to SQL
means redesigning your data. Firebase Auth users can be exported with their password hashes, but your
next auth system must support Firebase’s hashing to keep those passwords working. Our guide to
<a href="/blog/firebase-alternatives-india/">Firebase alternatives</a> walks through the move.</p>
<h2 id="when-you-need-neither" tabindex="-1"><a class="header-anchor" href="#when-you-need-neither">When you need neither</a></h2>
<p>Many apps use their backend platform only as a database. They have their own Node.js, Django or
FastAPI server that handles login and business logic, and talks to the database through an ORM.</p>
<p>For that app, a managed PostgreSQL database next to the server is simpler. <a href="/managed-postgresql-india/">Voroa’s managed
PostgreSQL in Mumbai</a> runs beside a service you
<a href="/docs/deploy-from-github/">deploy from GitHub</a>, billed in rupees with a GST invoice, and the database
is reached by your server rather than the browser. Read
<a href="/blog/supabase-alternative-india/">why teams move off Supabase</a> for the trade-offs.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-supabase-better-than-firebase-for-a-college-project" tabindex="-1"><a class="header-anchor" href="#is-supabase-better-than-firebase-for-a-college-project">Is Supabase better than Firebase for a college project?</a></h3>
<p>If the project has relational data and you want to learn SQL, yes. A free Supabase project pauses after
a week without activity, so open it before your demo or viva. Firebase’s free plan does not pause.</p>
<h3 id="can-firebase-use-postgresql" tabindex="-1"><a class="header-anchor" href="#can-firebase-use-postgresql">Can Firebase use PostgreSQL?</a></h3>
<p>Firebase Data Connect connects Firebase apps to a Cloud SQL for PostgreSQL instance, which is billed
separately as a Google Cloud resource. Firestore itself remains a document database.</p>
<h3 id="which-is-cheaper-in-india" tabindex="-1"><a class="header-anchor" href="#which-is-cheaper-in-india">Which is cheaper in India?</a></h3>
<p>For a small, read-light app, Firebase often is, because you pay only for operations above the free
quota. Supabase’s paid plan starts at $25 but does not charge per read, which is easier to predict as
the app grows.</p>
<h3 id="which-is-better-for-a-flutter-app" tabindex="-1"><a class="header-anchor" href="#which-is-better-for-a-flutter-app">Which is better for a Flutter app?</a></h3>
<p>For an offline-first Flutter app, Firebase, because of its mature offline sync. For a Flutter front end
over relational data and a web dashboard, Supabase.</p>
<h2 id="run-postgresql-next-to-your-app-with-voroa" tabindex="-1"><a class="header-anchor" href="#run-postgresql-next-to-your-app-with-voroa">Run PostgreSQL next to your app with Voroa</a></h2>
<p>If your choice comes down to SQL and your own backend, start with a free PostgreSQL database in Mumbai
and connect it with one <code>DATABASE_URL</code>. The steps are in
<a href="/docs/postgresql-creating-connecting/">creating and connecting a database</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>DDoS protection for small apps in India: what it changes</title>
      <link>https://getvoroa.com/blog/ddos-protection-for-small-apps-in-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/ddos-protection-for-small-apps-in-india/</guid>
      <pubDate>Fri, 02 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>

      <description><![CDATA[We put every Voroa service behind an edge network that absorbs DDoS floods. What changed for apps, what we tested, and the limits worth knowing.]]></description>
      <content:encoded><![CDATA[<p>Most small apps never see a real denial-of-service (DoS) attack. The ones that do usually find out the
hard way: the site is slow, then it is down, and the hosting bill or the inbox fills up with
complaints before anyone works out that the traffic is not real.</p>
<p>On 1 October we turned on DDoS protection for every service on Voroa. This post is what we learned
doing it - what the protection actually does, what it changes for an app, and the trade-offs we
chose to accept.</p>
<h2 id="two-layers-and-why-you-need-both" tabindex="-1"><a class="header-anchor" href="#two-layers-and-why-you-need-both">Two layers, and why you need both</a></h2>
<p>There are two very different kinds of defence against unwanted traffic.</p>
<p><strong>Network-level protection</strong> sits in front of everything. A large flood - millions of requests from
thousands of machines - has to be dropped before it reaches any one server, because no single server
can absorb it. This is the job of an <a href="https://getvoroa.com/docs/ddos-protection/">edge network</a>: a large set of machines spread across many
locations that receives your traffic first and only passes on what looks real.</p>
<p><strong>Application-level defence</strong> is your own code refusing to do expensive work for a client that is
asking too often. This handles the far more common case: one script in a loop, one scraper, one
misconfigured client hammering a login endpoint.</p>
<p>You need both. The edge stops what your server could never survive; rate limiting stops what the edge
would reasonably let through because it looks like an ordinary visitor.</p>
<h2 id="what-changed-on-voroa" tabindex="-1"><a class="header-anchor" href="#what-changed-on-voroa">What changed on Voroa</a></h2>
<p>Every service’s Voroa address now goes through an edge network by default. Our own proxy still limits each
visitor’s request rate, so one noisy client cannot take a service down for everyone else.</p>
<p>Customers on a paid plan can now put their own <a href="https://getvoroa.com/docs/custom-domains/">custom domains</a> behind the same protection. That takes
one DNS change: the domain’s record points at our edge address instead of directly at the service. We
prepare the domain first and issue its certificate before the switch wherever we can, so the site
keeps working while the record moves.</p>
<h2 id="the-trade-offs-we-accepted" tabindex="-1"><a class="header-anchor" href="#the-trade-offs-we-accepted">The trade-offs we accepted</a></h2>
<p>Putting an edge in front of an app is not free. These are the changes an app sees, and the settings
page shows them next to the switch:</p>
<ul>
<li><strong>The edge refuses uploads over 100 MB.</strong> Send large files straight to a storage service.</li>
<li><strong>The edge times out a request that takes more than 100 seconds to start answering.</strong> For long
work, answer first with a job id and carry on in the background.</li>
<li><strong>WebSockets work, but the edge can close an idle connection.</strong> Most client libraries already reconnect.</li>
<li><strong>The visitor’s address moves.</strong> It is the second-to-last entry of <code>X-Forwarded-For</code>, because the
last one is the edge server that connected to your service.</li>
</ul>
<p>Each service has its own switch. If one of these does not suit an app, its owner can turn the
protection off for that service alone.</p>
<h2 id="what-we-did-not-do" tabindex="-1"><a class="header-anchor" href="#what-we-did-not-do">What we did not do</a></h2>
<p>We did not turn on automated bot blocking. On our own platform it blocked the payment provider’s
webhooks: payments went through but our side never heard about them. For a platform that bills
customers, that failure is worse than the traffic it stops, so it stays off.</p>
<p>We also kept customer data where it was. We store it on servers in India; the edge network only
carries requests on their way to the service.</p>
<h2 id="if-you-run-your-own-servers" tabindex="-1"><a class="header-anchor" href="#if-you-run-your-own-servers">If you run your own servers</a></h2>
<p>A few things carry over to any small team:</p>
<ol>
<li><strong>Rate-limit the expensive endpoints first</strong> - login, signup, search, anything that sends email.
That is where a single bad client hurts most.</li>
<li><strong>Know your edge’s limits before you need them.</strong> Upload size and request timeouts are the two
that break real apps.</li>
<li><strong>Test the rollback.</strong> On 1 October we switched the protection off and on again in production,
and visitors saw no errors while it came back. A switch you have never flipped back is not a switch.</li>
</ol>
<p>Voroa customers can read the full details in the
<a href="https://getvoroa.com/docs/ddos-protection/">DDoS protection guide</a>, and see every protection we run on the
<a href="https://getvoroa.com/docs/security/">security overview</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>AWS Elastic Beanstalk vs Voroa: Node.js Hosting in India</title>
      <link>https://getvoroa.com/blog/aws-vs-voroa/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/aws-vs-voroa/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[Elastic Beanstalk vs Voroa for a Node.js app in India: the EB CLI flow, what it creates in your account, ports, India regions, rupee invoices and billing.]]></description>
      <content:encoded><![CDATA[<p>This Elastic Beanstalk vs Voroa comparison is for Indian developers who want AWS’s managed way to run a
Node.js web app and are wondering whether a simpler platform would do. Both take your code and run it for
you. The difference is how much of AWS you see along the way. Every AWS fact here comes from AWS’s own
documentation.</p>
<h2 id="why-elastic-beanstalk-is-awss-managed-app-option-here" tabindex="-1"><a class="header-anchor" href="#why-elastic-beanstalk-is-awss-managed-app-option-here">Why Elastic Beanstalk Is AWS’s Managed-App Option Here</a></h2>
<p>AWS App Runner “is no longer open to new customers” (AWS’s availability-change page), and AWS points new users
to ECS Express Mode. EC2, the plain virtual server, is infrastructure you manage yourself, so it is not the
like-for-like product. That leaves Elastic Beanstalk: you upload your application source bundle and
Elastic Beanstalk “automatically launches an environment” on EC2 or EKS.</p>
<h2 id="eb-init-eb-create-eb-open-the-beanstalk-nodejs-flow" tabindex="-1"><a class="header-anchor" href="#eb-init-eb-create-eb-open-the-beanstalk-nodejs-flow">eb init, eb create, eb open: The Beanstalk Node.js Flow</a></h2>
<p>AWS’s <a href="https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/nodejs-quickstart.html">Node.js on Elastic Beanstalk guide</a>
deploys with the EB CLI or the console. The quickstart runs:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">eb init -p node.js nodejs-tutorial --region us-east-2
eb create nodejs-env
eb open
</code></pre>
</div>
<p>AWS says it “takes about five minutes for Elastic Beanstalk to create your environment”. After that,
deploying a new version is quick because it does not need new instances.</p>
<h2 id="what-beanstalk-creates-in-your-aws-account" tabindex="-1"><a class="header-anchor" href="#what-beanstalk-creates-in-your-aws-account">What Beanstalk Creates in Your AWS Account</a></h2>
<p>Even a single-instance environment creates several AWS resources: an EC2 instance, an instance security
group, an S3 bucket, two CloudWatch alarms, a CloudFormation stack and a domain name. Most platforms put
Apache or nginx in front of your app as a reverse proxy. AWS says Elastic Beanstalk manages all of them, and
the CloudFormation template can be viewed in its console. For teams who know AWS, that visibility is a strength.</p>
<p>On Voroa there is nothing like that to look at. You connect a GitHub repository to a web service, and
Voroa builds and runs it in Mumbai.</p>
<h2 id="port-8080-by-default-vs-a-platform-set-port" tabindex="-1"><a class="header-anchor" href="#port-8080-by-default-vs-a-platform-set-port">Port 8080 by Default vs a Platform-Set PORT</a></h2>
<p>AWS says “Elastic Beanstalk forwards requests to your application on port 8080 by default for Node.js”.
Voroa sets <code>PORT</code> for you, and if your app listens on one other fixed port, such as 8080, Voroa detects
it. Reading <code>process.env.PORT</code> works on both. The <a href="/docs/deploy-nodejs/">Node.js deploy guide</a> has the
Voroa settings.</p>
<h2 id="deploying-a-bundle-vs-deploying-on-git-push" tabindex="-1"><a class="header-anchor" href="#deploying-a-bundle-vs-deploying-on-git-push">Deploying a Bundle vs Deploying on Git Push</a></h2>
<p>The Beanstalk pages we used describe deploys from the EB CLI and the console; they do not describe
deploying on a Git push. On Voroa, with auto-deploy on, each push to your branch becomes a new release,
the new release starts before the old one stops, and a rollback is one click. See
<a href="/docs/deploy-from-github/">how deploys from GitHub work</a>.</p>
<h2 id="mumbai-and-hyderabad-vs-mumbai-only" tabindex="-1"><a class="header-anchor" href="#mumbai-and-hyderabad-vs-mumbai-only">Mumbai and Hyderabad vs Mumbai Only</a></h2>
<p>The <a href="https://docs.aws.amazon.com/general/latest/gr/elasticbeanstalk.html">Elastic Beanstalk endpoints page</a>
lists Asia Pacific (Mumbai), <code>ap-south-1</code>, and Asia Pacific (Hyderabad), <code>ap-south-2</code>. Hyderabad is an
opt-in region. Voroa runs in Mumbai only, so if you want two India regions, AWS wins.</p>
<h2 id="rupee-invoices-from-aws-india" tabindex="-1"><a class="header-anchor" href="#rupee-invoices-from-aws-india">Rupee Invoices From AWS India</a></h2>
<p>With an India billing address, your agreement is with Amazon Web Services India Private Limited and your
invoice total is in rupees. PAN and GST numbers are managed in Tax Settings. Voroa also bills in rupees
with a GST invoice, and takes UPI, Indian cards and netbanking. On invoicing, the two are level.</p>
<h2 id="paying-for-resources-vs-paying-for-a-size" tabindex="-1"><a class="header-anchor" href="#paying-for-resources-vs-paying-for-a-size">Paying for Resources vs Paying for a Size</a></h2>
<p>AWS says: “There is no additional charge for Elastic Beanstalk.” The environment runs on the EC2 and other
resources it creates. For new accounts, AWS offers up to $200 in credits over 6 months, and on that free
plan the account closes on its own, unless you upgrade to a paid plan, after 6 months or when the credits
run out, whichever comes first.</p>
<p>Voroa prices each service size per month: paid web sizes start at Rs 298/month plus GST for 1 GB and 1
vCPU. The free Nano size is Rs 0 with 512 MB and 0.1 vCPU shared, subject to free capacity. See the
<a href="/docs/free-instances/">free instances page</a>.</p>
<h2 id="elastic-beanstalk-vs-voroa-for-one-nodejs-app" tabindex="-1"><a class="header-anchor" href="#elastic-beanstalk-vs-voroa-for-one-nodejs-app">Elastic Beanstalk vs Voroa for One Node.js App</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Elastic Beanstalk</th>
<th>Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Deploy</td>
<td>EB CLI or console, source bundle</td>
<td>GitHub, build on push when auto-deploy is on</td>
</tr>
<tr>
<td>First environment</td>
<td>About five minutes to create</td>
<td>Build, then a live URL</td>
</tr>
<tr>
<td>Resources in your account</td>
<td>EC2, security group, S3, CloudWatch, CloudFormation</td>
<td>None to manage</td>
</tr>
<tr>
<td>Default Node.js port</td>
<td>8080</td>
<td><code>PORT</code> set by Voroa; a fixed port is detected</td>
</tr>
<tr>
<td>India regions</td>
<td>Mumbai and Hyderabad</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Invoice</td>
<td>INR through AWS India</td>
<td>INR with a GST invoice</td>
</tr>
<tr>
<td>Pricing</td>
<td>No extra charge; you pay for the resources</td>
<td>Fixed size per service</td>
</tr>
</tbody>
</table></div>
<h2 id="when-beanstalk-is-the-better-fit" tabindex="-1"><a class="header-anchor" href="#when-beanstalk-is-the-better-fit">When Beanstalk Is the Better Fit</a></h2>
<p>Beanstalk is the better fit if you need two India regions, if the rest of your company runs on AWS, if
your team wants to see and tune every resource, or if your customers ask for AWS by name. Voroa fits a
small team that wants to push code and get a running Node.js service, a
<a href="/managed-postgresql-india/">managed PostgreSQL database</a> and <a href="/background-workers/">background workers</a>
on one bill, without managing cloud resources.</p>
<h2 id="moving-a-beanstalk-app-to-git-push-deploys" tabindex="-1"><a class="header-anchor" href="#moving-a-beanstalk-app-to-git-push-deploys">Moving a Beanstalk App to Git-Push Deploys</a></h2>
<p><img src="https://media.getvoroa.com/img/88dbb2e6ba48e533.png" alt="Four-step flow: upload a bundle or use the CLI, create an Elastic Beanstalk environment on a single instance, and on Voroa a git push instead" width="700" loading="eager" decoding="async"></p>
<ol>
<li>Put the app in a GitHub repository if it only exists as a bundle.</li>
<li>Create a <a href="/web-services/">Voroa web service</a> and connect the repository.</li>
<li>Copy your environment settings into Voroa environment variables.</li>
<li><a href="/docs/import-existing-database/">Import your PostgreSQL database</a> by pasting its connection string.</li>
<li>Test on the Voroa URL, then switch DNS.</li>
</ol>
<p>Also weighing DigitalOcean? Read <a href="/blog/digitalocean-vs-voroa/">App Platform vs Voroa</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="can-i-still-sign-up-for-aws-app-runner" tabindex="-1"><a class="header-anchor" href="#can-i-still-sign-up-for-aws-app-runner">Can I still sign up for AWS App Runner?</a></h3>
<p>No. AWS says App Runner is no longer open to new customers, and points new users to ECS
Express Mode.</p>
<h3 id="does-elastic-beanstalk-run-in-india" tabindex="-1"><a class="header-anchor" href="#does-elastic-beanstalk-run-in-india">Does Elastic Beanstalk run in India?</a></h3>
<p>Yes. Its endpoints page lists Mumbai (<code>ap-south-1</code>) and Hyderabad (<code>ap-south-2</code>).</p>
<h3 id="is-elastic-beanstalk-free" tabindex="-1"><a class="header-anchor" href="#is-elastic-beanstalk-free">Is Elastic Beanstalk free?</a></h3>
<p>AWS says there is no additional charge for Elastic Beanstalk itself. The environment runs on EC2 or EKS
resources in your account.</p>
<h2 id="push-a-nodejs-app-to-mumbai" tabindex="-1"><a class="header-anchor" href="#push-a-nodejs-app-to-mumbai">Push a Node.js App to Mumbai</a></h2>
<p>Connect a repository and your Node.js app runs in Mumbai, with a fixed monthly price and a GST invoice in
rupees. Compare sizes on the <a href="/pricing/">Voroa pricing page</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Backend API Hosting in India: Deploy, Scale and Keep Costs Low</title>
      <link>https://getvoroa.com/blog/backend-api-hosting-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/backend-api-hosting-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>APIs</category>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[A practical guide to backend API hosting in India: choosing a stack, deploying from GitHub, the database and cache, scaling, VPS vs platform, and real costs.]]></description>
      <content:encoded><![CDATA[<p>Backend API hosting in India means running the server your mobile app or website talks to, on a
machine close to your users, at a price that makes sense in rupees. This guide is for developers who
have an API working on their laptop and want it live: what it needs, how to deploy it, how to set up
the database and background jobs, how it scales, and what it costs.</p>
<p><img src="https://media.getvoroa.com/img/a287920c8b5d0628.png" alt="A frontend calling a backend API service, which connects to a managed database, a cache and a background worker" width="700" loading="eager" decoding="async"></p>
<h2 id="what-is-backend-api-hosting" tabindex="-1"><a class="header-anchor" href="#what-is-backend-api-hosting">What is backend API hosting?</a></h2>
<p>Backend API hosting is running your API as a long-lived process on a server that has a public HTTPS
address. Your frontend or mobile app sends requests to that address; the API reads or writes data
and sends back JSON. The host keeps the process running, restarts it if it crashes, and puts a new
version live when you ship one.</p>
<h3 id="how-a-backend-api-works" tabindex="-1"><a class="header-anchor" href="#how-a-backend-api-works">How a backend API works</a></h3>
<p>A request arrives on a port, your framework routes it to a handler, the handler talks to a database
or another service, and the response goes back. Everything that is slow or can fail - a database
query, a third-party call - sits inside that request, which is why where the server runs matters.</p>
<h3 id="what-you-need" tabindex="-1"><a class="header-anchor" href="#what-you-need">What you need</a></h3>
<ul>
<li>The API code in a Git repository</li>
<li>A start command that runs a production server</li>
<li>A port to listen on (on Voroa, read it from <code>PORT</code>)</li>
<li>Environment variables for secrets and the database URL</li>
<li>A database, and often a cache</li>
</ul>
<h2 id="choosing-a-backend-stack" tabindex="-1"><a class="header-anchor" href="#choosing-a-backend-stack">Choosing a backend stack</a></h2>
<p>Pick the stack your team already knows. Hosting should not decide your language.</p>
<h3 id="nodejs" tabindex="-1"><a class="header-anchor" href="#nodejs">Node.js</a></h3>
<p>Express, Fastify and NestJS are common choices. Node.js suits APIs that do a lot of waiting on I/O.
The <a href="https://nodejs.org/en/docs">Node.js documentation</a> covers the runtime; our
<a href="/blog/node-js-hosting-india/">Node.js hosting checklist</a> covers what to check in a host.</p>
<h3 id="python" tabindex="-1"><a class="header-anchor" href="#python">Python</a></h3>
<p>FastAPI and Django REST Framework are the usual picks. Run FastAPI with Uvicorn and Django with
Gunicorn - never the development server. See <a href="/blog/python-hosting-india/">Python hosting in India</a>.</p>
<h3 id="other-frameworks" tabindex="-1"><a class="header-anchor" href="#other-frameworks">Other frameworks</a></h3>
<p>Go, Ruby, Java or anything else works the same way if it listens for HTTP. If your stack needs system
packages, build it from a Dockerfile.</p>
<h2 id="how-to-deploy-a-backend-api-in-india" tabindex="-1"><a class="header-anchor" href="#how-to-deploy-a-backend-api-in-india">How to deploy a backend API in India</a></h2>
<p>These steps use Voroa, but the same shape applies on most platforms.</p>
<h3 id="1-prepare-the-api" tabindex="-1"><a class="header-anchor" href="#1-prepare-the-api">1. Prepare the API</a></h3>
<p>Make it read its port and settings from the environment, and add a health route such as <code>/health</code>
that returns 200. Commit your lockfile so every build installs the same packages.</p>
<h3 id="2-push-it-to-github" tabindex="-1"><a class="header-anchor" href="#2-push-it-to-github">2. Push it to GitHub</a></h3>
<p>Connect the repository to Voroa once. You choose which repositories it can read, and the code stays
private.</p>
<h3 id="3-create-a-web-service" tabindex="-1"><a class="header-anchor" href="#3-create-a-web-service">3. Create a web service</a></h3>
<p>Create a web service from the repository, pick the branch, and set the build and start commands if
the defaults are not right. The <a href="/docs/build-and-start-commands/">build and start commands guide</a>
explains each field.</p>
<h3 id="4-add-environment-variables" tabindex="-1"><a class="header-anchor" href="#4-add-environment-variables">4. Add environment variables</a></h3>
<p>Add <code>DATABASE_URL</code>, your JWT secret and any API keys on the service’s Environment page. Values are
stored encrypted and are not shown back after you save. Details are in
<a href="/docs/environment-variables/">environment variables</a>.</p>
<h3 id="5-deploy" tabindex="-1"><a class="header-anchor" href="#5-deploy">5. Deploy</a></h3>
<p>Voroa builds the code, starts the new release, and switches traffic once it is ready. With
auto-deploy on, every push to the branch repeats this.</p>
<h3 id="6-test" tabindex="-1"><a class="header-anchor" href="#6-test">6. Test</a></h3>
<p>Call the service’s HTTPS URL, check the health route, then watch the runtime logs while you run a few
real requests. Add your own domain when you are happy.</p>
<h2 id="a-simple-backend-api-architecture" tabindex="-1"><a class="header-anchor" href="#a-simple-backend-api-architecture">A simple backend API architecture</a></h2>
<p>Most APIs need four pieces, and nothing more to start with.</p>
<h3 id="the-api" tabindex="-1"><a class="header-anchor" href="#the-api">The API</a></h3>
<p>One web service with a public URL. Keep it stateless: no sessions in memory, so any copy can answer
any request.</p>
<h3 id="postgresql" tabindex="-1"><a class="header-anchor" href="#postgresql">PostgreSQL</a></h3>
<p>Your data lives in <a href="/managed-postgresql-india/">managed PostgreSQL</a>, connected through <code>DATABASE_URL</code>.
Run migrations as part of your release, not by hand.</p>
<h3 id="redis" tabindex="-1"><a class="header-anchor" href="#redis">Redis</a></h3>
<p>Sessions, rate limits and cached responses go in a
<a href="/managed-redis-india/">Redis-compatible key value store</a>. Anything you cannot rebuild belongs in
PostgreSQL, not in the cache.</p>
<h3 id="workers" tabindex="-1"><a class="header-anchor" href="#workers">Workers</a></h3>
<p>Emails, image processing and slow third-party calls go on a queue that a
<a href="/background-workers/">background worker</a> processes, so the API answers quickly.</p>
<h2 id="how-to-make-your-api-scale" tabindex="-1"><a class="header-anchor" href="#how-to-make-your-api-scale">How to make your API scale</a></h2>
<p>Start small and grow in this order:</p>
<ol>
<li><strong>Fix the slow queries first.</strong> An index on the right column often does more than a bigger server.</li>
<li><strong>Cache what is read often.</strong> Put repeated reads in the key value store.</li>
<li><strong>Move slow work to a worker.</strong> Requests should not wait for emails or reports.</li>
<li><strong>Pick a bigger size.</strong> More memory and CPU for the same service.</li>
<li><strong>Run more copies.</strong> Paid sizes can run extra copies of a service, which only works if the API is
stateless.</li>
</ol>
<h2 id="vps-vs-deployment-platform" tabindex="-1"><a class="header-anchor" href="#vps-vs-deployment-platform">VPS vs deployment platform</a></h2>
<p>A VPS gives you full control of the machine. You also own everything after setup: OS patches,
restarts, HTTPS certificates, log rotation, backups and monitoring. A deployment platform takes those
off your hands; you push code and it builds, runs and serves it.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>VPS</th>
<th>Deployment platform</th>
</tr>
</thead>
<tbody>
<tr>
<td>Setup</td>
<td>You install everything</td>
<td>Connect a repository</td>
</tr>
<tr>
<td>New version</td>
<td>Copy, build, restart yourself</td>
<td>Every push builds a new release</td>
</tr>
<tr>
<td>HTTPS</td>
<td>You set up and renew certificates</td>
<td>Handled for you</td>
</tr>
<tr>
<td>Rollback</td>
<td>Whatever you build</td>
<td>Previous version from the dashboard</td>
</tr>
<tr>
<td>Best for</td>
<td>Unusual setups, full root access</td>
<td>Apps that live in Git</td>
</tr>
</tbody>
</table></div>
<p>For a longer comparison, read
<a href="/blog/web-app-hosting-india/">shared hosting vs VPS vs a deploy platform</a>.</p>
<h2 id="backend-api-hosting-costs-in-india" tabindex="-1"><a class="header-anchor" href="#backend-api-hosting-costs-in-india">Backend API hosting costs in India</a></h2>
<p>Three things decide what you really pay:</p>
<ul>
<li><strong>The currency.</strong> A bill in US dollars adds a forex markup to your card. Voroa bills in rupees.</li>
<li><strong>The tax invoice.</strong> A GST invoice lets a registered business claim input tax credit. Voroa issues one.</li>
<li><strong>The sizes you actually need.</strong> Start on the free size (Rs 0, 512 MB memory, subject to free
capacity) while you build. Paid sizes start at Rs 298 a month plus GST; see <a href="/pricing/">pricing</a>.</li>
</ul>
<p>Remember the database, cache and worker are separate services with their own sizes.</p>
<h2 id="free-backend-hosting-and-what-it-costs-in-inr" tabindex="-1"><a class="header-anchor" href="#free-backend-hosting-and-what-it-costs-in-inr">Free backend hosting and what it costs in INR</a></h2>
<p>A small backend can start at Rs 0. On the free tier you can run the API on the free <strong>Nano</strong> size and add
one free managed Postgres (1 GB of storage) and one free key-value store, subject to free capacity. No card
is needed.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Stack</th>
<th>Services</th>
<th>Price before GST</th>
</tr>
</thead>
<tbody>
<tr>
<td>Free</td>
<td>API on Nano, free Postgres, free key-value</td>
<td>Rs 0</td>
</tr>
<tr>
<td>First paying users</td>
<td>API on Starter (1 GB, 1 vCPU), Postgres on DB Starter (1 GB)</td>
<td>Rs 597 per month</td>
</tr>
<tr>
<td>Growing</td>
<td>API on Standard (2 GB, 1 vCPU), Postgres on DB Basic (5 GB storage)</td>
<td>Rs 997 per month</td>
</tr>
</tbody>
</table></div>
<p>The free API sleeps after 15 minutes without requests, and free services in a workspace share 750
running hours a month, so move the API to a paid size before launch. Paid prices exclude 18% GST and come
with a GST invoice. The limits are listed in <a href="/docs/free-instances/">Voroa’s free tier</a>.</p>
<h2 id="backend-api-problems-and-their-usual-cause" tabindex="-1"><a class="header-anchor" href="#backend-api-problems-and-their-usual-cause">Backend API problems and their usual cause</a></h2>
<h3 id="the-app-starts-but-the-url-does-not-answer" tabindex="-1"><a class="header-anchor" href="#the-app-starts-but-the-url-does-not-answer">The app starts but the URL does not answer</a></h3>
<p>The server is probably listening on <code>127.0.0.1</code>. Listen on <code>0.0.0.0</code> and read the port from <code>PORT</code>.</p>
<h3 id="it-works-locally-but-crashes-on-deploy" tabindex="-1"><a class="header-anchor" href="#it-works-locally-but-crashes-on-deploy">It works locally but crashes on deploy</a></h3>
<p>A missing environment variable is the usual cause. Check the runtime logs for the first error line.</p>
<h3 id="requests-time-out-under-load" tabindex="-1"><a class="header-anchor" href="#requests-time-out-under-load">Requests time out under load</a></h3>
<p>Look for a slow query or a slow external call inside the request, and move that work to a worker.</p>
<h3 id="cors-errors-in-the-browser" tabindex="-1"><a class="header-anchor" href="#cors-errors-in-the-browser">CORS errors in the browser</a></h3>
<p>Allow your frontend’s exact origin in the API’s CORS settings, including the <code>https://</code> part.</p>
<h2 id="deploy-a-backend-api-with-voroa" tabindex="-1"><a class="header-anchor" href="#deploy-a-backend-api-with-voroa">Deploy a backend API with Voroa</a></h2>
<p>Voroa runs your API in Mumbai, builds it from GitHub on every push, and bills you in rupees with a
GST invoice. You can start on the free size with no card. See what an
<a href="/web-services/">API web service includes</a>, or follow the
<a href="/docs/deploy-from-github/">first deploy guide</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="what-is-the-best-way-to-host-a-backend-api-in-india" tabindex="-1"><a class="header-anchor" href="#what-is-the-best-way-to-host-a-backend-api-in-india">What is the best way to host a backend API in India?</a></h3>
<p>Host it on a server in India, deploy it from Git so every change is repeatable, and keep secrets in
environment variables. A deployment platform does most of this for you; a VPS gives more control but
more work.</p>
<h3 id="can-i-host-a-backend-api-for-free" tabindex="-1"><a class="header-anchor" href="#can-i-host-a-backend-api-for-free">Can I host a backend API for free?</a></h3>
<p>Yes, to start. Voroa’s free size costs Rs 0 and needs no card, subject to free capacity. Move to a
paid size when real traffic arrives.</p>
<h3 id="do-i-need-docker-to-deploy-an-api" tabindex="-1"><a class="header-anchor" href="#do-i-need-docker-to-deploy-an-api">Do I need Docker to deploy an API?</a></h3>
<p>No. For Node.js and Python, Voroa detects the language and builds it. Use a Dockerfile only when you
need system packages or an exact image.</p>
<h3 id="where-should-i-run-background-jobs" tabindex="-1"><a class="header-anchor" href="#where-should-i-run-background-jobs">Where should I run background jobs?</a></h3>
<p>In a background worker that reads from a queue, not inside the API request.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Claude Code MCP for Web Hosting: Deploy, Read Logs, Fix</title>
      <link>https://getvoroa.com/blog/claude-code-web-hosting-automation/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/claude-code-web-hosting-automation/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Deploys</category>
      <category>India</category>
      <description><![CDATA[Set up Claude Code MCP with Voroa so the agent pushes, reads the failing build log, fixes the code and redeploys. Config, scopes, SSH and the limits it keeps.]]></description>
      <content:encoded><![CDATA[<p>Claude Code MCP support means the agent in your terminal can talk to your hosting the same way it talks to
your files. On Voroa, one command gives it deploy status, build logs and runtime logs, so a broken build
becomes something it reads and repairs instead of something you describe to it. Below: the setup command,
the permission rules that keep it moving, a real-shaped session where a build fails and gets fixed, and
how far you can leave it alone.</p>
<h2 id="why-a-terminal-agent-should-see-the-platform" tabindex="-1"><a class="header-anchor" href="#why-a-terminal-agent-should-see-the-platform">Why a terminal agent should see the platform</a></h2>
<p>Claude Code lives in the shell where you run <code>git push</code>. Its blind spot is what happens after the push:
the build runs elsewhere, and the error lands in a dashboard it has never seen.</p>
<p>Voroa closes that gap with a hosted MCP server. Nothing runs on your laptop apart from Claude Code itself.</p>
<h2 id="register-voroa-with-claude-mcp-add" tabindex="-1"><a class="header-anchor" href="#register-voroa-with-claude-mcp-add">Register Voroa with <code>claude mcp add</code></a></h2>
<p>First, in the Voroa dashboard go to <strong>Settings</strong>, then <strong>API tokens</strong>, and create a token. It begins with
<code>voroa_cat_</code> and appears only once, so copy it straight away. Then in your terminal:</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>claude mcp add --transport http voroa https://api.getvoroa.com/mcp --header &quot;Authorization: Bearer paste-your-token-here&quot;
</code></pre>
</div>
<p>That saves the server at Claude Code’s default <code>local</code> scope. Avoid <code>--scope project</code> for this one: it
writes a <code>.mcp.json</code> into the repository, and a token in a committed file is a leaked token. The
<a href="https://code.claude.com/docs/en/mcp">Claude Code MCP reference</a> explains <code>local</code>, <code>project</code> and <code>user</code>.</p>
<p>To confirm, ask Claude Code which of your Voroa services deployed last. Real names back means it is
connected. If not, work through the checks in
<a href="/docs/connect-an-ai-client/">connecting an AI client to Voroa</a>.</p>
<h2 id="the-token-decides-how-far-claude-code-can-reach" tabindex="-1"><a class="header-anchor" href="#the-token-decides-how-far-claude-code-can-reach">The token decides how far Claude Code can reach</a></h2>
<p>Each token holds one scope. <code>services:read</code> sees services, deploys and logs. <code>services:act</code> adds redeploy,
restart and rollback. <code>services:write</code> adds env variables and domains. <code>services:admin</code> can only propose
creating or deleting things; a person approves those eight actions in the dashboard within an hour.</p>
<p>Two limits hold at every scope: Claude Code sees secret names but never their values, and it cannot fetch a
database password. Tokens last 90 days (30 on <code>services:admin</code>) and cover one workspace. For a
fix-and-ship loop, <code>services:act</code> is the right size. The whole tool list lives on the
<a href="/docs/mcp-server/">Voroa MCP server page</a>.</p>
<h2 id="pre-approving-tools-so-the-loop-does-not-stall" tabindex="-1"><a class="header-anchor" href="#pre-approving-tools-so-the-loop-does-not-stall">Pre-approving tools so the loop does not stall</a></h2>
<p>By default Claude Code asks before most shell commands. That is sensible, but a loop that pauses at every
<code>git push</code> and every log read is not much of a loop. Add allow rules for the calls you trust:</p>
<ul>
<li><code>Bash(git push *)</code> lets it push without asking each time;</li>
<li><code>mcp__voroa__*</code> lets it call Voroa’s tools freely.</li>
</ul>
<p>Everything else still prompts. The token already caps what Voroa accepts, so allowing its tools is far
safer than allowing every shell command.</p>
<h2 id="a-worked-session-one-failed-build-fixed-end-to-end" tabindex="-1"><a class="header-anchor" href="#a-worked-session-one-failed-build-fixed-end-to-end">A worked session: one failed build, fixed end to end</a></h2>
<p><img src="https://media.getvoroa.com/img/b60f394c20cf48cc.png" alt="Diagram of four steps in order: push code, read deploy status, read build log, then fix and redeploy" width="700" loading="eager" decoding="async"></p>
<p>Say you ask: “Add a health check route to the API and ship it.” This example assumes the service’s
build command is <code>npm run build</code>. Here is how the session goes.</p>
<ol>
<li>Claude Code writes the route, commits and runs <code>git push</code>. With <a href="/docs/auto-deploy/">auto-deploy</a> on,
Voroa starts a build from that push.</li>
<li>It calls <code>get_deploy_history</code>, picks out the new deploy, then polls <code>get_service_status</code>. The status
comes back failed.</li>
<li>It calls <code>get_build_log</code>. The last lines show <code>npm ERR! Missing script: &quot;build&quot;</code> because the new
<code>package.json</code> lost its build script in the edit.</li>
<li>It restores the script, commits, pushes, and polls again. This time the deploy goes live.</li>
<li>If the build had passed but the service had not come up, it would have called <code>get_runtime_logs</code>
instead, which is where a missing variable shows itself.</li>
</ol>
<p>You typed one sentence. More prompts in this style are in the
<a href="/docs/use-with-coding-agents/">guide to shipping with a coding agent</a>.</p>
<h2 id="leaving-claude-code-to-run-unattended" tabindex="-1"><a class="header-anchor" href="#leaving-claude-code-to-run-unattended">Leaving Claude Code to run unattended</a></h2>
<p>The permission modes range from <code>default</code>, which asks, through <code>acceptEdits</code>, <code>plan</code> and <code>auto</code> (a
classifier reviews each call), to <code>bypassPermissions</code>. The official
<a href="https://code.claude.com/docs/en/permissions">permissions guide</a> says to use that last one only in isolated
environments. A laptop with push access to production is not isolated.</p>
<p>A safer recipe for long runs: keep a normal mode, add the two allow rules above, and let the Voroa token
be the hard limit. Even left alone, it cannot create a billable service or delete a database, because those
only ever become proposals.</p>
<h2 id="when-the-answer-is-inside-the-container" tabindex="-1"><a class="header-anchor" href="#when-the-answer-is-inside-the-container">When the answer is inside the container</a></h2>
<p>Some faults never reach a log: a file in the wrong folder, a variable that resolved to something odd.
Open the SSH Access page on your service, add your public key once, and copy the command it shows:</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>ssh svc-a1b2c3d4e5f6g7h8i9j0k1l2@ssh.getvoroa.com -p 2222
</code></pre>
</div>
<p>Run it in a second tab and paste what you find back to Claude Code. A hand edit is gone after the next
deploy unless it is on a persistent disk, so diagnose there and let the agent commit the fix. Details in the
<a href="/docs/ssh-access/">SSH access guide</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="does-claude-code-need-a-paid-anthropic-plan" tabindex="-1"><a class="header-anchor" href="#does-claude-code-need-a-paid-anthropic-plan">Does Claude Code need a paid Anthropic plan?</a></h3>
<p>Yes: a Pro, Max, Team, Enterprise or Console account. The free <a href="http://claude.ai">claude.ai</a> plan does not include it.</p>
<h3 id="what-if-my-token-leaks" tabindex="-1"><a class="header-anchor" href="#what-if-my-token-leaks">What if my token leaks?</a></h3>
<p>Delete it under <strong>Settings, API tokens</strong>. It stops on the next request.</p>
<h3 id="i-use-codex-or-cursor-as-well-is-it-the-same" tabindex="-1"><a class="header-anchor" href="#i-use-codex-or-cursor-as-well-is-it-the-same">I use Codex or Cursor as well. Is it the same?</a></h3>
<p>The server is the same; the client setup differs. See the <a href="/blog/codex-web-hosting-automation/">Codex CLI walkthrough</a>
and <a href="/blog/cursor-web-hosting-automation/">Cursor’s editor setup</a>.</p>
<h2 id="put-claude-code-on-your-next-voroa-deploy" tabindex="-1"><a class="header-anchor" href="#put-claude-code-on-your-next-voroa-deploy">Put Claude Code on your next Voroa deploy</a></h2>
<p>Your code can run as <a href="/web-services/">Voroa web services, hosted in Mumbai</a> and invoiced in rupees with GST.
Register the server once, and the next failed build gets fixed by the agent that caused it. New to push-based deploys? Read <a href="/blog/deploy-from-github-india/">how deploying from GitHub works</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Codex MCP Setup for Web Hosting: Deploy and Fix Failed Builds</title>
      <link>https://getvoroa.com/blog/codex-web-hosting-automation/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/codex-web-hosting-automation/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Deploys</category>
      <category>India</category>
      <description><![CDATA[Connect OpenAI Codex CLI to Voroa with a codex mcp entry in config.toml. The agent pushes, reads the build log, fixes and redeploys. Sandbox, scopes and SSH.]]></description>
      <content:encoded><![CDATA[<p>A codex mcp entry is three lines of TOML, and it changes what you can hand to OpenAI’s Codex CLI. Instead
of “write this feature”, you can say “ship this feature and verify it is live”, and Codex can check the
result on Voroa itself. This guide covers the config entry, the sandbox and approval settings that decide
whether a push even leaves your machine, how to phrase a ship-and-verify task, and why you should still
read the diff first.</p>
<h2 id="what-codex-cli-is-missing-out-of-the-box" tabindex="-1"><a class="header-anchor" href="#what-codex-cli-is-missing-out-of-the-box">What Codex CLI is missing out of the box</a></h2>
<p>Codex works inside your repository with the tools already on your machine. It is good at the edit, test,
commit part. It has no idea whether your hosting accepted the result. Voroa’s MCP endpoint, which Voroa runs for you, answers exactly that question: did the deploy start, did it pass, and if not, what does the build log
say.</p>
<h2 id="the-configtoml-entry-for-voroa" tabindex="-1"><a class="header-anchor" href="#the-configtoml-entry-for-voroa">The <code>config.toml</code> entry for Voroa</a></h2>
<p>Install Codex if needed (<code>npm install -g @openai/codex</code>, or Homebrew, or the shell installer) and sign in
with a ChatGPT plan or an API key. The <a href="https://github.com/openai/codex">Codex repository on GitHub</a> lists
every install route.</p>
<p>Next, create a Voroa token under <strong>Settings, API tokens</strong> in the dashboard. You will see its <code>voroa_cat_</code> value
only at creation. Export it in your shell profile as <code>VOROA_TOKEN</code>, then add to
<code>~/.codex/config.toml</code>:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">toml</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-toml">[mcp_servers.voroa]
url = &quot;https://api.getvoroa.com/mcp&quot;
bearer_token_env_var = &quot;VOROA_TOKEN&quot;
</code></pre>
</div>
<p>Codex sends that variable as an <code>Authorization: Bearer</code> header, so the secret stays in your environment
and out of the file. The <a href="https://learn.chatgpt.com/docs/extend/mcp?surface=cli">Codex MCP page</a> lists the
other supported fields. If Codex cannot name your real services when asked, our <a href="/docs/connect-an-ai-client/">AI client connection checks</a> cover the usual causes.</p>
<h2 id="sandbox-and-approvals-why-the-first-push-fails" tabindex="-1"><a class="header-anchor" href="#sandbox-and-approvals-why-the-first-push-fails">Sandbox and approvals: why the first push fails</a></h2>
<p>Codex runs commands in a sandbox, <code>workspace-write</code> by default, and network access for commands starts
switched off. <code>git push</code> needs the network. So does <code>ssh</code>. Until you allow it, both simply fail:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">toml</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-toml">[sandbox_workspace_write]
network_access = true
</code></pre>
</div>
<p>Pair that with <code>approval_policy = &quot;on-request&quot;</code>, so Codex can stop and ask you before a step you care
about. Stay away from <code>--yolo</code> (<code>--dangerously-bypass-approvals-and-sandbox</code>), which removes every
protection in one flag. OpenAI’s <a href="https://learn.chatgpt.com/docs/agent-approvals-security">approvals and security notes</a>
describe each option.</p>
<h2 id="writing-a-ship-this-and-verify-task" tabindex="-1"><a class="header-anchor" href="#writing-a-ship-this-and-verify-task">Writing a “ship this and verify” task</a></h2>
<p>A good task names the outcome, not just the change:</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>Upgrade express to the latest minor version, run the tests, push to main,
then use the Voroa tools to confirm the new deploy is live. If the build
fails, read the build log, fix the cause and try again. Stop after three attempts.
</code></pre>
</div>
<p><img src="https://media.getvoroa.com/img/b60f394c20cf48cc.png" alt="Flow diagram in four boxes: push code, then read deploy status, then read build log, then fix and redeploy" width="700" loading="eager" decoding="async"></p>
<p>Codex pushes, then looks up the fresh deploy with <code>get_deploy_history</code> and watches <code>get_service_status</code>.
On a red build it pulls <code>get_build_log</code>, edits, and pushes again. A green build with a dead service sends
it to <code>get_runtime_logs</code>. Because Voroa builds every push when <a href="/docs/auto-deploy/">auto-deploy</a> is on,
the push is the deploy; the token is only for reading the outcome. The same task runs without a chat
window through <code>codex exec</code>, handy after a scheduled dependency bump. More task ideas are in
<a href="/docs/use-with-coding-agents/">Voroa’s coding agent guide</a>.</p>
<h2 id="read-the-diff-before-codex-pushes" tabindex="-1"><a class="header-anchor" href="#read-the-diff-before-codex-pushes">Read the diff before Codex pushes</a></h2>
<p>On Voroa, a push to the deploy branch is a release. So the moment worth your attention is just before it.
With <code>on-request</code> approvals, ask Codex to show <code>git diff</code> and wait for your go-ahead before <code>git push</code>.
Thirty seconds of reading beats a rollback. If something slips through anyway, a <code>services:act</code> token lets
Codex roll back to a deploy you already had.</p>
<h2 id="where-voroa-draws-the-line-for-any-codex-token" tabindex="-1"><a class="header-anchor" href="#where-voroa-draws-the-line-for-any-codex-token">Where Voroa draws the line for any Codex token</a></h2>
<p><code>services:read</code> covers the loop above. <code>services:act</code> adds redeploy, restart and rollback;
<code>services:write</code> adds env variables and domains. Creating or deleting a service, database or domain is
never done by the token: on <code>services:admin</code> Codex can propose it, and you approve in the dashboard within
the hour. Codex can learn which secrets exist but never their contents, and no Voroa tool hands out a database password. See the
<a href="/docs/mcp-server/">full Voroa MCP tool list</a>.</p>
<h2 id="shelling-into-the-service-when-logs-run-dry" tabindex="-1"><a class="header-anchor" href="#shelling-into-the-service-when-logs-run-dry">Shelling into the service when logs run dry</a></h2>
<p>Your service’s SSH Access page holds a key list and a ready-made login line of this shape, where the user
name is the service’s own ID:</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>ssh &lt;service-id&gt;@ssh.getvoroa.com -p 2222
</code></pre>
</div>
<p>The sandbox network setting above also covers <code>ssh</code>, but opening the session yourself is simpler. Tell
Codex what you find. Edits made in there vanish at the next release unless they sit on a persistent disk,
so look, then fix in git. Setup and troubleshooting:
<a href="/docs/ssh-access/">SSH into a running service</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="do-i-need-codex-mcp-login" tabindex="-1"><a class="header-anchor" href="#do-i-need-codex-mcp-login">Do I need <code>codex mcp login</code>?</a></h3>
<p>That command is for servers that sign you in with OAuth. For Codex, this guide connects with a Voroa
token through the <code>bearer_token_env_var</code> line above.</p>
<h3 id="should-codex-and-claude-code-use-one-token" tabindex="-1"><a class="header-anchor" href="#should-codex-and-claude-code-use-one-token">Should Codex and Claude Code use one token?</a></h3>
<p>Create a separate token for each agent, so revoking one leaves the other working. The
<a href="/blog/claude-code-web-hosting-automation/">Claude Code version</a> uses the same server.</p>
<h2 id="hand-codex-a-real-voroa-service" tabindex="-1"><a class="header-anchor" href="#hand-codex-a-real-voroa-service">Hand Codex a real Voroa service</a></h2>
<p>Start with one of your <a href="/web-services/">web services on Voroa</a>: Mumbai region, rupee pricing, GST invoices.
Add the entry, open the sandbox network, and give Codex a task that ends in a live deploy. Prefer an
editor? Read the <a href="/blog/cursor-web-hosting-automation/">Cursor walkthrough</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Cursor MCP for Web Hosting: Let the Agent Deploy and Debug</title>
      <link>https://getvoroa.com/blog/cursor-web-hosting-automation/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/cursor-web-hosting-automation/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Deploys</category>
      <category>India</category>
      <description><![CDATA[Add Voroa to Cursor MCP in mcp.json so the agent pushes, reads the build log, fixes the code and redeploys. Config, run modes, token scopes, SSH and the limits.]]></description>
      <content:encoded><![CDATA[<p>With Cursor MCP, your editor becomes the one window where you write code, see why a deploy broke and get it
fixed. Connect Voroa once and you can ask Cursor’s agent about a failing deploy while the file is still
open. This post is written for people who live in the editor: the <code>mcp.json</code> block, what to ask when a
build goes red, how Run Modes gate Voroa’s tools, and how to pair it with SSH from the terminal panel.</p>
<h2 id="cursor-as-the-place-you-already-look" tabindex="-1"><a class="header-anchor" href="#cursor-as-the-place-you-already-look">Cursor as the place you already look</a></h2>
<p>In an IDE, context is everything. The agent can see the file, the diff and the terminal. What it does not
see is the build output on your host. Voroa’s MCP server, run by Voroa, turns that output into one more thing Cursor can read, next to your code.</p>
<h2 id="one-block-in-mcpjson" tabindex="-1"><a class="header-anchor" href="#one-block-in-mcpjson">One block in <code>mcp.json</code></a></h2>
<p>Create a token in the Voroa dashboard under <strong>Settings, API tokens</strong> (copy the <code>voroa_cat_</code> value the moment it
appears; Voroa will not show it again) and store it as an environment variable named <code>VOROA_TOKEN</code>. Then add this to
<code>~/.cursor/mcp.json</code> for all projects, or to <code>.cursor/mcp.json</code> for just one:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">json</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;voroa&quot;: {
      &quot;url&quot;: &quot;https://api.getvoroa.com/mcp&quot;,
      &quot;headers&quot;: { &quot;Authorization&quot;: &quot;Bearer ${env:VOROA_TOKEN}&quot; }
    }
  }
}
</code></pre>
</div>
<p>The <code>${env:...}</code> reference matters most in the project file, since that file sits in your repository.
The <a href="https://cursor.com/docs/context/mcp">Cursor MCP documentation</a> covers the format and transports.
Restart Cursor and ask the agent to name your most recent Voroa deploy. A real service name means it
worked; a reply about a missing tool means Cursor has not loaded the file yet.
Our <a href="/docs/connect-an-ai-client/">client connection troubleshooting</a> covers the rest.</p>
<h2 id="asking-about-a-failing-deploy-from-the-editor" tabindex="-1"><a class="header-anchor" href="#asking-about-a-failing-deploy-from-the-editor">Asking about a failing deploy from the editor</a></h2>
<p><img src="https://media.getvoroa.com/img/b60f394c20cf48cc.png" alt="Four-part diagram: the agent pushes code, reads the deploy status, reads the build log, and fixes then redeploys" width="700" loading="eager" decoding="async"></p>
<p>You push from Cursor, switch to another file, and a few minutes later the service is still on the old
version. Instead of opening the dashboard, type into the agent chat:</p>
<div class="codeblock"><div class="codeblock-head"><span></span><button type="button" class="codeblock-copy">Copy</button></div><pre><code>Why did my last Voroa deploy of the api service fail? Fix it here and push again.
</code></pre>
</div>
<p>The agent reads the deploy history, confirms the failure through the service status, and opens the build
log. Say the log points at an import that does not exist. The agent jumps to that file, which is already
open in your editor, corrects the path and shows you the change. Once you accept, it commits and pushes,
and <a href="/docs/auto-deploy/">auto-deploy</a> starts a new build that it watches to the end. When a build passes but
the service will not start, it reads the runtime logs instead. More prompts like this are in
<a href="/docs/use-with-coding-agents/">Voroa’s playbook for coding agents</a>.</p>
<h2 id="how-run-modes-gate-voroas-tools" tabindex="-1"><a class="header-anchor" href="#how-run-modes-gate-voroas-tools">How Run Modes gate Voroa’s tools</a></h2>
<p>Cursor asks for approval before using MCP tools, and those tools follow the same Run Modes as terminal
commands:</p>
<ul>
<li><strong>Allowlist</strong>: only calls you have allowed run on their own;</li>
<li><strong>Auto-review</strong>: allowlisted calls run, others go to the sandbox or a classifier;</li>
<li><strong>Run Everything</strong>: every tool call runs automatically.</li>
</ul>
<p>For a project that deploys to production on push, Allowlist is the sensible start. Allow the Voroa read
tools first, and add push once you trust the pattern. Cursor’s
<a href="https://cursor.com/docs/agent/run-modes">Run Modes page</a> explains each one. By default the sandbox network
permits GitHub, so a push from the agent should get through.</p>
<h2 id="what-a-cursor-connected-token-can-and-cannot-reach" tabindex="-1"><a class="header-anchor" href="#what-a-cursor-connected-token-can-and-cannot-reach">What a Cursor-connected token can and cannot reach</a></h2>
<p>Pick the scope when you create the token. <code>services:read</code> is enough to diagnose; <code>services:act</code> lets the
agent redeploy or roll back; <code>services:write</code> lets it set env variables. Even <code>services:admin</code> cannot
create or delete services, databases or domains. It can only propose them for you to approve in the
dashboard. The agent sees which variables are set but not their values, it cannot retrieve database credentials, and a
token covers one workspace for 90 days (30 on admin). The complete list is on the <a href="/docs/mcp-server/">Voroa MCP server reference</a>.</p>
<h2 id="ssh-from-cursors-terminal-panel" tabindex="-1"><a class="header-anchor" href="#ssh-from-cursors-terminal-panel">SSH from Cursor’s terminal panel</a></h2>
<p>When the logs cannot tell you whether a file exists in the running container, go to the SSH Access page
for that service. Register your public key there and copy the login line, which uses port 2222 on
<code>ssh.getvoroa.com</code> with the service ID as the user name.</p>
<p>Paste it into a terminal inside Cursor and run it yourself. Cursor’s docs do not say whether the agent
sandbox allows arbitrary SSH hosts, so a session you open by hand is the reliable route. Check what the
agent asked about, then paste the output into the chat. Whatever you change by hand is wiped by the following deploy, except
files kept on a persistent disk, so the real fix still goes through the editor and git. See
<a href="/docs/ssh-access/">SSH access to your service</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="does-this-work-with-the-cursor-cli-too" tabindex="-1"><a class="header-anchor" href="#does-this-work-with-the-cursor-cli-too">Does this work with the Cursor CLI too?</a></h3>
<p>Cursor has a CLI (check it with <code>agent --version</code>). On Voroa’s side nothing changes: same URL, same header.</p>
<h3 id="is-the-setup-different-from-claude-code-or-codex" tabindex="-1"><a class="header-anchor" href="#is-the-setup-different-from-claude-code-or-codex">Is the setup different from Claude Code or Codex?</a></h3>
<p>Only the client config. Compare the <a href="/blog/claude-code-web-hosting-automation/">Claude Code terminal setup</a>
and the <a href="/blog/codex-web-hosting-automation/">Codex CLI config</a>.</p>
<h2 id="try-it-on-a-voroa-web-service" tabindex="-1"><a class="header-anchor" href="#try-it-on-a-voroa-web-service">Try it on a Voroa web service</a></h2>
<p>Run your app as a <a href="/web-services/">Mumbai-hosted Voroa web service</a>, paid in rupees with a proper GST bill.
Add one block to <code>mcp.json</code>, keep Allowlist on, and the next red build becomes a question you ask in the editor.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Deploy from GitHub: How Push-to-Deploy Works, Step by Step</title>
      <link>https://getvoroa.com/blog/deploy-from-github-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/deploy-from-github-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Deploys</category>
      <category>GitHub</category>
      <category>India</category>
      <description><![CDATA[What happens when you deploy from GitHub: connecting the repository, the build, the new release, auto-deploy, previews and rollback. Explained for India.]]></description>
      <content:encoded><![CDATA[<p>To deploy from GitHub means your live app follows your repository. You push a commit, and a few
minutes later that commit is what your users see. No FTP, no SSH, no manual restart. This post walks
through what happens between the push and the live URL, so you know what to expect and where to look
when something goes wrong.</p>
<p><img src="https://media.getvoroa.com/img/8e5ee91b686ba3a4.png" alt="The steps from a git push to a live release: connect, build, start, switch traffic" width="700" loading="eager" decoding="async"></p>
<h2 id="step-1-connect-the-repository" tabindex="-1"><a class="header-anchor" href="#step-1-connect-the-repository">Step 1: connect the repository</a></h2>
<p>You install the platform’s GitHub app on the repositories you choose. The platform can then read
those repositories, and only those. On Voroa, your code stays private. GitHub’s own docs explain
<a href="https://docs.github.com/en/apps/using-github-apps/about-using-github-apps">how GitHub Apps get repository access</a>.</p>
<h2 id="step-2-a-push-starts-a-build" tabindex="-1"><a class="header-anchor" href="#step-2-a-push-starts-a-build">Step 2: a push starts a build</a></h2>
<h3 id="which-branch-deploys" tabindex="-1"><a class="header-anchor" href="#which-branch-deploys">Which branch deploys</a></h3>
<p>You pick one branch for the service, usually <code>main</code>. When
<a href="/docs/auto-deploy/">auto-deploy</a> is on, a push to that branch starts a build. A push to another
branch does not. You can change the branch or the folder later in
<a href="/docs/branch-and-root-directory/">branch and root directory</a>.</p>
<h3 id="what-the-build-does" tabindex="-1"><a class="header-anchor" href="#what-the-build-does">What the build does</a></h3>
<p>The build installs your dependencies and runs your build command. If you use a Dockerfile, it builds
the image from that instead.</p>
<h3 id="if-the-build-fails" tabindex="-1"><a class="header-anchor" href="#if-the-build-fails">If the build fails</a></h3>
<p>The live app does not change. The previous release keeps serving, and the build log shows the error.
Fix it, push again, and a new build starts.</p>
<h2 id="step-3-the-new-release-starts" tabindex="-1"><a class="header-anchor" href="#step-3-the-new-release-starts">Step 3: the new release starts</a></h2>
<p>The new release starts next to the old one. When it is ready, traffic moves to it, and then the old
one stops. With a <a href="/docs/deploy-status/">health check</a> set, traffic moves only once the new release
answers, so users do not hit a moment when nothing is running.</p>
<h2 id="step-4-roll-back-if-needed" tabindex="-1"><a class="header-anchor" href="#step-4-roll-back-if-needed">Step 4: roll back if needed</a></h2>
<p>Every release is kept in the <a href="/docs/deploy-history-and-logs/">deploy history</a>. If the new one
misbehaves, roll back to the previous version, then fix the code and push again. Rollback needs at
least one earlier deploy.</p>
<h2 id="preview-a-change-before-it-goes-live" tabindex="-1"><a class="header-anchor" href="#preview-a-change-before-it-goes-live">Preview a change before it goes live</a></h2>
<p>A pull request can get its own temporary copy of the app, so a teammate can check the change on a
real URL before it merges. See <a href="/docs/pr-preview-environments/">preview environments</a> for how it
works and what it costs.</p>
<h2 id="good-habits-for-push-to-deploy" tabindex="-1"><a class="header-anchor" href="#good-habits-for-push-to-deploy">Good habits for push-to-deploy</a></h2>
<ul>
<li><strong>Protect the deploy branch.</strong> Merge through pull requests so every live commit was reviewed.</li>
<li><strong>Keep secrets out of Git.</strong> Use environment variables; a push should never carry a key.</li>
<li><strong>Commit lockfiles.</strong> Every build then installs the same packages.</li>
<li><strong>Add a health route.</strong> A quick <code>/health</code> check makes a broken release obvious at once.</li>
</ul>
<h2 id="turning-it-on" tabindex="-1"><a class="header-anchor" href="#turning-it-on">Turning it on</a></h2>
<p>The full walk-through, with screenshots, is in
<a href="/docs/deploy-from-github/">how to deploy a web app from GitHub</a>.</p>
<h2 id="why-it-matters-in-india" tabindex="-1"><a class="header-anchor" href="#why-it-matters-in-india">Why it matters in India</a></h2>
<p>Your servers run in Mumbai, you are billed in rupees with a GST invoice, and the workflow is the same
push-to-deploy flow used by teams everywhere. There is nothing extra to learn to run close to Indian
users.</p>
<h2 id="deploy-from-github-with-voroa" tabindex="-1"><a class="header-anchor" href="#deploy-from-github-with-voroa">Deploy from GitHub with Voroa</a></h2>
<p>If you are moving off Heroku, read <a href="/blog/heroku-alternative-india/">a Heroku alternative for India</a>.
To see what a deployed service includes, look at <a href="/web-services/">web app hosting on Voroa</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>DigitalOcean App Platform vs Voroa: Node.js Hosting in India</title>
      <link>https://getvoroa.com/blog/digitalocean-vs-voroa/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/digitalocean-vs-voroa/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[DigitalOcean App Platform vs Voroa for Node.js apps in India: Bangalore vs Mumbai, USD vs rupee invoices, free tier, autoscaling, rollbacks and databases.]]></description>
      <content:encoded><![CDATA[<p>This DigitalOcean App Platform vs Voroa comparison is for Indian developers deploying a Node.js web app
who want a managed platform, not a server. Both build your app from a Git repository and run it for you,
and both have a region in India. The differences are in billing, the free tier, scaling and databases.
Every DigitalOcean fact here comes from DigitalOcean’s own pages.</p>
<h2 id="why-this-compares-app-platform-not-droplets" tabindex="-1"><a class="header-anchor" href="#why-this-compares-app-platform-not-droplets">Why This Compares App Platform, Not Droplets</a></h2>
<p>DigitalOcean also sells Droplets, virtual machines you manage yourself from $4.00/month, but that is a
server, not a platform, so it is not compared here. App Platform is, in DigitalOcean’s words, “a fully
managed Platform-as-a-Service (PaaS) that deploys applications from Git repositories or container
images”. That is the like-for-like product.</p>
<h2 id="nodejs-on-app-platform-buildpacks-dockerfiles-deploy-on-push" tabindex="-1"><a class="header-anchor" href="#nodejs-on-app-platform-buildpacks-dockerfiles-deploy-on-push">Node.js on App Platform: Buildpacks, Dockerfiles, Deploy on Push</a></h2>
<p>The <a href="https://docs.digitalocean.com/products/app-platform/">App Platform documentation</a> describes building
with buildpacks or Dockerfiles, lists a Node.js buildpack, and says it will “Build and deploy apps on push
in the cloud”. Automatic redeploys are a checkbox, “Automatically Deploy Code Changes”.</p>
<h2 id="nodejs-on-voroa-repository-port-release" tabindex="-1"><a class="header-anchor" href="#nodejs-on-voroa-repository-port-release">Node.js on Voroa: Repository, PORT, Release</a></h2>
<p>On Voroa you connect a GitHub repository to a web service. Voroa detects the Node.js app, installs
dependencies, builds and starts it, or builds from your Dockerfile if you have one. Your server reads the
port Voroa sets:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">const port = process.env.PORT || 3000;
app.listen(port);
</code></pre>
</div>
<p>With auto-deploy on, each push becomes a new release, and the new release starts before the old one
stops. The <a href="/docs/deploy-nodejs/">Node.js deploy guide</a> has the steps.</p>
<h2 id="bangalore-and-mumbai-two-india-regions" tabindex="-1"><a class="header-anchor" href="#bangalore-and-mumbai-two-india-regions">Bangalore and Mumbai: Two India Regions</a></h2>
<p>DigitalOcean has BLR1 in Bangalore, and App Platform is available there, as well as in Singapore,
Sydney, New York, Amsterdam, London, Frankfurt, Toronto and others. Voroa runs in Mumbai only. Both keep a
Node.js app in India, so for Indian users neither has a region advantage; for users abroad, App Platform
has many more regions.</p>
<h2 id="usd-invoices-with-gst-vs-rupee-invoices" tabindex="-1"><a class="header-anchor" href="#usd-invoices-with-gst-vs-rupee-invoices">USD Invoices With GST vs Rupee Invoices</a></h2>
<p>DigitalOcean’s <a href="https://docs.digitalocean.com/platform/billing/taxes/ind/">India tax page</a> says it charges
18% GST to customers with a tax location in India, and that a business that registers its GSTIN gets no
GST on the invoice and pays it under reverse charge. It also says: “USD is the only currency we accept for
payments, so all invoices are in USD.”</p>
<p>Voroa bills in rupees with a GST invoice, and takes UPI, Indian cards and netbanking. Our
<a href="/docs/reverse-charge-foreign-cloud-hosting/">reverse charge explainer</a> covers what paying GST yourself
involves, and <a href="/docs/cloud-hosting-inr-vs-usd/">rupee versus dollar bills</a> shows the exchange-rate side.</p>
<h2 id="free-static-sites-vs-a-free-nodejs-service" tabindex="-1"><a class="header-anchor" href="#free-static-sites-vs-a-free-nodejs-service">Free Static Sites vs a Free Node.js Service</a></h2>
<p>App Platform’s free tier covers 3 static-site apps, with 1 GiB of transfer each. Its pricing page does not
state a free tier for a dynamic app such as a Node.js server.</p>
<p>Voroa’s free Nano size is Rs 0 with 512 MB and 0.1 vCPU shared, and it runs web services, static sites
and background workers, up to 4 services per workspace, subject to free capacity. The paid App Platform
container starts at $5.00/month for 1 vCPU shared and 512 MiB; Voroa’s paid web sizes start at Rs 298/month
plus GST for 1 GB and 1 vCPU.</p>
<h2 id="request-based-autoscaling-and-ten-deploy-rollbacks" tabindex="-1"><a class="header-anchor" href="#request-based-autoscaling-and-ten-deploy-rollbacks">Request-Based Autoscaling and Ten-Deploy Rollbacks</a></h2>
<p>App Platform offers “Request-based autoscaling”, where services “scale automatically based on HTTP
traffic metrics”. You can also “roll back to any of the ten most recent successful deployments”.</p>
<p>Voroa does not offer automatic scaling based on load today. You can change a service’s size or run
several copies of it, as the <a href="/docs/scaling/">scaling guide</a> explains. Rollback goes to the previous
release in one click. If you need autoscaling now, App Platform wins this point.</p>
<h2 id="dev-databases-managed-postgresql-and-valkey" tabindex="-1"><a class="header-anchor" href="#dev-databases-managed-postgresql-and-valkey">Dev Databases, Managed PostgreSQL and Valkey</a></h2>
<p>On DigitalOcean, an App Platform dev database is $7/month (PostgreSQL only), Managed PostgreSQL starts at
$15.15/month for 1 GiB and 1 vCPU, and Managed Caching for Valkey starts at $15.00/month.</p>
<p>Voroa’s <a href="/managed-postgresql-india/">managed PostgreSQL</a> starts with a free size, with nightly backups on
paid plans turned on with one switch, and a key-value store in the same workspace.</p>
<h2 id="digitalocean-app-platform-vs-voroa-line-by-line" tabindex="-1"><a class="header-anchor" href="#digitalocean-app-platform-vs-voroa-line-by-line">DigitalOcean App Platform vs Voroa, Line by Line</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>App Platform</th>
<th>Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Deploy</td>
<td>Git or container image; buildpacks or Dockerfile</td>
<td>GitHub; automatic build or Dockerfile</td>
</tr>
<tr>
<td>India region</td>
<td>Bangalore</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Invoice currency</td>
<td>USD, with 18% GST or reverse charge</td>
<td>INR with a GST invoice</td>
</tr>
<tr>
<td>Free tier</td>
<td>3 static sites</td>
<td>Nano size for web services too, subject to free capacity</td>
</tr>
<tr>
<td>Entry paid container</td>
<td>$5.00/month, 512 MiB</td>
<td>Rs 298/month plus GST, 1 GB</td>
</tr>
<tr>
<td>Autoscaling</td>
<td>Request-based</td>
<td>Manual size and copies</td>
</tr>
<tr>
<td>Postgres entry</td>
<td>Dev database $7/month</td>
<td>Free size to start</td>
</tr>
</tbody>
</table></div>
<p>DigitalOcean prices were taken from its pricing pages on 2026-09-30. Check them again before you decide.</p>
<h2 id="picking-between-app-platform-and-voroa" tabindex="-1"><a class="header-anchor" href="#picking-between-app-platform-and-voroa">Picking Between App Platform and Voroa</a></h2>
<p>App Platform wins if you need autoscaling, regions outside India, deploys from container images, or other
DigitalOcean products in the same account. Voroa wins if you want rupee invoices, a free tier that runs a
Node.js server, and a free database size to start.</p>
<h2 id="moving-an-app-platform-service-to-voroa" tabindex="-1"><a class="header-anchor" href="#moving-an-app-platform-service-to-voroa">Moving an App Platform Service to Voroa</a></h2>
<p><img src="https://media.getvoroa.com/img/e71b42fb539e62a5.png" alt="Four-step flow for a Node.js app on a PaaS: connect the repository, pick a region, build and deploy, and on Voroa it runs in Mumbai billed in rupees" width="700" loading="eager" decoding="async"></p>
<ol>
<li>Connect the same repository to a new <a href="/web-services/">Voroa web service</a>.</li>
<li>Copy the environment variables from the App Platform component.</li>
<li><a href="/docs/import-existing-database/">Import your PostgreSQL data</a> by pasting the old connection string.</li>
<li>Test on the Voroa URL, then switch DNS.</li>
</ol>
<p>Comparing AWS as well? Read <a href="/blog/aws-vs-voroa/">Elastic Beanstalk vs Voroa</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="does-digitalocean-app-platform-run-in-india" tabindex="-1"><a class="header-anchor" href="#does-digitalocean-app-platform-run-in-india">Does DigitalOcean App Platform run in India?</a></h3>
<p>Yes. App Platform is available in the Bangalore (BLR) region.</p>
<h3 id="can-i-pay-digitalocean-in-rupees" tabindex="-1"><a class="header-anchor" href="#can-i-pay-digitalocean-in-rupees">Can I pay DigitalOcean in rupees?</a></h3>
<p>Its India tax page says USD is the only accepted currency, so all invoices are in USD.</p>
<h3 id="is-there-a-free-nodejs-tier-on-app-platform" tabindex="-1"><a class="header-anchor" href="#is-there-a-free-nodejs-tier-on-app-platform">Is there a free Node.js tier on App Platform?</a></h3>
<p>The free tier on its pricing page covers 3 static-site apps. A free tier for dynamic apps is not stated.</p>
<h2 id="deploy-nodejs-in-mumbai-billed-in-rupees" tabindex="-1"><a class="header-anchor" href="#deploy-nodejs-in-mumbai-billed-in-rupees">Deploy Node.js in Mumbai, Billed in Rupees</a></h2>
<p>Connect a repository and run your Node.js app in Mumbai with a GST invoice in rupees. Start free, then
compare sizes on the <a href="/pricing/">Voroa pricing page</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Django Hosting in India Without Managing a Server</title>
      <link>https://getvoroa.com/blog/django-hosting-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/django-hosting-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <category>Python</category>
      <description><![CDATA[Django hosting in India without running a VPS: Gunicorn start command, settings from env vars, PostgreSQL, migrations, static files and deploying from GitHub.]]></description>
      <content:encoded><![CDATA[<p>Django hosting in India usually means one of two things: a shared plan with a control panel, or a
VPS where you set up Gunicorn, Nginx and systemd yourself. There is a third way. You push your code
to GitHub and a platform builds and runs it for you. This post explains what that third way needs
from your Django project, step by step.</p>
<p><img src="https://media.getvoroa.com/img/90abf5022d61bc56.png" alt="A Django project on GitHub built and started as a web service in India" width="700" loading="eager" decoding="async"></p>
<h2 id="what-your-django-project-needs" tabindex="-1"><a class="header-anchor" href="#what-your-django-project-needs">What your Django project needs</a></h2>
<h3 id="a-requirements-file" tabindex="-1"><a class="header-anchor" href="#a-requirements-file">A requirements file</a></h3>
<p>Keep <code>requirements.txt</code> at the root of the repository, with versions pinned. The build installs
from it, so an unpinned package cannot change between two builds.</p>
<h3 id="a-production-server" tabindex="-1"><a class="header-anchor" href="#a-production-server">A production server</a></h3>
<p>Django’s own <code>runserver</code> is for development only - the
<a href="https://docs.djangoproject.com/en/stable/howto/deployment/checklist/">Django deployment checklist</a>
says so plainly. Use Gunicorn:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">gunicorn myproject.wsgi --bind 0.0.0.0:$PORT
</code></pre>
</div>
<p>On Voroa you set this as the start command, and you can change it later without recreating the
service. Voroa sets <code>PORT</code> for you. The
<a href="/docs/build-and-start-commands/">build and start commands guide</a> explains both fields.</p>
<h3 id="settings-from-the-environment" tabindex="-1"><a class="header-anchor" href="#settings-from-the-environment">Settings from the environment</a></h3>
<p>Read <code>SECRET_KEY</code>, <code>DEBUG</code>, <code>ALLOWED_HOSTS</code> and <code>DATABASE_URL</code> from
<a href="/docs/environment-variables/">environment variables</a>, not from <code>settings.py</code>. Then the same code
runs on your laptop and in production, and no secret sits in Git.</p>
<h2 id="the-database" tabindex="-1"><a class="header-anchor" href="#the-database">The database</a></h2>
<p>Django needs a database. Create a
<a href="/managed-postgresql-india/">managed PostgreSQL database</a> on the same account, add its connection
URL as <code>DATABASE_URL</code>, and redeploy.</p>
<h3 id="run-migrations-on-every-release" tabindex="-1"><a class="header-anchor" href="#run-migrations-on-every-release">Run migrations on every release</a></h3>
<p>Run <code>python manage.py migrate</code> as part of the release, so the schema is ready before the new version
takes traffic. Doing it by hand is how a deploy ends up running new code against an old schema.</p>
<h3 id="moving-an-existing-database" tabindex="-1"><a class="header-anchor" href="#moving-an-existing-database">Moving an existing database</a></h3>
<p>Already have data elsewhere? You can
<a href="/docs/import-existing-database/">import an existing database</a> and point <code>DATABASE_URL</code> at it.</p>
<h2 id="static-files" tabindex="-1"><a class="header-anchor" href="#static-files">Static files</a></h2>
<p>Collect static files during the build:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">python manage.py collectstatic --noinput
</code></pre>
</div>
<p>Then serve them with a library such as WhiteNoise. That keeps the whole app to one service, with no
separate file server to run.</p>
<h2 id="deploying-on-every-push" tabindex="-1"><a class="header-anchor" href="#deploying-on-every-push">Deploying on every push</a></h2>
<p>Once the repository is connected, every push to your branch builds a new release. The old release
keeps serving until the new one is ready, so users do not see an error page during a deploy. If a
release is wrong, roll back to the previous version from the deploy history. The full steps are in
<a href="/docs/deploy-python-django/">our Python and Django guide</a>.</p>
<h2 id="background-work-in-django" tabindex="-1"><a class="header-anchor" href="#background-work-in-django">Background work in Django</a></h2>
<p>Sending email or generating reports inside a request makes pages slow. Put that work on a queue
(Celery or RQ) and run the consumer as a <a href="/background-workers/">background worker</a>, with the same
code and environment as the web service.</p>
<h2 id="is-it-cheaper-than-a-vps" tabindex="-1"><a class="header-anchor" href="#is-it-cheaper-than-a-vps">Is it cheaper than a VPS?</a></h2>
<p>A small VPS can cost less on paper. The difference is your time: patches, restarts, certificates and
log rotation are yours to handle on a VPS. On Voroa you can start on the free size (Rs 0, subject to
free capacity), and paid sizes start at Rs 298 a month plus GST, billed in rupees with a GST invoice.</p>
<h2 id="django-errors-you-will-meet-on-the-first-deploy" tabindex="-1"><a class="header-anchor" href="#django-errors-you-will-meet-on-the-first-deploy">Django errors you will meet on the first deploy</a></h2>
<ul>
<li><strong>DisallowedHost:</strong> add your Voroa URL and domain to <code>ALLOWED_HOSTS</code>.</li>
<li><strong>Static files return 404:</strong> <code>collectstatic</code> did not run in the build, or WhiteNoise is not in
<code>MIDDLEWARE</code>.</li>
<li><strong>The app starts, then the URL does not answer:</strong> Gunicorn is bound to <code>127.0.0.1</code> instead of
<code>0.0.0.0:$PORT</code>.</li>
</ul>
<h2 id="deploy-django-on-voroa" tabindex="-1"><a class="header-anchor" href="#deploy-django-on-voroa">Deploy Django on Voroa</a></h2>
<p>Using Flask or FastAPI instead? Read <a href="/blog/python-hosting-india/">Python hosting in India</a>. To see
everything a Django web service includes, see <a href="/web-services/">Django and Python hosting on Voroa</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Docker Hosting in India: Run Your Dockerfile Without a VPS</title>
      <link>https://getvoroa.com/blog/docker-hosting-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/docker-hosting-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Docker</category>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Docker hosting in India without a VPS: push a repository with a Dockerfile, let the platform build the image on every push, and get HTTPS and logs in Mumbai.]]></description>
      <content:encoded><![CDATA[<p>Most Docker hosting in India is a VPS with Docker installed. You still have to build images, run
containers, set up a reverse proxy, renew certificates and clean up old images before the disk fills.
If your app already has a Dockerfile, there is a shorter path: let a platform build the image from
your repository and run it for you. This post covers when that makes sense and what to get right.</p>
<p><img src="https://media.getvoroa.com/img/907938e67e347fc1.png" alt="A Dockerfile in a GitHub repository built into an image and run as a service" width="700" loading="eager" decoding="async"></p>
<h2 id="when-to-use-your-own-dockerfile" tabindex="-1"><a class="header-anchor" href="#when-to-use-your-own-dockerfile">When to use your own Dockerfile</a></h2>
<h3 id="you-need-system-packages" tabindex="-1"><a class="header-anchor" href="#you-need-system-packages">You need system packages</a></h3>
<p>If your app needs something like <code>ffmpeg</code>, a PDF library or a specific OS package, a Dockerfile
lets you install it.</p>
<h3 id="you-want-the-same-image-everywhere" tabindex="-1"><a class="header-anchor" href="#you-want-the-same-image-everywhere">You want the same image everywhere</a></h3>
<p>The image you test on your laptop is the image that runs in production. Nothing is detected or
guessed.</p>
<h3 id="when-you-do-not-need-one" tabindex="-1"><a class="header-anchor" href="#when-you-do-not-need-one">When you do not need one</a></h3>
<p>For a plain Node.js or Python app, you can skip the Dockerfile. Voroa detects the language and
builds it for you, as described in <a href="/blog/node-js-hosting-india/">Node.js hosting in India</a>.</p>
<h2 id="writing-a-dockerfile-that-deploys-well" tabindex="-1"><a class="header-anchor" href="#writing-a-dockerfile-that-deploys-well">Writing a Dockerfile that deploys well</a></h2>
<p>A few habits make builds faster and images smaller. The
<a href="https://docs.docker.com/build/building/best-practices/">Dockerfile best practices</a> guide covers them
in depth.</p>
<ul>
<li><strong>Copy the lockfile and install before copying the code.</strong> Docker then reuses the install layer
when only your code changes.</li>
<li><strong>Use a slim base image.</strong> Smaller images build and start faster.</li>
<li><strong>Use a multi-stage build</strong> when you compile assets, so build tools do not ship in the final image.</li>
<li><strong>Run one process.</strong> The container should start your server and nothing else.</li>
</ul>
<h2 id="how-it-works-on-voroa" tabindex="-1"><a class="header-anchor" href="#how-it-works-on-voroa">How it works on Voroa</a></h2>
<p>When you create the service, choose Docker as the runtime. Voroa then builds from your Dockerfile
on every push to your branch, starts the new container, and switches traffic once it is ready. The
guide is at <a href="/docs/docker/">Docker on Voroa</a>.</p>
<h3 id="which-port-should-the-container-listen-on" tabindex="-1"><a class="header-anchor" href="#which-port-should-the-container-listen-on">Which port should the container listen on?</a></h3>
<p>Voroa sets <code>PORT</code>. Listen on it if you can. If the container listens on exactly one other port,
Voroa detects that and routes to it. A server bound only to <code>127.0.0.1</code> cannot be reached from
outside the container.</p>
<h2 id="secrets-in-a-docker-build" tabindex="-1"><a class="header-anchor" href="#secrets-in-a-docker-build">Secrets in a Docker build</a></h2>
<p>Do not bake API keys into the image - anyone with the image can read them. Pass them as
<a href="/docs/environment-variables/">environment variables</a> at runtime. If the build itself needs a
secret, see <a href="/docs/docker-secrets/">secrets and Docker builds</a>.</p>
<h2 id="what-you-still-get" tabindex="-1"><a class="header-anchor" href="#what-you-still-get">What you still get</a></h2>
<p>A Docker service is a normal web service. It gets a URL with HTTPS, custom domains, build and
runtime logs, and rollback to the previous version. It runs in Mumbai and is billed in rupees with a GST invoice.</p>
<h2 id="docker-on-a-vps-vs-on-a-platform" tabindex="-1"><a class="header-anchor" href="#docker-on-a-vps-vs-on-a-platform">Docker on a VPS vs on a platform</a></h2>
<p>On a VPS, <code>docker compose up</code> is only the start: you also run the reverse proxy, TLS renewal, log
rotation, image cleanup and updates. On a platform those are handled, and a new release comes from a
git push instead of an SSH session.</p>
<h2 id="when-the-image-builds-but-the-container-misbehaves" tabindex="-1"><a class="header-anchor" href="#when-the-image-builds-but-the-container-misbehaves">When the image builds but the container misbehaves</a></h2>
<ul>
<li><strong>Build is slow every time:</strong> code is copied before dependencies are installed, so the cache never
hits.</li>
<li><strong>Container exits at once:</strong> the <code>CMD</code> runs a command that finishes instead of a server that stays
up.</li>
<li><strong>URL does not answer:</strong> the server listens only on <code>127.0.0.1</code>.</li>
<li><strong>Deploy refused with ports named:</strong> the container listens on more than one port; Voroa will not
guess which one to route to, so listen on <code>PORT</code>.</li>
</ul>
<h2 id="run-your-dockerfile-on-voroa" tabindex="-1"><a class="header-anchor" href="#run-your-dockerfile-on-voroa">Run your Dockerfile on Voroa</a></h2>
<p>For sizes and prices, see <a href="/web-services/">Docker and web app hosting on Voroa</a>, and start from the
<a href="/docs/deploy-from-github/">first deploy guide</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Fly.io Alternative for India: Node.js Hosting Near Your Users</title>
      <link>https://getvoroa.com/blog/fly-io-alternative-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/fly-io-alternative-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[A Fly.io alternative for Node.js web apps with users in India: Machines vs git push, regions, Asia bandwidth rates, trial vs free tier, Postgres and billing.]]></description>
      <content:encoded><![CDATA[<p>Choosing a <a href="http://Fly.io">Fly.io</a> alternative comes down to control against simplicity, and to where your users are. Fly gives you
Machines you can place in many regions and tune yourself; Voroa takes a GitHub repository and runs it in
Mumbai. This post is for Indian developers deploying a Node.js web app who are choosing between the two.
Every Fly fact here comes from Fly’s own docs.</p>
<h2 id="machines-flytoml-and-the-fly-launch-flow" tabindex="-1"><a class="header-anchor" href="#machines-flytoml-and-the-fly-launch-flow">Machines, fly.toml and the Fly Launch Flow</a></h2>
<p>Fly’s <a href="https://docs.fly.io/launch/">launch documentation</a> shows the flow: <code>fly launch</code> sets up the app,
<code>fly deploy</code> ships it, and <code>fly.toml</code> holds the configuration.</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">fly launch
fly deploy
</code></pre>
</div>
<p>Machines are priced per second, and prices vary by region through a regional multiplier. You decide where
Machines run and how many. That control is Fly’s main strength.</p>
<h2 id="dockerfile-first-deploys-on-both" tabindex="-1"><a class="header-anchor" href="#dockerfile-first-deploys-on-both">Dockerfile-First Deploys on Both</a></h2>
<p><img src="https://media.getvoroa.com/img/38251b2c66def93a.png" alt="Flow from a Dockerfile to a push to GitHub, then Voroa builds it, then the app is live in Mumbai" width="700" loading="eager" decoding="async"></p>
<p>If your Node.js app already has a Dockerfile, you can keep it. On Voroa you choose Docker as the runtime
and Voroa builds from the Dockerfile in your repository; the <a href="/docs/docker/">Dockerfile guide</a> covers the
settings. Without a Dockerfile, Voroa detects a Node.js app and builds it on its own. With auto-deploy on,
each push to your branch becomes a new release.</p>
<h2 id="no-india-region-what-singapore-means-for-your-users" tabindex="-1"><a class="header-anchor" href="#no-india-region-what-singapore-means-for-your-users">No India Region: What Singapore Means for Your Users</a></h2>
<p>The <a href="https://docs.fly.io/reference/regions/">Fly.io regions list</a> has three Asia-Pacific rows: Tokyo
(<code>nrt</code>), Singapore (<code>sin</code>) and Sydney (<code>syd</code>). There is no Mumbai or other India region, so every request
from a user in India travels to Singapore and back at best.</p>
<p>Voroa runs services and databases in Mumbai, its only region. For a global audience, Fly’s coverage is
the stronger choice. The <a href="/docs/data-residency-india/">data residency guide</a> explains what Mumbai-only
means for where your data is held.</p>
<h2 id="asia-pacific-bandwidth-and-regional-machine-pricing" tabindex="-1"><a class="header-anchor" href="#asia-pacific-bandwidth-and-regional-machine-pricing">Asia-Pacific Bandwidth and Regional Machine Pricing</a></h2>
<p>Fly’s <a href="https://docs.fly.io/about/pricing/">pricing page</a> lists data transfer from Asia Pacific at
$0.04/GB, against $0.02/GB for North America and Europe. The page builds Machine costs with a calculator
rather than printing a fixed monthly price, so work out your own number there.</p>
<p>Voroa prices each service size per month. Paid web sizes start at Rs 298/month plus GST for 1 GB and 1
vCPU, and bandwidth is a pooled workspace limit.</p>
<h2 id="a-trial-that-stops-after-five-minutes-vs-a-free-size" tabindex="-1"><a class="header-anchor" href="#a-trial-that-stops-after-five-minutes-vs-a-free-size">A Trial That Stops After Five Minutes vs a Free Size</a></h2>
<p><a href="http://Fly.io">Fly.io</a> has no free tier, only a trial: “2 hours of machine runtime or 7 days of access, whichever comes
first”, and trial Machines stop automatically after running for 5 minutes.</p>
<p>Voroa’s free Nano size is Rs 0 with 512 MB and 0.1 vCPU shared, up to 4 services per workspace, 5 GB
bandwidth and 500 build minutes, subject to free capacity. See the
<a href="/docs/free-instances/">free instances page</a>.</p>
<h2 id="managed-postgres-at-38-vs-a-free-size-in-mumbai" tabindex="-1"><a class="header-anchor" href="#managed-postgres-at-38-vs-a-free-size-in-mumbai">Managed Postgres at $38 vs a Free Size in Mumbai</a></h2>
<p>Fly’s Managed Postgres Basic plan is $38.00 monthly for Shared-2x CPU and 1 GB memory, plus $0.28/GB of
storage. Redis is offered through Upstash, billed on the Fly invoice. On Voroa, a
<a href="/managed-postgresql-india/">managed PostgreSQL database</a> starts with a free size, and a key-value store
sits in the same workspace.</p>
<h2 id="flyio-vs-voroa-side-by-side" tabindex="-1"><a class="header-anchor" href="#flyio-vs-voroa-side-by-side"><a href="http://Fly.io">Fly.io</a> vs Voroa Side by Side</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th><a href="http://Fly.io">Fly.io</a></th>
<th>Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Deploy</td>
<td><code>fly launch</code>, <code>fly deploy</code>, <code>fly.toml</code></td>
<td>GitHub push, when auto-deploy is on</td>
</tr>
<tr>
<td>Dockerfile</td>
<td>Yes</td>
<td>Yes, or automatic Node.js build</td>
</tr>
<tr>
<td>Nearest region to India</td>
<td>Singapore</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Asia-Pacific bandwidth</td>
<td>$0.04/GB</td>
<td>Pooled per workspace plan</td>
</tr>
<tr>
<td>Free option</td>
<td>Trial: 2 hours or 7 days</td>
<td>Nano size, subject to free capacity</td>
</tr>
<tr>
<td>Managed Postgres</td>
<td>From $38.00/month</td>
<td>Free size to start</td>
</tr>
<tr>
<td>Billing</td>
<td>USD; taxes not stated</td>
<td>INR with a GST invoice</td>
</tr>
</tbody>
</table></div>
<p>Fly prices were taken from its pricing pages on 2026-09-30. Check them again before you decide.</p>
<h2 id="what-you-stop-managing-with-a-git-push-flyio-alternative" tabindex="-1"><a class="header-anchor" href="#what-you-stop-managing-with-a-git-push-flyio-alternative">What You Stop Managing With a Git-Push <a href="http://Fly.io">Fly.io</a> Alternative</a></h2>
<p>On Voroa there is no <code>fly.toml</code> and no Machine placement to think about. You pick a size, push, and the
new release starts before the old one stops; a rollback is one click. Voroa sets <code>PORT</code>, and environment
values are encrypted at rest and never shown back. To grow, you change the size or run more copies of a
service; automatic scaling based on load is not available today.</p>
<h2 id="when-fly-wins" tabindex="-1"><a class="header-anchor" href="#when-fly-wins">When Fly Wins</a></h2>
<p>Fly wins when your users are spread across many countries, when you want fine control over Machines from
the CLI, or when per-second pricing suits a workload that runs only part of the day. Voroa wins when most
users are in India and you want rupee billing with a GST invoice.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="does-flyio-have-a-region-in-india" tabindex="-1"><a class="header-anchor" href="#does-flyio-have-a-region-in-india">Does <a href="http://Fly.io">Fly.io</a> have a region in India?</a></h3>
<p>Not in its regions list. The Asia-Pacific regions are Tokyo, Singapore and Sydney.</p>
<h3 id="is-there-a-free-plan-on-flyio" tabindex="-1"><a class="header-anchor" href="#is-there-a-free-plan-on-flyio">Is there a free plan on <a href="http://Fly.io">Fly.io</a>?</a></h3>
<p>No. Fly offers a trial of 2 hours of machine runtime or 7 days, whichever comes first.</p>
<h3 id="how-do-i-move-a-fly-app-to-voroa" tabindex="-1"><a class="header-anchor" href="#how-do-i-move-a-fly-app-to-voroa">How do I move a Fly app to Voroa?</a></h3>
<p>Connect the repository, copy the start command and environment variables from <code>fly.toml</code>, import your
database, then test and switch DNS. Our <a href="/blog/railway-alternative-india/">Railway alternative guide</a> and the
<a href="/blog/docker-hosting-india/">Docker hosting guide for India</a> cover the same kind of move.</p>
<h2 id="run-your-container-in-mumbai" tabindex="-1"><a class="header-anchor" href="#run-your-container-in-mumbai">Run Your Container in Mumbai</a></h2>
<p>Push your Node.js app or Dockerfile and run it in Mumbai, with a fixed monthly price and a GST invoice.
Compare sizes on the <a href="/pricing/">Voroa pricing page</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Free Web App Hosting in India: What the Free Tier Gives You</title>
      <link>https://getvoroa.com/blog/free-web-app-hosting-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/free-web-app-hosting-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Free tier</category>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[What free web app hosting in India really includes on Voroa: memory, CPU, bandwidth, build minutes, domains, limits, and when to move to a paid size.]]></description>
      <content:encoded><![CDATA[<p>Free web app hosting in India is a good way to try a platform before you pay. The catch with any
free tier is the small print: sleep timers, tiny limits, or a card you must add first. This post
lists exactly what Voroa’s free tier includes, what it does not, and the signs that your app has
outgrown it.</p>
<p><img src="https://media.getvoroa.com/img/fb0c3ae1698f75e4.png" alt="What the free tier includes per service and per workspace" width="700" loading="eager" decoding="async"></p>
<h2 id="what-you-get-per-service" tabindex="-1"><a class="header-anchor" href="#what-you-get-per-service">What you get, per service</a></h2>
<p>Every service can use the free Nano size, subject to free capacity:</p>
<ul>
<li>Rs 0</li>
<li>512 MB of memory</li>
<li>0.1 vCPU, shared</li>
<li>Runs in Mumbai (IN)</li>
</ul>
<p>Free services currently stay running between requests, so the first visitor does not wait for the
app to wake up. That is how it works today; if it changes, it will be announced first.</p>
<h2 id="what-you-get-per-workspace" tabindex="-1"><a class="header-anchor" href="#what-you-get-per-workspace">What you get, per workspace</a></h2>
<h3 id="limits-on-the-free-plan" tabindex="-1"><a class="header-anchor" href="#limits-on-the-free-plan">Limits on the Free plan</a></h3>
<ul>
<li>Up to 4 services in total</li>
<li>5 GB of bandwidth a month</li>
<li>500 build minutes a month</li>
<li>2 custom domains, with automatic HTTPS</li>
<li>1 background worker and 2 scheduled tasks</li>
</ul>
<h3 id="no-card-needed" tabindex="-1"><a class="header-anchor" href="#no-card-needed">No card needed</a></h3>
<p>You can deploy without adding a payment method.</p>
<h2 id="what-runs-on-the-free-tier" tabindex="-1"><a class="header-anchor" href="#what-runs-on-the-free-tier">What runs on the free tier</a></h2>
<p>Web services, <a href="/static-sites/">static sites</a> and background workers all run on the free size, and
the free plan also includes a small managed PostgreSQL database, subject to free capacity. The full
list is in <a href="/docs/free-instances/">free instances</a>.</p>
<h2 id="what-free-is-good-for" tabindex="-1"><a class="header-anchor" href="#what-free-is-good-for">What free is good for</a></h2>
<p>A portfolio, a college project, a client demo, a hackathon build, or a side project that gets a few
visitors a day.</p>
<h2 id="things-to-know-before-you-rely-on-it" tabindex="-1"><a class="header-anchor" href="#things-to-know-before-you-rely-on-it">Things to know before you rely on it</a></h2>
<ul>
<li><strong>Shared CPU.</strong> 0.1 vCPU is enough for light traffic, not for heavy work in each request.</li>
<li><strong>512 MB of memory.</strong> A large Node.js or Python app can run out; watch the runtime logs.</li>
<li><strong>Build minutes count.</strong> Each push uses build minutes; 500 a month is plenty for normal work.</li>
<li><strong>Capacity is shared.</strong> Free sizes are offered while free capacity is available.</li>
</ul>
<h2 id="a-free-tier-is-not-a-free-trial" tabindex="-1"><a class="header-anchor" href="#a-free-tier-is-not-a-free-trial">A free tier is not a free trial</a></h2>
<p>A trial ends on a date and then asks for a card. A free tier is a size you can stay on. On Voroa the
free size is not time-limited today: a small project can stay on it while it fits the limits
above, and you pay only when you choose a bigger size.</p>
<h2 id="when-it-is-not-enough" tabindex="-1"><a class="header-anchor" href="#when-it-is-not-enough">When it is not enough</a></h2>
<p>If the app is slow under load, runs out of memory, or needs more than 4 services, it is time to move
to a paid size. Paid sizes start at Rs 298 a month plus GST, billed in rupees with a GST invoice.
See <a href="/pricing/">pricing</a> for every size. Comparing with Netlify’s free plan? Its
<a href="/blog/netlify-free-plan-limits/">free plan limits</a> are set out separately.</p>
<h2 id="moving-up-later" tabindex="-1"><a class="header-anchor" href="#moving-up-later">Moving up later</a></h2>
<p>Changing the size does not mean moving the app. You pick a bigger size on the same service and it
redeploys. Your URL, domain and settings stay the same.</p>
<h2 id="make-the-most-of-free-hosting" tabindex="-1"><a class="header-anchor" href="#make-the-most-of-free-hosting">Make the most of free hosting</a></h2>
<p>Read the port from <code>PORT</code> - the <a href="https://nodejs.org/en/learn/command-line/how-to-read-environment-variables-from-nodejs">Node.js documentation</a>
covers reading environment variables - keep secrets in environment variables, and commit your
lockfile so builds are quick and repeatable.</p>
<h2 id="start-free-on-voroa" tabindex="-1"><a class="header-anchor" href="#start-free-on-voroa">Start free on Voroa</a></h2>
<p>Still choosing a type of hosting? Read
<a href="/blog/web-app-hosting-india/">shared hosting vs VPS vs a deploy platform</a>. To start, see
<a href="/web-services/">free web app hosting on Voroa</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>A Heroku Alternative for India: Move Your App, Step by Step</title>
      <link>https://getvoroa.com/blog/heroku-alternative-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/heroku-alternative-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[Looking for a Heroku alternative in India? What to check, how the move works for a Node.js or Python app, and what changes in deploys, data and billing.]]></description>
      <content:encoded><![CDATA[<p>Many developers in India start on Heroku, and many look for a Heroku alternative once the app
matters. The usual reasons are simple: the bill is in US dollars, and Heroku’s standard plans run in
the US and Europe - its Mumbai region is only for the higher-priced Private Spaces, as its own
<a href="https://devcenter.heroku.com/articles/regions">regions page</a> shows. This post covers what to check
in an alternative and how the move works.</p>
<p><img src="https://media.getvoroa.com/img/0e2510eff1dd3dc5.png" alt="Moving an app from one platform to another: repository, settings, database, domain" width="700" loading="eager" decoding="async"></p>
<h2 id="what-to-check-in-an-alternative" tabindex="-1"><a class="header-anchor" href="#what-to-check-in-an-alternative">What to check in an alternative</a></h2>
<h3 id="the-same-workflow" tabindex="-1"><a class="header-anchor" href="#the-same-workflow">The same workflow</a></h3>
<p>Heroku’s best idea is push-to-deploy. Keep it. Your alternative should deploy from GitHub on every
push, with rollbacks. Read <a href="/blog/deploy-from-github-india/">how push-to-deploy works</a> if you want
the details.</p>
<h3 id="servers-in-india" tabindex="-1"><a class="header-anchor" href="#servers-in-india">Servers in India</a></h3>
<p>If your users are in India, the server should be too, on every plan. On Voroa, services run in
Mumbai, including the free size.</p>
<h3 id="billing-that-works-for-an-indian-business" tabindex="-1"><a class="header-anchor" href="#billing-that-works-for-an-indian-business">Billing that works for an Indian business</a></h3>
<p>Rupee pricing, a GST invoice, and no foreign-currency charge on the card. Voroa bills in rupees with
a GST invoice. What the same app costs on Heroku, worked through in rupees, is in our
<a href="/blog/heroku-pricing-india/">Heroku pricing</a> guide.</p>
<h3 id="the-pieces-you-use" tabindex="-1"><a class="header-anchor" href="#the-pieces-you-use">The pieces you use</a></h3>
<p>List what your Heroku app uses today: web dynos, worker dynos, scheduler jobs, Postgres, Redis.
Each needs a home on the new platform.</p>
<h2 id="how-the-move-works" tabindex="-1"><a class="header-anchor" href="#how-the-move-works">How the move works</a></h2>
<h3 id="1-connect-the-same-repository" tabindex="-1"><a class="header-anchor" href="#1-connect-the-same-repository">1. Connect the same repository</a></h3>
<p>Your code does not change. Connect the GitHub repository to a new web service. The steps are in
<a href="/docs/deploy-from-github/">how to deploy from GitHub</a>.</p>
<h3 id="2-copy-your-config-vars" tabindex="-1"><a class="header-anchor" href="#2-copy-your-config-vars">2. Copy your config vars</a></h3>
<p>Heroku calls them config vars. Here they are
<a href="/docs/environment-variables/">environment variables</a>. Copy each one across.</p>
<h3 id="3-check-the-start-command" tabindex="-1"><a class="header-anchor" href="#3-check-the-start-command">3. Check the start command</a></h3>
<p>A <code>Procfile</code> line like <code>web: node server.js</code> becomes the service’s start command. A <code>worker:</code> line
becomes a <a href="/background-workers/">background worker</a>, and Heroku Scheduler jobs become
<a href="/docs/scheduled-tasks-cron/">scheduled tasks</a>.</p>
<h4 id="the-port" tabindex="-1"><a class="header-anchor" href="#the-port">The port</a></h4>
<p>Heroku sets <code>PORT</code>, and so does Voroa. If your app already reads <code>process.env.PORT</code>, nothing changes.</p>
<h3 id="4-move-the-database" tabindex="-1"><a class="header-anchor" href="#4-move-the-database">4. Move the database</a></h3>
<p>Create a <a href="/managed-postgresql-india/">managed PostgreSQL database</a> and
<a href="/docs/import-existing-database/">import your existing data</a>. Then point <code>DATABASE_URL</code> at it.</p>
<h3 id="5-switch-the-domain" tabindex="-1"><a class="header-anchor" href="#5-switch-the-domain">5. Switch the domain</a></h3>
<p>Add your domain to the new service, wait for HTTPS to be ready, then update DNS. Keep the old app
running until traffic has moved.</p>
<h2 id="before-you-switch" tabindex="-1"><a class="header-anchor" href="#before-you-switch">Before you switch</a></h2>
<p>Test the new service on its Voroa URL first. Run a few real flows - sign up, pay, upload - against
the new database. Once it works there, the domain switch is the only step your users notice.</p>
<h2 id="after-the-move" tabindex="-1"><a class="header-anchor" href="#after-the-move">After the move</a></h2>
<p>Watch the runtime logs for a day, compare error rates with the old app, and only then turn the
Heroku app off.</p>
<h2 id="common-migration-problems" tabindex="-1"><a class="header-anchor" href="#common-migration-problems">Common migration problems</a></h2>
<ul>
<li><strong>An add-on has no direct match.</strong> Check each Heroku add-on and decide whether it moves to a
managed service, to your own code, or to a third-party provider you call over the internet.</li>
<li><strong>Buildpack-specific settings.</strong> Settings that only a Heroku buildpack reads need an equivalent
build or start command.</li>
<li><strong>Hard-coded Heroku URLs.</strong> Search the code for <code>herokuapp.com</code> before you switch the domain.</li>
<li><strong>Background jobs stop running.</strong> A worker dyno that was not recreated as a background worker
means queued jobs silently pile up - check it on day one.</li>
</ul>
<h2 id="move-to-voroa" tabindex="-1"><a class="header-anchor" href="#move-to-voroa">Move to Voroa</a></h2>
<p>For sizes and prices, see <a href="/web-services/">web app hosting on Voroa</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Hostinger Alternative for Developers: Node.js Hosting in India</title>
      <link>https://getvoroa.com/blog/hostinger-alternative-developers-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/hostinger-alternative-developers-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[A Hostinger alternative for Node.js developers in India: which Hostinger plan runs Node.js, the 48-month price, Supabase Postgres, workers and monthly billing.]]></description>
      <content:encoded><![CDATA[<p>Many Indian developers already have a Hostinger account for a website, so a Hostinger alternative only
comes up once the project becomes a real Node.js backend: an API, a worker, a PostgreSQL database, or a
team that wants to pay month by month. This post is for developers deploying Express, NestJS, Next.js or
Fastify apps. It explains which Hostinger plan runs Node.js, what the price really is over time, and
where each option fits.</p>
<h2 id="hostinger-web-apps-and-the-plans-that-run-nodejs" tabindex="-1"><a class="header-anchor" href="#hostinger-web-apps-and-the-plans-that-run-nodejs">Hostinger Web Apps and the Plans That Run Node.js</a></h2>
<p>Hostinger’s <a href="https://www.hostinger.com/web-apps-hosting">web apps hosting page</a> sums up its pitch: “Push
from GitHub, drop a ZIP, or let your AI agent deploy for you. Flat monthly price, zero DevOps”. The
frameworks it lists are Express, NestJS, Next.js and Fastify.</p>
<p>There is a condition to check first. Hostinger’s support guide says Node.js deploys from GitHub need a
Business web hosting plan or any Cloud hosting plan. So confirm your plan qualifies before you build
around it. On Voroa, every web service can deploy from GitHub, including the free size.</p>
<h2 id="zip-upload-github-or-an-ai-agent-three-ways-to-ship" tabindex="-1"><a class="header-anchor" href="#zip-upload-github-or-an-ai-agent-three-ways-to-ship">ZIP Upload, GitHub or an AI Agent: Three Ways to Ship</a></h2>
<p><img src="https://media.getvoroa.com/img/5f4e039b5bdb9e07.png" alt="Flow from uploading files to a Git push instead, then build and release, then an HTTPS URL" width="700" loading="eager" decoding="async"></p>
<p>Hostinger gives you three ways to deploy, and its
<a href="https://docs.hostinger.com/node.js/github">Node.js GitHub guide</a> says it redeploys on every push. If you
like dropping a ZIP file, that is something Voroa does not offer.</p>
<p>Voroa has one path: connect a GitHub repository. Voroa installs dependencies, builds and starts the app,
and gives it an HTTPS URL. With auto-deploy on, each push becomes a new release; the new release starts
before the old one stops, and a rollback is one click. Your server reads <code>process.env.PORT</code>, which Voroa
sets. The <a href="/docs/deploy-nodejs/">Node.js deploy guide</a> walks through it, including Node versions 22, 20
and 18.</p>
<h2 id="the-48-month-term-and-the-renewal-price" tabindex="-1"><a class="header-anchor" href="#the-48-month-term-and-the-renewal-price">The 48-Month Term and the Renewal Price</a></h2>
<p>On Hostinger’s India site, the web apps plan is Rs 249/month as a promotional price on a 48-month term,
renewing at Rs 649/month. The US site shows $3.99/month on 48 months ($191.52 upfront), renewing at
$16.99/month. The low price needs a long prepay, and the renewal is more than double. Hostinger does offer
a 30-day money-back guarantee.</p>
<p>Voroa prices each service per month. Paid web sizes start at Rs 298/month plus GST for 1 GB and 1 vCPU,
and the free Nano size is Rs 0 with 512 MB and 0.1 vCPU shared, subject to free capacity. See
<a href="/docs/free-instances/">what the free tier includes</a>.</p>
<h2 id="mysql-supabase-postgres-and-voroas-managed-databases" tabindex="-1"><a class="header-anchor" href="#mysql-supabase-postgres-and-voroas-managed-databases">MySQL, Supabase Postgres and Voroa’s Managed Databases</a></h2>
<p>The Hostinger web apps page offers Managed MySQL and a one-click Supabase integration, which its
<a href="https://www.hostinger.com/in/web-apps-hosting">India web apps page</a> says provides “Auth, Postgres and
storage”. So Postgres is available through Supabase rather than as Hostinger’s own managed product. We
did not find a Redis product on that page.</p>
<p>Voroa offers a <a href="/managed-postgresql-india/">managed PostgreSQL database</a> with a free size, and a
<a href="/managed-redis-india/">key-value store for caching</a>, in the same workspace as your app and on the same
bill.</p>
<h2 id="where-the-web-apps-product-actually-runs" tabindex="-1"><a class="header-anchor" href="#where-the-web-apps-product-actually-runs">Where the Web Apps Product Actually Runs</a></h2>
<p>Hostinger’s server-location page lists India for web and cloud hosting plans, and Mumbai for Agency
plans. Its web apps product page lists only “North America, Europe, Asia”, so it does not say whether the
web apps product can use the India location. Ask before you buy if this matters. Voroa runs every service
and database in Mumbai.</p>
<h2 id="backends-need-more-than-a-web-server" tabindex="-1"><a class="header-anchor" href="#backends-need-more-than-a-web-server">Backends Need More Than a Web Server</a></h2>
<p>A real Node.js backend often has a queue consumer and a few scheduled jobs. On Voroa, a
<a href="/background-workers/">background worker</a> runs your queue consumer as its own service, and
<a href="/docs/scheduled-tasks-cron/">scheduled tasks</a> run your cron jobs. Environment values are encrypted at
rest and never shown back.</p>
<h2 id="hostinger-and-voroa-for-a-nodejs-app" tabindex="-1"><a class="header-anchor" href="#hostinger-and-voroa-for-a-nodejs-app">Hostinger and Voroa for a Node.js App</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Hostinger web apps</th>
<th>Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Deploy</td>
<td>GitHub, ZIP or AI agent</td>
<td>GitHub, build on push when auto-deploy is on</td>
</tr>
<tr>
<td>Node.js from GitHub</td>
<td>Business web or Cloud plans</td>
<td>Every web service</td>
</tr>
<tr>
<td>Entry price</td>
<td>Rs 249/month on 48 months, renews Rs 649/month</td>
<td>Rs 298/month plus GST, billed monthly</td>
</tr>
<tr>
<td>Free option</td>
<td>30-day money-back guarantee</td>
<td>Nano size, subject to free capacity</td>
</tr>
<tr>
<td>Database</td>
<td>MySQL; Postgres through Supabase</td>
<td>Managed PostgreSQL and key-value</td>
</tr>
<tr>
<td>India location</td>
<td>Listed for web and cloud hosting</td>
<td>Mumbai</td>
</tr>
</tbody>
</table></div>
<p>Hostinger prices were taken from its India and US pages on 2026-09-30. Check again before you decide.</p>
<h2 id="who-should-pick-a-hostinger-alternative-and-who-should-stay" tabindex="-1"><a class="header-anchor" href="#who-should-pick-a-hostinger-alternative-and-who-should-stay">Who Should Pick a Hostinger Alternative, and Who Should Stay</a></h2>
<p>Stay with Hostinger if you are happy to prepay for 48 months, your app uses MySQL or Supabase, you want
ZIP uploads, or you already run sites there and want one account. Move if you want month-by-month
billing, a managed Postgres next to your app, or workers and cron jobs as separate services. For a
deeper look at the stack, read our <a href="/blog/node-js-hosting-india/">Node.js hosting guide for India</a>, or
compare with <a href="/blog/render-alternative-india/">our Render alternative guide</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="is-hostingers-low-price-the-monthly-price" tabindex="-1"><a class="header-anchor" href="#is-hostingers-low-price-the-monthly-price">Is Hostinger’s low price the monthly price?</a></h3>
<p>The Rs 249/month price on the India site is promotional, on a 48-month term, and renews at Rs 649/month.</p>
<h3 id="can-i-use-postgresql-on-hostinger-web-apps" tabindex="-1"><a class="header-anchor" href="#can-i-use-postgresql-on-hostinger-web-apps">Can I use PostgreSQL on Hostinger web apps?</a></h3>
<p>Yes, through its one-click Supabase integration, which provides “Auth, Postgres and storage”. Hostinger’s
own managed database on that page is MySQL.</p>
<h3 id="do-i-need-a-dockerfile-for-voroa" tabindex="-1"><a class="header-anchor" href="#do-i-need-a-dockerfile-for-voroa">Do I need a Dockerfile for Voroa?</a></h3>
<p>No. Voroa detects a Node.js app and builds it. If you have a Dockerfile, you can build from it instead.</p>
<h2 id="deploy-an-express-or-nestjs-app-from-github" tabindex="-1"><a class="header-anchor" href="#deploy-an-express-or-nestjs-app-from-github">Deploy an Express or NestJS App from GitHub</a></h2>
<p>Pay month by month in rupees, with a GST invoice, and run your app, workers and database in Mumbai. Pick a
size from the <a href="/web-services/">web services overview</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Next.js Hosting in India with Server Rendering</title>
      <link>https://getvoroa.com/blog/nextjs-hosting-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/nextjs-hosting-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <category>Next.js</category>
      <description><![CDATA[Next.js hosting in India: when a static export is enough, running server rendering as a web service, build-time variables, and deploying every push to Mumbai.]]></description>
      <content:encoded><![CDATA[<p>Next.js hosting in India needs a little thought, because Next.js can be two different things. It can
be a static site that you export once, or a server that renders pages on each request. The choice
decides what you host, what it costs and how fast pages feel for users in India. This post explains
the difference and how to run the server version close to your users.</p>
<p><img src="https://media.getvoroa.com/img/05ee89a4f9d29b57.png" alt="A Next.js app built on each push and served from a server in Mumbai" width="700" loading="eager" decoding="async"></p>
<h2 id="static-export-or-server" tabindex="-1"><a class="header-anchor" href="#static-export-or-server">Static export or server?</a></h2>
<h3 id="static-export" tabindex="-1"><a class="header-anchor" href="#static-export">Static export</a></h3>
<p>If every page can be built ahead of time, set <code>output: 'export'</code> and <code>next build</code> gives you plain
files. The <a href="https://nextjs.org/docs/app/guides/static-exports">Next.js static exports guide</a> lists
what does not work in this mode. Commit the built files and host them as a
<a href="/static-sites/">static site</a>, which is free on Voroa. No build runs on a static site, so the built
files must be in the repository.</p>
<h3 id="server-rendering" tabindex="-1"><a class="header-anchor" href="#server-rendering">Server rendering</a></h3>
<p>If you use server-side rendering, API routes, middleware or image optimisation, you need a running
Node.js server. That is a web service.</p>
<h3 id="how-to-decide" tabindex="-1"><a class="header-anchor" href="#how-to-decide">How to decide</a></h3>
<p>If you are not sure, you probably need the server: most apps with login, a dashboard or data that
changes per user do.</p>
<h2 id="running-nextjs-as-a-web-service" tabindex="-1"><a class="header-anchor" href="#running-nextjs-as-a-web-service">Running Next.js as a web service</a></h2>
<h3 id="build-command" tabindex="-1"><a class="header-anchor" href="#build-command">Build command</a></h3>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">npm install &amp;&amp; npm run build
</code></pre>
</div>
<h3 id="start-command" tabindex="-1"><a class="header-anchor" href="#start-command">Start command</a></h3>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">npm start
</code></pre>
</div>
<p><code>next start</code> reads the <code>PORT</code> environment variable, which Voroa sets for you. The
<a href="/docs/build-and-start-commands/">build and start commands guide</a> shows where to set both.</p>
<h3 id="environment-variables-at-build-time" tabindex="-1"><a class="header-anchor" href="#environment-variables-at-build-time">Environment variables at build time</a></h3>
<p>Variables that start with <code>NEXT_PUBLIC_</code> are baked into the browser bundle during the build. Set
them before you deploy, and redeploy after you change them. Server-only secrets must not start with
<code>NEXT_PUBLIC_</code>, or they end up in every visitor’s browser. See
<a href="/docs/environment-variables/">environment variables</a>.</p>
<h2 id="deploys-and-rollbacks" tabindex="-1"><a class="header-anchor" href="#deploys-and-rollbacks">Deploys and rollbacks</a></h2>
<p>Every push to your branch builds a new release. The old release keeps serving until the new one is
ready, so visitors do not see a broken page mid-deploy. If a release is wrong, roll back in one
click. The full steps are in <a href="/docs/deploy-nextjs/">our Next.js deploy guide</a>.</p>
<h2 id="why-host-it-in-india" tabindex="-1"><a class="header-anchor" href="#why-host-it-in-india">Why host it in India</a></h2>
<p>Server-rendered pages are built on each request, often with a database call or two inside. A server
in Mumbai keeps that request short for users in India. You are billed in rupees with a GST invoice,
and paid sizes start at Rs 298 a month plus GST.</p>
<h2 id="caching-and-performance" tabindex="-1"><a class="header-anchor" href="#caching-and-performance">Caching and performance</a></h2>
<p>Pages that do not change per user can be generated once and reused instead of rendered on every
request. Use Next.js’s own caching and revalidation for those pages, keep database calls in server
components short, and put repeated reads in a cache. Every millisecond a server-rendered page spends
waiting is a millisecond the visitor waits too.</p>
<h2 id="nextjs-build-and-runtime-errors-to-expect" tabindex="-1"><a class="header-anchor" href="#nextjs-build-and-runtime-errors-to-expect">Next.js build and runtime errors to expect</a></h2>
<ul>
<li><strong>Build runs out of memory:</strong> build resources come from your workspace plan, not the service size -
trim the build or move to a higher plan.</li>
<li><strong>A variable is empty in the browser:</strong> it was added after the build; redeploy.</li>
<li><strong>Images do not load:</strong> remote image hosts must be listed in <code>next.config.js</code>.</li>
</ul>
<h2 id="host-nextjs-on-voroa" tabindex="-1"><a class="header-anchor" href="#host-nextjs-on-voroa">Host Next.js on Voroa</a></h2>
<p>Your Next.js app calls its own API? Read
<a href="/blog/backend-api-hosting-india/">backend API hosting in India</a>. For sizes and what is included, see
<a href="/web-services/">Next.js and web app hosting on Voroa</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Node.js Hosting in India: What to Check Before You Choose</title>
      <link>https://getvoroa.com/blog/node-js-hosting-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/node-js-hosting-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <category>Node.js</category>
      <description><![CDATA[Choosing Node.js hosting in India? Check the server city, how deploys and rollbacks work, the PORT setup, scaling, and how you are billed. A simple checklist.]]></description>
      <content:encoded><![CDATA[<p>Node.js hosting in India is easy to buy and hard to compare. Every plan says “fast” and “secure”.
What actually decides your day is simpler: where the server sits, how a new version goes live, what
happens when it breaks, and what the bill looks like at the end of the month. This post is a short
checklist you can use on any provider, including us.</p>
<p><img src="https://media.getvoroa.com/img/4240d56889247b9a.png" alt="A Node.js app going from a GitHub push to a live URL in Mumbai" width="700" loading="eager" decoding="async"></p>
<h2 id="where-the-server-runs" tabindex="-1"><a class="header-anchor" href="#where-the-server-runs">Where the server runs</a></h2>
<p>If most of your users are in India, a server in India cuts the round trip for every request. A
server in Europe or the US adds that distance to every API call your frontend makes, and a page that
makes ten calls pays it ten times.</p>
<h3 id="ask-for-the-region-by-name" tabindex="-1"><a class="header-anchor" href="#ask-for-the-region-by-name">Ask for the region by name</a></h3>
<p>“Asia” is not a region. Ask which city. On Voroa, services run in Mumbai (IN).</p>
<h2 id="how-a-new-version-goes-live" tabindex="-1"><a class="header-anchor" href="#how-a-new-version-goes-live">How a new version goes live</a></h2>
<p>This is where old-style hosting and a platform differ the most.</p>
<h3 id="upload-and-restart" tabindex="-1"><a class="header-anchor" href="#upload-and-restart">Upload and restart</a></h3>
<p>On a shared or VPS plan you copy files, run <code>npm install</code>, and restart the process yourself, often
with a process manager such as PM2. If the restart fails, the site is down until you fix it.</p>
<h3 id="deploy-from-github" tabindex="-1"><a class="header-anchor" href="#deploy-from-github">Deploy from GitHub</a></h3>
<p>On a platform, you connect a repository once. Every push to your branch builds a new release. On
Voroa the new release starts before the old one stops; with a health check set, traffic moves only
once it answers. The steps
are in <a href="/docs/deploy-nodejs/">our Node.js deploy guide</a>, and
<a href="/blog/deploy-from-github-india/">how push-to-deploy works</a> explains each stage.</p>
<h3 id="rolling-back" tabindex="-1"><a class="header-anchor" href="#rolling-back">Rolling back</a></h3>
<p>Things break. Check that you can go back to the previous release quickly. On Voroa every release is
kept in the <a href="/docs/deploy-history-and-logs/">deploy history</a>, and you can roll back to the previous
version from there.</p>
<h2 id="the-port-question" tabindex="-1"><a class="header-anchor" href="#the-port-question">The port question</a></h2>
<p>A Node.js server has to listen somewhere. Voroa sets <code>PORT</code> for you, so reading
<code>process.env.PORT</code> is the safest choice:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0');
</code></pre>
</div>
<p>If your app listens on one fixed port instead, Voroa detects it and routes to it. Listen on
<code>0.0.0.0</code>, not <code>127.0.0.1</code>, or nothing outside the container can reach it.</p>
<h3 id="a-quick-test" tabindex="-1"><a class="header-anchor" href="#a-quick-test">A quick test</a></h3>
<p>Run your app locally with <code>PORT=4000 node server.js</code>. If it answers on 4000, it will work here.</p>
<h2 id="nodejs-version-and-packages" tabindex="-1"><a class="header-anchor" href="#nodejs-version-and-packages">Node.js version and packages</a></h2>
<p>Pin the Node.js version your app was tested on, and commit <code>package-lock.json</code> so every build
installs the same packages. The <a href="https://nodejs.org/en/about/previous-releases">Node.js release schedule</a>
shows which versions are still supported - an LTS version is the safe default.</p>
<h2 id="secrets-and-configuration" tabindex="-1"><a class="header-anchor" href="#secrets-and-configuration">Secrets and configuration</a></h2>
<p>Database URLs and API keys belong in <a href="/docs/environment-variables/">environment variables</a>, not in
the repository. On Voroa, values are stored encrypted and are not shown back after you save them.</p>
<h2 id="scaling-when-traffic-grows" tabindex="-1"><a class="header-anchor" href="#scaling-when-traffic-grows">Scaling when traffic grows</a></h2>
<p>Start small. When the app needs more, pick a bigger size for the same service; paid sizes can also
run extra copies. Keep the app stateless - sessions in a database or cache, not in memory - so any
copy can answer any request.</p>
<h2 id="what-you-will-be-billed" tabindex="-1"><a class="header-anchor" href="#what-you-will-be-billed">What you will be billed</a></h2>
<p>Look for three things: the currency, the tax invoice, and whether the price moves with the exchange
rate.</p>
<ul>
<li>Billing in rupees means no forex markup on your card.</li>
<li>A GST invoice lets a registered business claim input tax credit.</li>
<li>A free size lets you test before paying. On Voroa the free size is Rs 0 (subject to free
capacity), and paid sizes start at Rs 298 a month plus GST. See <a href="/pricing/">pricing</a>.</li>
</ul>
<h2 id="free-nodejs-hosting-and-prices-in-inr" tabindex="-1"><a class="header-anchor" href="#free-nodejs-hosting-and-prices-in-inr">Free Node.js hosting and prices in INR</a></h2>
<p>You can host a Node.js app on Voroa for free while you build, and move to a paid size when it has
users. Free Node.js hosting here means the <strong>Nano</strong> size; paid sizes are priced in rupees and never
sleep.</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Size</th>
<th>Price</th>
<th>Memory</th>
<th>CPU</th>
<th>When idle</th>
</tr>
</thead>
<tbody>
<tr>
<td>Nano</td>
<td>Free</td>
<td>512 MB</td>
<td>0.1 vCPU, shared</td>
<td>Sleeps after 15 minutes without requests</td>
</tr>
<tr>
<td>Starter</td>
<td>Rs 298 a month</td>
<td>1 GB</td>
<td>1 vCPU</td>
<td>Always on</td>
</tr>
<tr>
<td>Standard</td>
<td>Rs 498 per month</td>
<td>2 GB</td>
<td>1 vCPU</td>
<td>Always on</td>
</tr>
<tr>
<td>Pro</td>
<td>Rs 998 per month</td>
<td>4 GB</td>
<td>2 vCPU</td>
<td>Always on</td>
</tr>
</tbody>
</table></div>
<ul>
<li><strong>No card for the free tier.</strong> A free web service wakes on the next request, and the free services
in a workspace share 750 running hours a month.</li>
<li><strong>Free plan limits.</strong> 5 GB of bandwidth a month and 2 custom domains with automatic HTTPS. The full
list is in <a href="/docs/free-instances/">what the free tier includes</a>.</li>
<li><strong>Paid prices exclude 18% GST.</strong> Every payment gets a GST invoice, paid by UPI or an Indian card,
with no forex markup.</li>
</ul>
<h2 id="a-short-checklist" tabindex="-1"><a class="header-anchor" href="#a-short-checklist">A short checklist</a></h2>
<ol>
<li>The server city is in India.</li>
<li>Deploys come from GitHub, with health checks and rollback.</li>
<li>The app reads <code>PORT</code> and listens on <code>0.0.0.0</code>.</li>
<li>Secrets live in environment variables.</li>
<li>The invoice is in rupees, with GST.</li>
</ol>
<h2 id="host-your-nodejs-app-on-voroa" tabindex="-1"><a class="header-anchor" href="#host-your-nodejs-app-on-voroa">Host your Node.js app on Voroa</a></h2>
<p>Voroa builds your Node.js app from GitHub, runs it in Mumbai and bills you in rupees. See what a
<a href="/web-services/">Node.js web service includes</a>. Building an API? Read
<a href="/blog/backend-api-hosting-india/">backend API hosting in India</a> next.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Python Hosting in India for Flask and FastAPI Apps</title>
      <link>https://getvoroa.com/blog/python-hosting-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/python-hosting-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <category>Python</category>
      <description><![CDATA[A plain guide to Python hosting in India for Flask and FastAPI: the right server, binding the port, pinning packages, secrets, and shipping from GitHub.]]></description>
      <content:encoded><![CDATA[<p>Python hosting in India is often sold as “cPanel with Python support”. That works for a script. It
is awkward for a Flask or FastAPI app that needs its own process, its own packages and a clean way
to ship updates. This post covers what a small Python web app needs to run well in production, and
the mistakes that usually stop it working on the first deploy.</p>
<p><img src="https://media.getvoroa.com/img/8f581a253104c9f4.png" alt="A Flask or FastAPI app running behind a production server on a platform in India" width="700" loading="eager" decoding="async"></p>
<h2 id="pick-the-right-server" tabindex="-1"><a class="header-anchor" href="#pick-the-right-server">Pick the right server</a></h2>
<p>The built-in development servers are not built for production traffic. Use a production server
instead.</p>
<h3 id="flask" tabindex="-1"><a class="header-anchor" href="#flask">Flask</a></h3>
<p>Flask is a WSGI app. Run it with Gunicorn:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">gunicorn app:app --bind 0.0.0.0:$PORT
</code></pre>
</div>
<p>The <a href="https://flask.palletsprojects.com/en/stable/deploying/">Flask deployment docs</a> list other
servers if you prefer one.</p>
<h3 id="fastapi" tabindex="-1"><a class="header-anchor" href="#fastapi">FastAPI</a></h3>
<p>FastAPI is an ASGI app. Run it with Uvicorn:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">uvicorn main:app --host 0.0.0.0 --port $PORT
</code></pre>
</div>
<p>Both read the port from <code>$PORT</code>, which Voroa sets for you. Bind to <code>0.0.0.0</code>, not <code>127.0.0.1</code>.</p>
<h2 id="pin-your-packages" tabindex="-1"><a class="header-anchor" href="#pin-your-packages">Pin your packages</a></h2>
<p>List every package with a version in <code>requirements.txt</code>. An unpinned package can change between two
builds and break an app that worked yesterday. Keep development-only tools out of the production
list.</p>
<h3 id="check-your-python-version" tabindex="-1"><a class="header-anchor" href="#check-your-python-version">Check your Python version</a></h3>
<p>If you need a specific version, say so in the project. The build uses it instead of guessing.</p>
<h2 id="set-the-build-and-start-commands" tabindex="-1"><a class="header-anchor" href="#set-the-build-and-start-commands">Set the build and start commands</a></h2>
<p>Most Python apps need no build command beyond installing packages. The start command is the server
line above. Both are editable later; the
<a href="/docs/build-and-start-commands/">build and start commands guide</a> explains each field.</p>
<h2 id="keep-secrets-out-of-the-code" tabindex="-1"><a class="header-anchor" href="#keep-secrets-out-of-the-code">Keep secrets out of the code</a></h2>
<p>API keys and database URLs belong in environment variables. On Voroa you add them on the service’s
Environment page, and the service redeploys to pick them up. Values are stored encrypted and are not
shown back to you after you save them. See <a href="/docs/environment-variables/">environment variables</a>.</p>
<h2 id="add-a-database" tabindex="-1"><a class="header-anchor" href="#add-a-database">Add a database</a></h2>
<p>For data that must survive a restart, use <a href="/managed-postgresql-india/">managed PostgreSQL</a> and read
its URL from <code>DATABASE_URL</code>. SQLite files on a service’s disk are not a database for production.</p>
<h2 id="ship-updates-from-github" tabindex="-1"><a class="header-anchor" href="#ship-updates-from-github">Ship updates from GitHub</a></h2>
<p>Connect your repository once. Every push builds and starts a new release, and the old one keeps
serving until the new one is ready. If a release is bad, roll back to the previous version. Keep a <code>/health</code> route that returns 200 so a broken
release is caught before it takes traffic.
The setup is in <a href="/docs/deploy-python-django/">our Python deploy guide</a>.</p>
<h2 id="why-a-flask-or-fastapi-app-fails-on-its-first-deploy" tabindex="-1"><a class="header-anchor" href="#why-a-flask-or-fastapi-app-fails-on-its-first-deploy">Why a Flask or FastAPI app fails on its first deploy</a></h2>
<ul>
<li><strong>ModuleNotFoundError:</strong> the package is missing from <code>requirements.txt</code>.</li>
<li><strong>The URL does not answer:</strong> the server is bound to <code>127.0.0.1</code> or a hard-coded port.</li>
<li><strong>Works locally, fails in production:</strong> an environment variable you have locally is not set on
the service.</li>
</ul>
<h2 id="what-it-costs" tabindex="-1"><a class="header-anchor" href="#what-it-costs">What it costs</a></h2>
<p>On Voroa you can start on the free size, which has 512 MB of memory (subject to free capacity). Paid
sizes start at Rs 298 a month plus GST, and you get a GST invoice in rupees.</p>
<h2 id="host-your-python-app-on-voroa" tabindex="-1"><a class="header-anchor" href="#host-your-python-app-on-voroa">Host your Python app on Voroa</a></h2>
<p>For a full Django project, read <a href="/blog/django-hosting-india/">Django hosting in India</a>. For
everything a Python web service includes, see <a href="/web-services/">Python app hosting on Voroa</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Railway Alternative for India: Node.js Hosting, Fixed Prices</title>
      <link>https://getvoroa.com/blog/railway-alternative-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/railway-alternative-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[A Railway alternative for Node.js web apps in India: usage pricing vs fixed sizes, trial credit vs a free size, who backs up Postgres, regions, and how to move.]]></description>
      <content:encoded><![CDATA[<p>When you weigh a Railway alternative, the biggest difference is how you pay. Railway charges for the memory and processor
you use; Voroa charges a fixed monthly price per service size. This post is for Indian developers
running a Node.js web app, an API or a small stack with a database. It covers setup, the bill, the free
option, database backups and regions, using facts from Railway’s own pages.</p>
<h2 id="four-ways-to-start-on-railway-one-on-voroa" tabindex="-1"><a class="header-anchor" href="#four-ways-to-start-on-railway-one-on-voroa">Four Ways to Start on Railway, One on Voroa</a></h2>
<p>Railway’s <a href="https://docs.railway.com/quick-start">quick start guide</a> shows four ways in: from a GitHub
repo, with <code>railway up</code> from the CLI, from a Docker image, or from a template.</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">bash</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-bash">railway up
</code></pre>
</div>
<p>On Voroa, you connect a GitHub repository to a web service. Voroa installs dependencies, builds and
starts your Node.js app, and with auto-deploy on, each push becomes a new release. The new release starts
before the old one stops, and a rollback is one click. The <a href="/docs/deploy-from-github/">GitHub deploy guide</a>
has the screens. If Railway’s CLI and templates are how you like to work, that is a point for Railway.</p>
<h2 id="predicting-a-months-bill-usage-rates-vs-fixed-sizes" tabindex="-1"><a class="header-anchor" href="#predicting-a-months-bill-usage-rates-vs-fixed-sizes">Predicting a Month’s Bill: Usage Rates vs Fixed Sizes</a></h2>
<p>Railway Hobby is $5/month and includes $5 of monthly usage credits. Pro is $20/month per workspace.
Usage costs about $10 per GB of RAM per month and about $20 per vCPU per month. Our
<a href="/blog/railway-pricing-india/">Railway pricing guide</a> covers the free tier and works three bills through to rupees.</p>
<p>So the bill depends on what your app actually uses. A Node.js API that sits mostly idle can cost very
little. By Railway’s listed rates, a service that used a full 1 GB of RAM and 1 vCPU all month would be
about $30 of usage, before anything else is added.</p>
<p>On Voroa, a web service at the Starter size is Rs 298 a month plus GST for 1 GB and 1 vCPU, whatever it
uses inside that. You know the number before the month starts. If your load is spiky and mostly low,
Railway’s usage model may cost less; if it is steady, a fixed size is easier to budget.</p>
<h2 id="railways-trial-credit-vs-a-standing-free-size" tabindex="-1"><a class="header-anchor" href="#railways-trial-credit-vs-a-standing-free-size">Railway’s Trial Credit vs a Standing Free Size</a></h2>
<p>Railway’s trial gives a $5 one-time credit for 30 days, with limits of 1 GB RAM, 2 vCPU and a 0.5 GB
volume. After that, the Free plan gives $1 of free credit per month.</p>
<p>Voroa’s free Nano size is Rs 0 with 512 MB and 0.1 vCPU shared, up to 4 services per workspace, 5 GB
bandwidth and 500 build minutes, subject to free capacity. It is not time-limited today.</p>
<h2 id="unmanaged-database-templates-vs-managed-postgres-backups" tabindex="-1"><a class="header-anchor" href="#unmanaged-database-templates-vs-managed-postgres-backups">Unmanaged Database Templates vs Managed Postgres Backups</a></h2>
<p>Railway’s <a href="https://docs.railway.com/databases">databases page</a> says its database templates are
“unmanaged services”, and that you are responsible for configuring backups and disaster recovery.</p>
<p>A Voroa <a href="/managed-postgresql-india/">managed PostgreSQL database</a> runs in Mumbai. Paid plans include
nightly backups, off by default and turned on with one switch. A
<a href="/managed-redis-india/">key-value store for caching and queues</a> is also available.</p>
<h2 id="singapore-is-railways-closest-region-to-india" tabindex="-1"><a class="header-anchor" href="#singapore-is-railways-closest-region-to-india">Singapore Is Railway’s Closest Region to India</a></h2>
<p>Railway’s <a href="https://docs.railway.com/reference/deployment-regions">deployment regions reference</a> lists US
West (California), US East (Virginia), EU West (Amsterdam) and Southeast Asia (Singapore). There is no
India region. Voroa runs in Mumbai only, so for users outside India, Railway’s spread of regions is the
better fit.</p>
<h2 id="sharing-variables-across-several-services" tabindex="-1"><a class="header-anchor" href="#sharing-variables-across-several-services">Sharing Variables Across Several Services</a></h2>
<p>On Voroa, an <a href="/docs/environment-groups/">environment group</a> holds variables you link to many services,
so a shared value is changed once. Values are encrypted at rest and not shown back after you save them.
When you move from Railway, any variable that points at a Railway database needs the new Voroa value.</p>
<h2 id="railway-vs-voroa-for-a-nodejs-stack" tabindex="-1"><a class="header-anchor" href="#railway-vs-voroa-for-a-nodejs-stack">Railway vs Voroa for a Node.js Stack</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Railway</th>
<th>Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Deploy</td>
<td>GitHub, CLI, Docker image, template</td>
<td>GitHub, build on push when auto-deploy is on</td>
</tr>
<tr>
<td>Pricing model</td>
<td>Usage-based, in USD</td>
<td>Fixed size per service, in INR</td>
</tr>
<tr>
<td>Entry paid</td>
<td>Hobby $5/month incl. $5 usage</td>
<td>Rs 298/month plus GST, 1 GB, 1 vCPU</td>
</tr>
<tr>
<td>Free option</td>
<td>$5 trial credit, then $1 credit a month</td>
<td>Nano size, subject to free capacity</td>
</tr>
<tr>
<td>Database backups</td>
<td>Your job on templates</td>
<td>Nightly on paid plans, one switch</td>
</tr>
<tr>
<td>Nearest region to India</td>
<td>Singapore</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Tax</td>
<td>Sales tax/VAT by location; GST not stated</td>
<td>GST invoice</td>
</tr>
</tbody>
</table></div>
<p>Railway prices were taken from its pricing page on 2026-09-30. Check again before you decide.</p>
<h2 id="is-a-fixed-price-railway-alternative-right-for-your-workload" tabindex="-1"><a class="header-anchor" href="#is-a-fixed-price-railway-alternative-right-for-your-workload">Is a Fixed-Price Railway Alternative Right for Your Workload?</a></h2>
<p>Railway wins for bursty or tiny workloads, for people who like its CLI and templates, and for users in
the US or Europe. Voroa wins for Indian users, for a bill you can predict in rupees with a GST invoice,
and for a database whose backups the platform runs for you.</p>
<h2 id="moving-railway-services-and-postgres" tabindex="-1"><a class="header-anchor" href="#moving-railway-services-and-postgres">Moving Railway Services and Postgres</a></h2>
<p><img src="https://media.getvoroa.com/img/acb2360e4ad8c0c7.png" alt="Four-step flow for moving from Railway: same repo, copy variables, import database, switch domain" width="700" loading="eager" decoding="async"></p>
<ol>
<li>Connect each repository to a new web service or <a href="/background-workers/">background worker</a>.</li>
<li>Copy the variables from each Railway service.</li>
<li><a href="/docs/import-existing-database/">Import the Railway database</a> by pasting its connection string.</li>
<li>Test on the Voroa URL, then switch your domain.</li>
</ol>
<p>For the same comparison with a different platform, see <a href="/blog/fly-io-alternative-india/">our Fly.io alternative guide</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="does-railway-have-a-region-in-india" tabindex="-1"><a class="header-anchor" href="#does-railway-have-a-region-in-india">Does Railway have a region in India?</a></h3>
<p>Not in its deployment regions reference. Singapore is the nearest.</p>
<h3 id="are-railway-databases-backed-up-automatically" tabindex="-1"><a class="header-anchor" href="#are-railway-databases-backed-up-automatically">Are Railway databases backed up automatically?</a></h3>
<p>Railway’s docs say its database templates are unmanaged, and configuring backups is your job.</p>
<h3 id="is-voroa-usage-based" tabindex="-1"><a class="header-anchor" href="#is-voroa-usage-based">Is Voroa usage-based?</a></h3>
<p>No. Each service has a fixed monthly size, and the workspace plan sets pooled limits such as bandwidth
and build minutes.</p>
<h2 id="give-your-nodejs-stack-a-fixed-monthly-price" tabindex="-1"><a class="header-anchor" href="#give-your-nodejs-stack-a-fixed-monthly-price">Give Your Node.js Stack a Fixed Monthly Price</a></h2>
<p>Run your app and database in Mumbai with a rupee bill you can predict. Start free, then pick a size on the
<a href="/pricing/">Voroa pricing page</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Render Alternative for India: Web App and Node.js Hosting</title>
      <link>https://getvoroa.com/blog/render-alternative-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/render-alternative-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[A Render alternative for Node.js web apps in India: deploy flow, free tier spin-down, regions and rupee billing, databases, scaling and when Render is better.]]></description>
      <content:encoded><![CDATA[<p>If you are looking for a Render alternative, this post is for developers in India who deploy a Node.js web app, or any typical
web app, and want to know which platform suits them. The two work in a very similar way, so the
differences are in the details: what happens on the free tier, where the server runs, which currency you
pay in, and how the database is handled. Here is each one, with the facts from Render’s own pages.</p>
<h2 id="the-same-git-push-flow-on-both-platforms" tabindex="-1"><a class="header-anchor" href="#the-same-git-push-flow-on-both-platforms">The Same Git-Push Flow on Both Platforms</a></h2>
<p>Render’s <a href="https://render.com/docs/web-services">web services documentation</a> says it “builds and deploys
your code with every push to your linked Git branch”. It has native runtimes for Node.js, Python, Go,
Rust and more, and it can also deploy a prebuilt Docker image.</p>
<p>Voroa works the same way for a Node.js app. You connect a GitHub repository, pick the branch, and Voroa
installs dependencies, builds and starts the app. With auto-deploy on, each push becomes a new release.
The new release starts before the old one stops, and a rollback to the previous release is one click. The
<a href="/docs/deploy-nodejs/">Node.js deploy guide</a> has the steps. Your server should read the port from the
environment:</p>
<div class="codeblock"><div class="codeblock-head"><span class="codeblock-lang">js</span><button type="button" class="codeblock-copy">Copy</button></div><pre><code class="language-js">const port = process.env.PORT || 3000;
app.listen(port, () =&gt; console.log(`listening on ${port}`));
</code></pre>
</div>
<p>Voroa sets <code>PORT</code>. If your app listens on one other fixed port instead, Voroa detects it.</p>
<h2 id="renders-free-spin-down-and-your-apps-first-request" tabindex="-1"><a class="header-anchor" href="#renders-free-spin-down-and-your-apps-first-request">Render’s Free Spin-Down and Your App’s First Request</a></h2>
<p>Render spins down a free web service that goes 15 minutes without inbound traffic. The next visitor
waits while it wakes up. Each workspace gets 750 free instance hours per calendar month, and free Render
Postgres databases expire 30 days after creation, followed by a 14-day grace period.</p>
<p>On Voroa, the free Nano size is Rs 0 with 512 MB and 0.1 vCPU shared. A free workspace gets up to 4
services, 5 GB bandwidth, 500 build minutes and 2 custom domains, subject to free capacity. Free services
currently stay running between requests. The details are on the
<a href="/docs/free-instances/">free instances page</a>.</p>
<h2 id="oregon-to-singapore-renders-regions-seen-from-india" tabindex="-1"><a class="header-anchor" href="#oregon-to-singapore-renders-regions-seen-from-india">Oregon to Singapore: Render’s Regions Seen From India</a></h2>
<p>Render’s <a href="https://render.com/docs/regions">regions page</a> lists Oregon, Ohio, Virginia, Frankfurt and
Singapore. There is no India region, so requests from Indian users go to Singapore at best.</p>
<p>Voroa runs services and databases in Mumbai. That is our only region. If your users are mostly in the US
or Europe, Render’s regions are closer to them.</p>
<h2 id="paying-render-in-dollars-voroa-in-rupees" tabindex="-1"><a class="header-anchor" href="#paying-render-in-dollars-voroa-in-rupees">Paying Render in Dollars, Voroa in Rupees</a></h2>
<p>Render shows its prices in USD. Its pricing and FAQ pages do not state anything about GST or Indian tax
invoices. Voroa bills in rupees, with a GST invoice, and takes UPI, Indian cards and netbanking. Our
explainer on <a href="/docs/cloud-hosting-inr-vs-usd/">rupee versus dollar hosting bills</a> shows what a dollar bill
really costs once card markup and exchange rates are counted.</p>
<h2 id="render-postgres-and-key-value-compared-with-voroas" tabindex="-1"><a class="header-anchor" href="#render-postgres-and-key-value-compared-with-voroas">Render Postgres and Key Value Compared With Voroa’s</a></h2>
<p>Render Postgres starts at $6/month (256 MB RAM, 100 connections) and Key Value at $10/month (256 MB),
each with a free tier. On Voroa, a
<a href="/managed-postgresql-india/">managed PostgreSQL database in Mumbai</a> starts with a free size, and a
key-value store sits in the same workspace, on the same rupee bill.</p>
<h2 id="scaling-a-nodejs-service-up-or-out" tabindex="-1"><a class="header-anchor" href="#scaling-a-nodejs-service-up-or-out">Scaling a Node.js Service Up or Out</a></h2>
<p>On Render, the entry paid web service is $7/month with 512 MB and less than 1 CPU; the next is $25/month
with 2 GB and 1 CPU. On Voroa, paid web sizes start at Rs 298/month plus GST for 1 GB and 1 vCPU. You can
change a service’s size or run several copies of it from its Compute page. Automatic scaling based on
load is not available on Voroa today. See <a href="/docs/scaling/">scaling up or out</a>.</p>
<h2 id="render-vs-voroa-feature-by-feature" tabindex="-1"><a class="header-anchor" href="#render-vs-voroa-feature-by-feature">Render vs Voroa Feature by Feature</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Render</th>
<th>Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Deploy on Git push</td>
<td>Yes</td>
<td>Yes, when auto-deploy is on</td>
</tr>
<tr>
<td>Node.js support</td>
<td>Native runtime</td>
<td>Built automatically; Node 22, 20 or 18</td>
</tr>
<tr>
<td>Nearest region to India</td>
<td>Singapore</td>
<td>Mumbai</td>
</tr>
<tr>
<td>Free web service</td>
<td>Spins down after 15 minutes idle</td>
<td>Currently stays running, subject to free capacity</td>
</tr>
<tr>
<td>Entry paid web service</td>
<td>$7/month, 512 MB</td>
<td>Rs 298/month plus GST, 1 GB</td>
</tr>
<tr>
<td>Billing</td>
<td>USD; GST not stated</td>
<td>INR with a GST invoice</td>
</tr>
<tr>
<td>Managed Postgres</td>
<td>From $6/month, free tier</td>
<td>Free size to start</td>
</tr>
</tbody>
</table></div>
<p>Render prices were taken from its pricing page on 2026-09-30. Prices change, so check both before you
decide.</p>
<h2 id="where-each-platform-wins" tabindex="-1"><a class="header-anchor" href="#where-each-platform-wins">Where Each Platform Wins</a></h2>
<p>Render wins if most of your users are in the US or Europe, if you need more than one region, or if you
deploy prebuilt images from a registry. Its Pro workspace plan is $25/month, and Hobby is $0.</p>
<p>Voroa wins if your users are in India, if you want a free service that does not spin down today, or if
your business needs rupee billing and a GST invoice. If Netlify is on your list too, see
<a href="/blog/netlify-alternative-india/">a Netlify alternative for India</a>.</p>
<h2 id="moving-to-a-render-alternative-without-changing-your-code" tabindex="-1"><a class="header-anchor" href="#moving-to-a-render-alternative-without-changing-your-code">Moving to a Render Alternative Without Changing Your Code</a></h2>
<p><img src="https://media.getvoroa.com/img/7c017798a5c4e37d.png" alt="Four-step flow for moving from Render: same repo, copy env vars, import database, switch domain" width="700" loading="eager" decoding="async"></p>
<ol>
<li>Connect the same GitHub repository to a new <a href="/web-services/">Voroa web service</a>.</li>
<li>Copy your environment variables. Voroa encrypts them at rest and does not show them back.</li>
<li><a href="/docs/import-existing-database/">Import your Render database</a> by pasting its connection string. Do it
well before a free database’s 30-day expiry.</li>
<li>Test on the Voroa URL, then switch your domain.</li>
</ol>
<p>Comparing more platforms? Read <a href="/blog/railway-alternative-india/">our Railway alternative guide</a> or our
<a href="/blog/node-js-hosting-india/">Node.js hosting guide for India</a>.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="does-render-have-a-server-in-india" tabindex="-1"><a class="header-anchor" href="#does-render-have-a-server-in-india">Does Render have a server in India?</a></h3>
<p>Not according to its regions page. Singapore is the nearest.</p>
<h3 id="why-is-my-free-render-app-slow-on-the-first-visit" tabindex="-1"><a class="header-anchor" href="#why-is-my-free-render-app-slow-on-the-first-visit">Why is my free Render app slow on the first visit?</a></h3>
<p>Render spins down a free web service after 15 minutes without inbound traffic, so the first request after
that waits for it to start.</p>
<h3 id="can-i-use-my-own-dockerfile-on-voroa" tabindex="-1"><a class="header-anchor" href="#can-i-use-my-own-dockerfile-on-voroa">Can I use my own Dockerfile on Voroa?</a></h3>
<p>Yes. Choose Docker as the runtime and Voroa builds from the Dockerfile in your repository.</p>
<h2 id="start-a-nodejs-service-in-mumbai" tabindex="-1"><a class="header-anchor" href="#start-a-nodejs-service-in-mumbai">Start a Node.js Service in Mumbai</a></h2>
<p>Connect a repository and your app runs in Mumbai with a rupee bill and a GST invoice. Start on the free
size, then compare sizes on the <a href="/pricing/">Voroa pricing page</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Vercel Alternative for India: Node.js and Full-Stack Hosting</title>
      <link>https://getvoroa.com/blog/vercel-alternative-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/vercel-alternative-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Migration</category>
      <description><![CDATA[A Vercel alternative for Node.js and full-stack apps in India: functions vs a long-running service, Hobby plan rules, regions, rupee billing and databases.]]></description>
      <content:encoded><![CDATA[<p>A Vercel alternative is not a like-for-like swap. Vercel is built around the frontend and serverless
functions; Voroa runs your Node.js app as a long-running web service, next to its database and workers.
This post is for Indian developers with a Next.js, Express or full-stack app who want to know where each
part of their stack fits best. Every Vercel fact here comes from Vercel’s own docs.</p>
<h2 id="where-a-nodejs-backend-fits-on-vercel" tabindex="-1"><a class="header-anchor" href="#where-a-nodejs-backend-fits-on-vercel">Where a Node.js Backend Fits on Vercel</a></h2>
<p>On Vercel, backend code runs as functions. Vercel has a Mumbai region (<code>bom1</code>) among 19 compute regions,
but the <a href="https://vercel.com/docs/regions">Vercel regions page</a> says functions run in <code>iad1</code> (Washington,
D.C.) unless you change the region. If your API talks to a database in India, change that setting first.</p>
<p>The free Hobby plan limits include 4 CPU-hours of Active CPU and 1,000,000 function invocations. If you
exceed them, you wait until 30 days have passed.</p>
<h2 id="a-long-running-nodejs-service-on-voroa" tabindex="-1"><a class="header-anchor" href="#a-long-running-nodejs-service-on-voroa">A Long-Running Node.js Service on Voroa</a></h2>
<p>On Voroa, a Node.js app is a process that keeps running: you connect a GitHub repository, set a <code>start</code>
script, and Voroa builds and runs it in Mumbai. Voroa sets <code>PORT</code>, so a server that reads
<code>process.env.PORT</code> works as it is. The <a href="/docs/deploy-nodejs/">Node.js deploy guide</a> covers the build and
start settings, and a <a href="/web-services/">Voroa web service</a> page lists what each size includes.</p>
<h2 id="preview-deployments-and-the-push-to-deploy-loop" tabindex="-1"><a class="header-anchor" href="#preview-deployments-and-the-push-to-deploy-loop">Preview Deployments and the Push-to-Deploy Loop</a></h2>
<p>Vercel’s <a href="https://vercel.com/docs/git">Git documentation</a> says it deploys automatically on every branch
push and on merges to the production branch, from GitHub, GitLab, Bitbucket or Azure DevOps, and every
push gets a preview deployment. For reviewing frontend changes, this is Vercel’s strongest point.</p>
<p>Voroa deploys from GitHub. With auto-deploy on, each push to the branch becomes a new release, the new
release starts before the old one stops, and you can roll back in one click.</p>
<h2 id="the-hobby-plans-non-commercial-rule" tabindex="-1"><a class="header-anchor" href="#the-hobby-plans-non-commercial-rule">The Hobby Plan’s Non-Commercial Rule</a></h2>
<p>Vercel Hobby “restricts users to non-commercial, personal use only”. It also allows 100 GB Fast Data
Transfer and 100 deployments per day, and it cannot deploy from a private repository owned by a GitHub
organization.</p>
<p>Voroa’s free Nano size is Rs 0 with 512 MB and 0.1 vCPU shared, up to 4 services per workspace, 5 GB
bandwidth and 500 build minutes, subject to free capacity. See
<a href="/docs/free-instances/">what the free tier includes</a>.</p>
<h2 id="vercel-pro-seats-and-gst-vs-a-rupee-invoice" tabindex="-1"><a class="header-anchor" href="#vercel-pro-seats-and-gst-vs-a-rupee-invoice">Vercel Pro Seats and GST vs a Rupee Invoice</a></h2>
<p>Vercel Pro is $20/month, and developer seats are $20 per user per month. Our
<a href="/blog/vercel-pricing-india/">Vercel pricing guide</a> works three bills through to rupees. Vercel’s
<a href="https://vercel.com/docs/pricing/taxes">tax page</a> says prices are in USD and exclude GST, and that it began
collecting VAT, GST or equivalent from international customers on 1 April 2026. It does not mention a
GSTIN field or rupee invoices. For the tax side of a foreign bill, read
<a href="/docs/reverse-charge-foreign-cloud-hosting/">reverse charge on foreign hosting</a>.</p>
<p>Voroa bills in rupees with a GST invoice, and paid web sizes start at Rs 298/month plus GST.</p>
<h2 id="databases-marketplace-integrations-vs-postgres-in-mumbai" tabindex="-1"><a class="header-anchor" href="#databases-marketplace-integrations-vs-postgres-in-mumbai">Databases: Marketplace Integrations vs Postgres in Mumbai</a></h2>
<p><img src="https://media.getvoroa.com/img/e7a3fbfed4b53e65.png" alt="A full-stack app on Voroa: a static site, an API web service, a PostgreSQL database and a background worker" width="700" loading="eager" decoding="async"></p>
<p>Vercel’s plan comparison lists Blob under storage. For Postgres or Redis, its pricing page points to
Marketplace Integrations. On Voroa, a
<a href="/managed-postgresql-india/">managed PostgreSQL database</a>, a key-value store,
<a href="/background-workers/">background workers</a> and static sites live in one workspace with your API.</p>
<h2 id="vercel-vs-voroa-for-a-nextjs-or-express-app" tabindex="-1"><a class="header-anchor" href="#vercel-vs-voroa-for-a-nextjs-or-express-app">Vercel vs Voroa for a Next.js or Express App</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Vercel</th>
<th>Voroa</th>
</tr>
</thead>
<tbody>
<tr>
<td>Backend model</td>
<td>Functions</td>
<td>Long-running web service</td>
</tr>
<tr>
<td>Deploy on push</td>
<td>Yes, with a preview per push</td>
<td>Yes, when auto-deploy is on</td>
</tr>
<tr>
<td>India region</td>
<td>Mumbai; functions default to Washington, D.C.</td>
<td>Mumbai, default and only</td>
</tr>
<tr>
<td>Free plan</td>
<td>Hobby, non-commercial only</td>
<td>Nano size, subject to free capacity</td>
</tr>
<tr>
<td>Entry paid</td>
<td>Pro $20/month plus $20 per seat</td>
<td>Rs 298/month plus GST per service</td>
</tr>
<tr>
<td>Billing</td>
<td>USD, GST added from April 2026</td>
<td>INR with a GST invoice</td>
</tr>
<tr>
<td>Postgres</td>
<td>Marketplace Integrations</td>
<td>First-party, managed</td>
</tr>
</tbody>
</table></div>
<p>Vercel prices were taken from its pricing pages on 2026-09-30. Check again before you decide.</p>
<h2 id="splitting-the-stack-frontend-on-vercel-backend-in-mumbai" tabindex="-1"><a class="header-anchor" href="#splitting-the-stack-frontend-on-vercel-backend-in-mumbai">Splitting the Stack: Frontend on Vercel, Backend in Mumbai</a></h2>
<p>You do not have to pick one. Vercel wins for a frontend-heavy app, preview deployments and a global
audience. Voroa wins for the parts that must run all the time near Indian users: the API, the worker and
the database. Many teams keep the frontend on Vercel and point it at an API on Voroa. Still choosing the frontend host? Read
<a href="/blog/netlify-vs-vercel-india/">Netlify vs Vercel for Indian teams</a>.</p>
<h2 id="moving-a-nextjs-app-to-a-vercel-alternative" tabindex="-1"><a class="header-anchor" href="#moving-a-nextjs-app-to-a-vercel-alternative">Moving a Next.js App to a Vercel Alternative</a></h2>
<ol>
<li>Check that the app runs with <code>next build</code> and <code>next start</code>. The
<a href="/docs/deploy-nextjs/">Next.js guide</a> lists the settings.</li>
<li>Connect the repository to a new web service.</li>
<li>Copy the environment variables from Vercel.</li>
<li>Test on the Voroa URL, then move the domain.</li>
</ol>
<p>Our <a href="/blog/nextjs-hosting-india/">Next.js hosting guide for India</a> goes deeper, and
<a href="/blog/render-alternative-india/">our Render alternative guide</a> compares another backend platform.</p>
<h2 id="faq" tabindex="-1"><a class="header-anchor" href="#faq">FAQ</a></h2>
<h3 id="can-i-use-vercels-free-plan-for-a-business" tabindex="-1"><a class="header-anchor" href="#can-i-use-vercels-free-plan-for-a-business">Can I use Vercel’s free plan for a business?</a></h3>
<p>Vercel’s Hobby page says the plan is for non-commercial, personal use only. Its paid entry plan is Pro, at
$20/month plus $20 per developer seat.</p>
<h3 id="does-vercel-have-servers-in-india" tabindex="-1"><a class="header-anchor" href="#does-vercel-have-servers-in-india">Does Vercel have servers in India?</a></h3>
<p>Yes, Mumbai (<code>bom1</code>). Functions default to Washington, D.C., so change the function region yourself.</p>
<h3 id="can-a-websocket-server-run-on-voroa" tabindex="-1"><a class="header-anchor" href="#can-a-websocket-server-run-on-voroa">Can a WebSocket server run on Voroa?</a></h3>
<p>Yes. Voroa services <a href="/docs/websockets/">support WebSockets and Socket.IO</a> over secure <code>wss://</code>, because a
web service is a long-running process rather than a function.</p>
<h2 id="host-your-nodejs-api-next-to-your-users" tabindex="-1"><a class="header-anchor" href="#host-your-nodejs-api-next-to-your-users">Host Your Node.js API Next to Your Users</a></h2>
<p>Run your API, workers and database in Mumbai on one rupee bill with a GST invoice. Start free, then see
sizes on the <a href="/pricing/">Voroa pricing page</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Shared Hosting vs VPS vs a Deploy Platform for Web Apps in India</title>
      <link>https://getvoroa.com/blog/web-app-hosting-india/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/web-app-hosting-india/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Hosting</category>
      <category>India</category>
      <description><![CDATA[Shared hosting vs VPS vs a deploy platform: a plain comparison for web apps in India by setup work, deploys, scaling, security and how you are billed.]]></description>
      <content:encoded><![CDATA[<p>Shared hosting vs VPS is the first choice most developers in India face, and a deploy platform is the third option. All
three can run your app. They differ in how much of the work is yours - setup, deploys, security
updates, scaling and the bill. This post compares them in plain terms, so you can pick the one that
fits your project today and know when to switch.</p>
<p><img src="https://media.getvoroa.com/img/4b360ee9770598f8.png" alt="Three hosting options compared by how much of the setup you manage yourself" width="700" loading="eager" decoding="async"></p>
<h2 id="shared-hosting" tabindex="-1"><a class="header-anchor" href="#shared-hosting">Shared hosting</a></h2>
<p>Many websites share one server and one control panel.</p>
<h3 id="good-for" tabindex="-1"><a class="header-anchor" href="#good-for">Good for</a></h3>
<p>A WordPress site or a PHP app with light traffic.</p>
<h3 id="hard-for" tabindex="-1"><a class="header-anchor" href="#hard-for">Hard for</a></h3>
<p>A Node.js or Python app that needs its own long-running process, its own packages, and a clean way
to ship updates. Background jobs and custom ports are usually off the table.</p>
<h2 id="a-vps" tabindex="-1"><a class="header-anchor" href="#a-vps">A VPS</a></h2>
<p>A virtual machine that is yours: you get root access and install what you like.</p>
<h3 id="good-for-1" tabindex="-1"><a class="header-anchor" href="#good-for-1">Good for</a></h3>
<p>Full control. You pick the OS, the web server and the database, and run anything.</p>
<h3 id="hard-for-1" tabindex="-1"><a class="header-anchor" href="#hard-for-1">Hard for</a></h3>
<p>Everything after setup is yours: security patches, restarts, HTTPS certificates, backups, logs and
monitoring. A VPS that nobody updates is a security risk, and a disk that fills up at night takes the
app down with it.</p>
<h2 id="a-deploy-platform" tabindex="-1"><a class="header-anchor" href="#a-deploy-platform">A deploy platform</a></h2>
<p>You push code from Git; the platform builds, runs and serves it.</p>
<h3 id="good-for-2" tabindex="-1"><a class="header-anchor" href="#good-for-2">Good for</a></h3>
<p>Apps in a Git repository. You connect GitHub once, and every push becomes a new release. See
<a href="/blog/deploy-from-github-india/">how push-to-deploy works</a>.</p>
<h3 id="hard-for-2" tabindex="-1"><a class="header-anchor" href="#hard-for-2">Hard for</a></h3>
<p>Very unusual setups. If you need a custom kernel or full root access, a VPS fits better.</p>
<h3 id="what-voroa-handles" tabindex="-1"><a class="header-anchor" href="#what-voroa-handles">What Voroa handles</a></h3>
<p>On Voroa, a web service gets a URL with HTTPS, a new release on every push, rollback to the previous version,
<a href="/docs/custom-domains/">custom domains</a>, logs, and environment variables. It runs in Mumbai.</p>
<h2 id="side-by-side" tabindex="-1"><a class="header-anchor" href="#side-by-side">Side by side</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th></th>
<th>Shared hosting</th>
<th>VPS</th>
<th>Deploy platform</th>
</tr>
</thead>
<tbody>
<tr>
<td>Setup work</td>
<td>Low</td>
<td>High</td>
<td>Low</td>
</tr>
<tr>
<td>Runs Node.js / Python servers</td>
<td>Limited</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>New version</td>
<td>Upload files</td>
<td>You script it</td>
<td>Every git push</td>
</tr>
<tr>
<td>HTTPS</td>
<td>Usually included</td>
<td>You set it up</td>
<td>Included</td>
</tr>
<tr>
<td>OS updates</td>
<td>Host</td>
<td>You</td>
<td>Platform</td>
</tr>
<tr>
<td>Scaling</td>
<td>Change plan</td>
<td>Resize or add machines</td>
<td>Bigger size or more copies</td>
</tr>
</tbody>
</table></div>
<h2 id="how-the-bill-differs" tabindex="-1"><a class="header-anchor" href="#how-the-bill-differs">How the bill differs</a></h2>
<p>Shared hosting and VPS plans are often cheapest per month. A platform saves your time. On Voroa you
can start free (subject to free capacity), and paid sizes start at Rs 298 a month plus GST, billed
in rupees with a GST invoice. The official
<a href="https://www.gst.gov.in/">GST portal</a> is where a business registers to claim input tax credit.</p>
<h2 id="so-which-one" tabindex="-1"><a class="header-anchor" href="#so-which-one">So which one?</a></h2>
<ul>
<li><strong>A blog or a brochure site:</strong> shared hosting is fine. If it is plain HTML, a
<a href="/static-sites/">static site</a> is free on Voroa.</li>
<li><strong>An app you want to control down to the OS:</strong> a VPS.</li>
<li><strong>An app in GitHub that you want live without running servers:</strong> a platform.</li>
</ul>
<h2 id="when-to-switch" tabindex="-1"><a class="header-anchor" href="#when-to-switch">When to switch</a></h2>
<p>Move off shared hosting when your app needs its own process. Move off a VPS when upkeep takes more
time than building. Move to a VPS when a platform genuinely cannot run what you need.</p>
<h2 id="try-a-platform-on-voroa" tabindex="-1"><a class="header-anchor" href="#try-a-platform-on-voroa">Try a platform on Voroa</a></h2>
<p>New to the platform idea? <a href="/docs/what-is-voroa/">What is Voroa</a> explains it in two minutes. To try
free first, read <a href="/blog/free-web-app-hosting-india/">free web app hosting in India</a>. For sizes and
prices, see <a href="/web-services/">web app hosting on Voroa</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>UPI AutoPay Limit Explained: Rs 15,000, Rs 1 Lakh and Mandates</title>
      <link>https://getvoroa.com/blog/recurring-payments-india-mandate-ceilings/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/recurring-payments-india-mandate-ceilings/</guid>
      <pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>India</category>
      <category>Payments</category>
      <description><![CDATA[UPI AutoPay limit explained: debits up to Rs 15,000 run without your PIN, bigger ones need approval. What 'auto paid' means, Rs 1 lakh cases and daily rules.]]></description>
      <content:encoded><![CDATA[<p>The UPI AutoPay limit is Rs 15,000 per debit for most payments: up to that amount, a recurring payment
runs on its own after you approve the mandate once. Above it, you approve each debit with your UPI PIN.
This guide explains that rule as the payer sees it, then the mandate ceilings a business has to design
around. The rules come from RBI’s e-mandate framework, checked on 14 November 2026, plus what we
measured on Voroa’s own live billing.</p>
<h2 id="what-does-amount-up-to-rs-15000-will-be-auto-paid-mean" tabindex="-1"><a class="header-anchor" href="#what-does-amount-up-to-rs-15000-will-be-auto-paid-mean">What does “amount up to Rs 15,000 will be auto paid” mean?</a></h2>
<p>It means each debit of Rs 15,000 or less will go through automatically, without asking
for your UPI PIN again. It does not mean Rs 15,000 will be taken. The merchant debits only the amount due,
and RBI requires them to tell you at least 24 hours before each debit. A debit above Rs 15,000 waits for
your approval.</p>
<p>The same answer covers “setting an AutoPay of Rs 15,000 means”. You are setting a <strong>maximum</strong>, not a
charge. A Rs 499 monthly plan on a Rs 15,000 mandate still costs Rs 499 a month.</p>
<p>Google Pay’s help page says the same thing from the app’s side: if the mandate amount is less than
Rs 15,000, you enter your UPI PIN only for the first payment; if it is more, you enter your PIN each time
a payment is due.</p>
<h2 id="what-is-the-upi-autopay-limit-in-2026" tabindex="-1"><a class="header-anchor" href="#what-is-the-upi-autopay-limit-in-2026">What is the UPI AutoPay limit in 2026?</a></h2>
<p>RBI’s <strong>Digital Payments - E-mandate Framework, 2026</strong>, issued on 21 April 2026, sets two numbers:</p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Payment type</th>
<th>Runs without fresh approval up to</th>
<th>Above that</th>
</tr>
</thead>
<tbody>
<tr>
<td>Most recurring payments: subscriptions, OTT, SaaS, utilities, loan EMIs</td>
<td>Rs 15,000 per debit</td>
<td>You approve each debit (UPI PIN or OTP)</td>
</tr>
<tr>
<td>Insurance premiums, mutual fund subscriptions, credit card bill payments</td>
<td>Rs 1,00,000 per debit</td>
<td>You approve each debit</td>
</tr>
</tbody>
</table></div>
<p>The framework says “All recurring transactions may be authorised without AFA up to ₹15,000/- per
transaction”. AFA means an additional factor of authentication, which on UPI is your PIN. The first
debit under a new mandate always needs your approval, whatever the amount.</p>
<p>The history explains why so many pages show different numbers. RBI raised the figure from Rs 5,000 to
Rs 15,000 in June 2022, added the Rs 1,00,000 categories in December 2023, and folded both into the 2026
framework.</p>
<h2 id="how-many-autopays-can-you-set-and-how-many-run-in-a-day" tabindex="-1"><a class="header-anchor" href="#how-many-autopays-can-you-set-and-how-many-run-in-a-day">How many AutoPays can you set, and how many run in a day?</a></h2>
<p>We could not find a fixed number in RBI’s 2026 framework or in any NPCI page we could read. The
framework’s section on transaction limits gives the Rs 15,000 and Rs 1,00,000 amounts, but no count of
mandates per person and no count of AutoPay debits per day. So you can have several AutoPays at once, one
per merchant or service.</p>
<p>What does limit a day’s debits:</p>
<ul>
<li><strong>Your bank’s daily UPI limits.</strong> Each bank sets how much, and how many UPI payments, an account can
make in a day. Check your bank’s app for your own numbers.</li>
<li><strong>Each mandate’s own maximum.</strong> A debit above the amount you approved is refused.</li>
<li><strong>Retry rules.</strong> Under NPCI rules reported from August 2025, a failed debit gets at most one original
attempt plus three retries, run outside peak hours.</li>
</ul>
<p>If an app or website quotes a fixed daily count of AutoPays, ask which rule it comes from. We did not
find one in the official texts.</p>
<h2 id="why-a-business-sees-more-than-one-upi-autopay-limit" tabindex="-1"><a class="header-anchor" href="#why-a-business-sees-more-than-one-upi-autopay-limit">Why a business sees more than one “UPI AutoPay limit”</a></h2>
<p>If you sell a subscription in India, the payment rail decides a large part of how much you can charge a
customer every month. Each recurring method carries a ceiling on the mandate amount, the ceilings are far
apart, and you cannot negotiate any of them.</p>
<p><strong>Short answer:</strong> for a business, “the UPI AutoPay limit” is three different numbers. Rs 15,000 is the
RBI threshold above which the customer must approve each debit. The mandate ceiling is higher: our gateway
accepted UPI mandates up to Rs 1,00,000 for our merchant category. UPI Reserve Pay, a different product,
has a far lower cap.</p>
<p>Most guides quote one of these numbers and call it “the limit”. We did too, in the first version of this
post, and we were wrong. Here is what each number actually means.</p>
<h2 id="what-a-mandate-actually-is" tabindex="-1"><a class="header-anchor" href="#what-a-mandate-actually-is">What a mandate actually is</a></h2>
<p>A recurring charge in India is two separate things. The <strong>mandate</strong> is the customer’s standing
permission, registered once, that names a maximum amount. The <strong>debit</strong> is each individual charge against
it. A debit is allowed only if it fits within the mandate’s maximum.</p>
<p>That word <em>maximum</em> trips people up. You are not registering “Rs 1,999 a month”. You are registering a
ceiling that every future debit must fit under. Get the ceiling wrong at registration and the failure does
not appear until month four, when the customer adds a service and the debit finally exceeds what they once
authorised.</p>
<p>Mandates come in two shapes. A <strong>fixed</strong> mandate debits the same amount every cycle, which suits a flat
subscription. A <strong>variable</strong> mandate lets each debit change, up to the maximum, which is what usage-based
or multi-service billing needs. Either way, the maximum is the number that matters.</p>
<h2 id="the-three-numbers-people-call-the-upi-autopay-limit" tabindex="-1"><a class="header-anchor" href="#the-three-numbers-people-call-the-upi-autopay-limit">The three numbers people call “the UPI AutoPay limit”</a></h2>
<p><img src="https://media.getvoroa.com/img/2a6d58b8251941ef.png" alt="Bar chart of three UPI limits: Rs 1,00,000 mandate ceiling, Rs 15,000 RBI approval threshold, and a far lower Reserve Pay block" width="700" loading="eager" decoding="async"></p>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Number</th>
<th>What it limits</th>
<th>Who sets it</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Rs 15,000</strong></td>
<td>The largest recurring debit that can run without the customer approving it again (no additional factor of authentication)</td>
<td>RBI, for cards, prepaid instruments and UPI</td>
</tr>
<tr>
<td><strong>Rs 1,00,000</strong></td>
<td>The largest UPI AutoPay mandate our gateway accepted for our merchant category</td>
<td>NPCI and the gateway, varying by merchant category</td>
</tr>
<tr>
<td><strong>Reserve Pay block limit</strong></td>
<td>How much a UPI Reserve Pay mandate can block in the payer’s account</td>
<td>NPCI and issuer banks; far below the AutoPay ceiling</td>
</tr>
</tbody>
</table></div>
<h3 id="rs-15000-the-authentication-threshold-not-the-mandate-ceiling" tabindex="-1"><a class="header-anchor" href="#rs-15000-the-authentication-threshold-not-the-mandate-ceiling">Rs 15,000: the authentication threshold, not the mandate ceiling</a></h3>
<p>RBI’s e-mandate framework lets a registered mandate run recurring debits without fresh customer
authentication only up to a threshold. RBI raised it from Rs 5,000 to Rs 15,000 in June 2022. In December
2023 it raised the figure to Rs 1,00,000 for three categories only: mutual fund subscriptions, insurance
premiums and credit card bill payments. The April 2026 framework keeps both numbers. A SaaS subscription
is not one of those categories.</p>
<p>Above the threshold, the mandate still works, but each debit needs the customer to approve it. For a
subscription business, that means a renewal above Rs 15,000 is no longer truly automatic.</p>
<h3 id="rs-100000-the-mandate-ceiling-we-measured" tabindex="-1"><a class="header-anchor" href="#rs-100000-the-mandate-ceiling-we-measured">Rs 1,00,000: the mandate ceiling we measured</a></h3>
<p>The mandate ceiling is a separate number: the maximum the customer authorises when they set up the
mandate, which every later debit must fit under. <a href="/docs/upi-autopay-cloud-hosting/">Voroa sizes that maximum to your
bill</a>, not to the rail’s limit. In September 2026 we registered UPI
AutoPay mandates against our live account: Rs 1,00,000 was accepted, and Rs 2,00,000 was refused with an
error saying the amount “cannot be greater than Rs. 100000.00 for this merchant category code”.</p>
<p>Note the last four words. The ceiling depends on your merchant category code (MCC), so a business in
another category can see a different number. Treat ours as one measurement, not a universal rule. RBI’s
2026 framework does not print a single mandate maximum either; it refers to “the overall cap fixed by the
RBI” for variable mandates without stating it on that page.</p>
<h3 id="upi-reserve-pay-the-trap-we-fell-into" tabindex="-1"><a class="header-anchor" href="#upi-reserve-pay-the-trap-we-fell-into">UPI Reserve Pay: the trap we fell into</a></h3>
<p>UPI Reserve Pay (NPCI calls it Single Block Multiple Debit, or SBMD) looks similar but behaves differently.
Instead of permission to debit later, it <strong>blocks</strong> money in the payer’s account up front, and its cap is
far lower than the AutoPay ceiling.</p>
<p>Our first integration requested Reserve Pay by accident: one token-type field in the order told the gateway
to create an SBMD mandate instead of an AutoPay one. The gateway reported a Rs 15,000 SBMD maximum, the
payer’s app showed “UPI Reserve Pay” on the approval screen, and setup failed. We read that error as “UPI
AutoPay is capped at Rs 15,000” and published it here. The fix was to stop sending that field.</p>
<p>If your gateway quotes a UPI ceiling in error text, check which product the order actually requested
before you design around the number.</p>
<h2 id="other-e-mandate-rules-that-affect-renewals" tabindex="-1"><a class="header-anchor" href="#other-e-mandate-rules-that-affect-renewals">Other e-mandate rules that affect renewals</a></h2>
<p>The amount limits are not the only RBI rules that shape a recurring charge. Four more decide whether a
renewal goes through on time:</p>
<ul>
<li><strong>Registration needs authentication.</strong> The customer approves the mandate once, with their UPI PIN or a
card OTP. Changing or cancelling it needs authentication too.</li>
<li><strong>The first debit needs authentication.</strong> RBI’s 2026 framework says the first transaction under an
e-mandate requires it, even below Rs 15,000. It can be combined with registration when both happen
together.</li>
<li><strong>Every debit needs a pre-debit notification.</strong> The customer must be told about each debit at least
24 hours before it runs, so your billing schedule has to send the notice a day ahead.</li>
<li><strong>The customer can cancel at any time.</strong> They can withdraw the mandate, or opt out of a single debit,
from their UPI app or card issuer, so a registered mandate never guarantees payment.</li>
</ul>
<h2 id="upi-card-and-enach-ceilings-voroa-measured" tabindex="-1"><a class="header-anchor" href="#upi-card-and-enach-ceilings-voroa-measured">UPI, card and eNACH ceilings Voroa measured</a></h2>
<div class="table-scroll"><table>
<thead>
<tr>
<th>Method</th>
<th>Mandate ceiling</th>
<th>How we know</th>
<th>Customer setup</th>
<th>Practical use</th>
</tr>
</thead>
<tbody>
<tr>
<td>UPI AutoPay</td>
<td>Rs 1,00,000 for our MCC</td>
<td>Measured on our live account, September 2026</td>
<td>UPI PIN in their app, active at once</td>
<td>Consumer and small-team plans</td>
</tr>
<tr>
<td>Card e-mandate</td>
<td>Rs 2,00,000</td>
<td>Measured in our gateway’s test mode, August 2026</td>
<td>Card OTP at checkout</td>
<td>Mid-market, where a card is acceptable</td>
</tr>
<tr>
<td>eNACH (bank e-mandate)</td>
<td>Up to Rs 1,00,00,000</td>
<td>Gateway documentation only; not verified end to end by us</td>
<td>Net banking or debit card; the bank can take days to activate it</td>
<td>Enterprise and annual contracts</td>
</tr>
</tbody>
</table></div>
<p>The Rs 15,000 authentication threshold applies on top of the card and UPI ceilings. A card mandate can be
registered for Rs 2,00,000, yet a Rs 20,000 debit against it still needs the customer to approve it. We
have not yet run a debit above Rs 15,000 ourselves, so we describe that rule from RBI’s text, not from our
own measurement.</p>
<p>The regulator and the networks revise these numbers, and your category can change which one applies.
Confirm the current figure with your own gateway before you build around it.</p>
<h2 id="example-a-voroa-bill-that-outgrows-its-mandate" tabindex="-1"><a class="header-anchor" href="#example-a-voroa-bill-that-outgrows-its-mandate">Example: a Voroa bill that outgrows its mandate</a></h2>
<p>The ceiling applies to the <strong>consolidated</strong> debit, not to one line item. If a customer runs four services
and you bill them on one mandate, what matters is the total, with tax.</p>
<p>Work an example. A plan at Rs 1,999 plus 18 percent GST is Rs 2,359 a month: inside every ceiling and below
the authentication threshold, so it renews silently. A plan at Rs 29,999 plus GST is Rs 35,399. It fits a
UPI mandate on our category, but it is above Rs 15,000, so every renewal waits for the customer to approve
it. A bill of Rs 1,50,000 a month cannot go on a UPI mandate at all on our category, however the customer
feels about it.</p>
<p>So the method is not a preference you collect at signup and forget. It is a function of what the customer
will eventually owe. Two design consequences follow:</p>
<ol>
<li><strong>Check the cap against the projected debit, not the first one.</strong> The first order is almost always the
smallest. If you validate only at registration, you have validated the easiest case and shipped a bug
that fires months later.</li>
<li><strong>Re-check on every change that moves the bill.</strong> Adding a service, changing a plan or a tax change can
push a consolidated debit past a ceiling, or past the authentication threshold, that was fine when the
mandate was registered.</li>
</ol>
<h2 id="how-to-avoid-failed-upi-autopay-renewals" tabindex="-1"><a class="header-anchor" href="#how-to-avoid-failed-upi-autopay-renewals">How to avoid failed UPI AutoPay renewals</a></h2>
<p>The worst version of this bug is the quiet one: the mandate registers, months pass, the debit is
attempted, the rail refuses it, and the first person to find out is a customer whose service was suspended
for a payment they never had a chance to make.</p>
<p>The fix is unglamorous. Compute the projected consolidated debit <strong>before</strong> registering a mandate, compare
it to that method’s ceiling, and if it does not fit, say so at the point of choice, while the customer is
still on the page. “This plan needs a card or bank mandate” is a fine sentence to show someone. A suspended
service four months later is not.</p>
<p>For debits above the authentication threshold, plan for the approval step: send the pre-debit notice
early, and give the customer a <a href="/docs/renewal-payment-link/">payment link as a fallback</a> if they miss the
approval.</p>
<h3 id="what-happens-when-a-upi-autopay-debit-fails" tabindex="-1"><a class="header-anchor" href="#what-happens-when-a-upi-autopay-debit-fails">What happens when a UPI AutoPay debit fails</a></h3>
<p>A debit can still fail inside every limit: low balance, a revoked mandate, or a bank decline. Under NPCI
rules in force since August 2025, as reported by banks and legal publishers:</p>
<ul>
<li><strong>Four attempts at most.</strong> One original debit plus three retries; after that, the debit is dropped.</li>
<li><strong>Off-peak windows only.</strong> Recurring debits may run before 10:00 AM, between 1:00 PM and 5:00 PM, or
after 9:30 PM, not in the busy hours between.</li>
</ul>
<p>So retries are a scarce resource, not a loop you can run until the money arrives. Schedule them in allowed
windows, spread them over days rather than minutes, and switch the customer to a payment link before the
last attempt is spent.</p>
<h2 id="how-voroa-enforces-mandate-limits-in-code" tabindex="-1"><a class="header-anchor" href="#how-voroa-enforces-mandate-limits-in-code">How Voroa enforces mandate limits in code</a></h2>
<p>Every mandate rule lives in one module, and every caller reads its ceiling from there. That sounds like
ordinary hygiene, and it is, but it exists because of a specific failure: the cap was once written in
three places, one was edited, and the other two kept the old value. The rule that replaced it is simple:
one home for the number, and nobody gets a local copy.</p>
<p>The ceilings themselves are a constraint, not configuration. Each one sits in code next to the probe that
established it, and none is editable from an admin panel, because raising one in our own code would not
raise it at the network. It would only move the failure from our validation to the rail, where it costs a
customer instead of a build. What we can set is a lower limit of our own, and whether a method is offered
at all.</p>
<p>The lesson from getting Rs 15,000 wrong is the one we now apply to every payment limit: a ceiling is a
measurement, not something you read in a document.</p>
<h2 id="frequently-asked-questions" tabindex="-1"><a class="header-anchor" href="#frequently-asked-questions">Frequently asked questions</a></h2>
<h3 id="what-is-the-upi-autopay-limit-in-india" tabindex="-1"><a class="header-anchor" href="#what-is-the-upi-autopay-limit-in-india">What is the UPI AutoPay limit in India?</a></h3>
<p>Rs 15,000 per debit for most recurring payments: up to that amount, debits run without your UPI PIN after
the first one. Insurance premiums, mutual fund subscriptions and credit card bills go up to Rs 1,00,000.
Above the limit, AutoPay still works, but you approve each debit. The mandate itself can be set higher; our
gateway accepted Rs 1,00,000 for our merchant category.</p>
<h3 id="can-upi-autopay-charge-more-than-rs-15000" tabindex="-1"><a class="header-anchor" href="#can-upi-autopay-charge-more-than-rs-15000">Can UPI AutoPay charge more than Rs 15,000?</a></h3>
<p>Yes, if the mandate allows it. RBI’s Rs 15,000 figure is not a ban on larger debits. It is the point above
which you must authenticate each debit, for example with your UPI PIN. The payment goes through once you
approve it, but it is no longer fully automatic.</p>
<h3 id="will-the-full-mandate-amount-be-debited-every-month" tabindex="-1"><a class="header-anchor" href="#will-the-full-mandate-amount-be-debited-every-month">Will the full mandate amount be debited every month?</a></h3>
<p>No. The mandate amount is a maximum. The merchant debits only what you owe for that cycle, and must notify
you at least 24 hours before each debit, so you can check the amount or opt out.</p>
<h3 id="what-is-the-difference-between-upi-autopay-and-upi-reserve-pay" tabindex="-1"><a class="header-anchor" href="#what-is-the-difference-between-upi-autopay-and-upi-reserve-pay">What is the difference between UPI AutoPay and UPI Reserve Pay?</a></h3>
<p>UPI AutoPay registers permission to debit later, up to a maximum, and money stays in the customer’s account
until each debit. UPI Reserve Pay, also called Single Block Multiple Debit, blocks the amount in the
customer’s account up front and lets the merchant debit against that block. Its cap is far lower than
AutoPay’s.</p>
<h3 id="which-recurring-payment-method-should-an-indian-saas-business-use" tabindex="-1"><a class="header-anchor" href="#which-recurring-payment-method-should-an-indian-saas-business-use">Which recurring payment method should an Indian SaaS business use?</a></h3>
<p>Lead with UPI AutoPay for bills that stay under Rs 15,000 a cycle, since customers already use it and
renewals stay automatic. Offer a card mandate for larger bills or customers who prefer cards, and eNACH for
enterprise or annual contracts where the amount exceeds what UPI and cards allow.</p>
<h3 id="what-happens-if-a-upi-autopay-payment-fails" tabindex="-1"><a class="header-anchor" href="#what-happens-if-a-upi-autopay-payment-fails">What happens if a UPI AutoPay payment fails?</a></h3>
<p>The merchant can retry, but NPCI rules since August 2025 allow at most four attempts per debit: the original
plus three retries, run only in off-peak windows. After that, the debit is dropped. A well-run billing
system tells the customer early and offers a one-off payment link before the retries run out.</p>
<h2 id="further-reading" tabindex="-1"><a class="header-anchor" href="#further-reading">Further reading</a></h2>
<ul>
<li>RBI, <a href="https://www.rbi.org.in/Scripts/BS_ViewMasDirections.aspx?id=13374">Digital Payments - E-mandate Framework, 2026</a> (21 April 2026), which sets the Rs 15,000 and Rs 1,00,000 thresholds, first-debit authentication and the 24-hour notice</li>
<li>RBI circular of <a href="https://www.rbi.org.in/Scripts/NotificationUser.aspx?Id=12570&amp;Mode=0">December 12, 2023 on processing e-mandates for recurring transactions</a>, which added the Rs 1,00,000 categories</li>
<li><a href="/docs/upi-autopay-cloud-hosting/">UPI AutoPay for cloud hosting: mandates, limits and what breaks</a>, how Voroa sizes a mandate</li>
<li><a href="https://www.scconline.com/blog/post/2025/07/30/upi-changes-starting-august-1-ncpi-guidelines-upi-api-usage-2025/">Summary of UPI changes from August 1, 2025</a>, including AutoPay retries and time windows</li>
<li><a href="/docs/renewal-payment-link/">Paying a renewal by payment link</a> when an auto-debit does not go through</li>
<li><a href="/docs/add-a-payment-method/">Add a payment method on Voroa</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>A deploy is not finished when the container starts</title>
      <link>https://getvoroa.com/blog/a-deploy-is-not-done-when-the-container-starts/</link>
      <guid isPermaLink="true">https://getvoroa.com/blog/a-deploy-is-not-done-when-the-container-starts/</guid>
      <pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate>
      <dc:creator>Sagar Tayde</dc:creator>
      <category>Deploys</category>
      <category>Reliability</category>
      <description><![CDATA[A running container is not a finished deploy. What a deployment health check should verify before calling a service live: crash loops, readiness, ports, routing.]]></description>
      <content:encoded><![CDATA[<p>The easiest deploy status to build is the one that lies. The container process started, so the
platform shows “Live”, and the customer opens their URL to a 502.</p>
<p><strong>Short answer:</strong> a running container only proves that a process started and has not exited yet. A
deploy is done when the service has stayed up through a crash-loop window and answered a request on
the port it actually listens on. That second check, the deployment health check, is the one most
platforms skip.</p>
<p>That gap, between <em>a process exists</em> and <em>this service answers</em>, is where most of the support load
in a deploy platform comes from. Closing it is not hard. It is just work nobody does, because the
optimistic version demos well.</p>
<h2 id="what-running-actually-proves" tabindex="-1"><a class="header-anchor" href="#what-running-actually-proves">What “running” actually proves</a></h2>
<p>A container’s state tells you the supervisor started a process and the process has not exited yet.
It does not tell you:</p>
<ul>
<li>whether the app bound a port, or bound the wrong one</li>
<li>whether it finished booting, or is thirty seconds into loading a model</li>
<li>whether it crashed and is in a restart loop that happens to be “up” at the instant you looked</li>
<li>whether anything in front of it (the proxy, the certificate, DNS) can actually route to it</li>
</ul>
<p>Each of those produces a healthy-looking container and a broken URL. A status that reports on the
first while the customer experiences the fourth is not a status. It is a coin flip with good
branding.</p>
<h3 id="running-vs-ready-vs-live" tabindex="-1"><a class="header-anchor" href="#running-vs-ready-vs-live">Running vs ready vs live</a></h3>
<div class="table-scroll"><table>
<thead>
<tr>
<th>State</th>
<th>What it proves</th>
<th>What it does not prove</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Running</strong></td>
<td>A process started and has not exited</td>
<td>That it listens anywhere, or will still be up in five seconds</td>
</tr>
<tr>
<td><strong>Ready</strong></td>
<td>The process answers a request on its port</td>
<td>That the public URL routes to it</td>
</tr>
<tr>
<td><strong>Live</strong></td>
<td>Real traffic reaches the new version through the domain</td>
<td>Nothing more is needed for a deploy verdict</td>
</tr>
</tbody>
</table></div>
<p>Kubernetes users will recognise the split: it is the difference between a container that is
running and one that passes its readiness probe. Most simpler platforms collapse all three rows
into one green tick.</p>
<h2 id="the-restart-loop-is-the-honest-test" tabindex="-1"><a class="header-anchor" href="#the-restart-loop-is-the-honest-test">The restart loop is the honest test</a></h2>
<p>The restart loop deserves special attention, because it is the case where naive status reporting is
not merely incomplete but actively backwards.</p>
<p>A container that crashes on boot and restarts every two seconds spends most of its life in the
<code>running</code> state. Poll it at a random moment and you will usually catch it running. The more broken
it is, the faster it crash-loops, and the more reliably it looks fine. Any check that samples state
once will report the worst service on the platform as healthy.</p>
<p>So the check cannot be a sample. It has to be a state that persists: has this service stayed up,
and answered, for long enough that “it is up” is a claim about the service rather than about the
moment you asked.</p>
<h2 id="what-a-deployment-health-check-should-verify" tabindex="-1"><a class="header-anchor" href="#what-a-deployment-health-check-should-verify">What a deployment health check should verify</a></h2>
<p><img src="https://media.getvoroa.com/img/8451bb8a6e045881.png" alt="Four deploy checks in order: build finished, container stayed running, service answered on its port, traffic reaches it" width="700" loading="eager" decoding="async"></p>
<p>Our rule is that a deploy counts as successful only when something outside the container has
confirmed it. A complete verdict checks four things, in order:</p>
<ol>
<li><strong>The build finished.</strong> Not started: finished, with the exit status read, not inferred.</li>
<li><strong>The container reached a running state and stayed there</strong> across a window long enough that a
crash loop cannot hide inside it.</li>
<li><strong>The service answered</strong> on the port it actually listens on, at the health path if one is
configured. This is the check that separates “a process exists” from “a service exists”.</li>
<li><strong>Routing resolves.</strong> The domain, the certificate and the proxy rule all point at the new
container, not the previous one.</li>
</ol>
<p>Steps 1 to 3 are cheap and belong inside the deploy itself. Step 4 depends on DNS and certificate
issuance, which can lag the deploy by minutes, so treat it as a separate signal rather than a reason
to hold the deploy open.</p>
<h3 id="why-the-previous-version-should-keep-serving" tabindex="-1"><a class="header-anchor" href="#why-the-previous-version-should-keep-serving">Why the previous version should keep serving</a></h3>
<p>A verdict that waits for confirmation needs somewhere for traffic to go while it waits. Keep the
old container serving until the new one passes its checks. A deploy that fails its health check
then costs the customer nothing but a red line in their history, instead of an outage.</p>
<h2 id="the-portless-exception-and-why-it-matters" tabindex="-1"><a class="header-anchor" href="#the-portless-exception-and-why-it-matters">The portless exception, and why it matters</a></h2>
<p>Not every service has a port. A background worker, a cron host and a static site all deploy
through the same pipeline, and none of them answers HTTP on a port of its own.</p>
<p>The tempting shortcut is to run the port probe anyway and treat the failure as non-fatal. Do not.
A check that is allowed to fail without consequence is not a check. It is a line in a log that
teaches everyone to ignore it, and the day it starts failing for a real reason, it has already been
trained out of everyone’s attention.</p>
<p>Skip the probe explicitly for service types that have no port, and resolve on the running state
instead. The verdict stays honest because the check that was skipped was skipped on purpose, by
type, in code you can read, not because it failed and nobody minded.</p>
<h2 id="saying-i-dont-know" tabindex="-1"><a class="header-anchor" href="#saying-i-dont-know">Saying “I don’t know”</a></h2>
<p>The last piece is the one that takes nerve: when the platform cannot confirm a deploy, it has to
say so, rather than rounding up to success.</p>
<p>“Deployed, but we could not confirm it is serving traffic” is an unsatisfying sentence to put in a
product. It is also the truth, it is actionable, and it is far better than a green tick above a URL
that returns 502. Customers forgive a platform that is honest about uncertainty. They do not forgive
one that was confidently wrong, because they spent the afternoon debugging their own code on its
say-so.</p>
<p>The general principle, past deploys: a status that cannot be wrong in a way you would notice is not
telling you anything.</p>
<h2 id="frequently-asked-questions" tabindex="-1"><a class="header-anchor" href="#frequently-asked-questions">Frequently asked questions</a></h2>
<h3 id="why-does-my-app-show-502-bad-gateway-when-the-container-is-running" tabindex="-1"><a class="header-anchor" href="#why-does-my-app-show-502-bad-gateway-when-the-container-is-running">Why does my app show 502 Bad Gateway when the container is running?</a></h3>
<p>A 502 means the proxy reached your container but got no valid answer. The usual causes are an app
listening on a different port than the one the proxy targets, an app bound to <code>127.0.0.1</code> instead of
<code>0.0.0.0</code>, or an app that is still booting or stuck in a crash loop. A running container rules out
none of these.</p>
<h3 id="what-is-the-difference-between-a-liveness-check-and-a-readiness-check" tabindex="-1"><a class="header-anchor" href="#what-is-the-difference-between-a-liveness-check-and-a-readiness-check">What is the difference between a liveness check and a readiness check?</a></h3>
<p>A liveness check asks whether the process is still alive and should be restarted if not. A readiness
check asks whether it can serve traffic right now. A deploy verdict needs readiness: a process can
be alive for minutes while it loads data, and sending it traffic in that time still fails.</p>
<h3 id="how-long-should-a-deploy-wait-before-calling-a-service-healthy" tabindex="-1"><a class="header-anchor" href="#how-long-should-a-deploy-wait-before-calling-a-service-healthy">How long should a deploy wait before calling a service healthy?</a></h3>
<p>Long enough to cover your slowest normal boot plus a margin, so a crash loop cannot pass by luck.
Most web apps answer within seconds; apps that load large models or run migrations at start may need
a minute or more. Make the window configurable per service rather than global.</p>
<h3 id="do-background-workers-need-a-health-check" tabindex="-1"><a class="header-anchor" href="#do-background-workers-need-a-health-check">Do background workers need a health check?</a></h3>
<p>They cannot be probed over HTTP because they have no port. Check that the process reached a running
state and stayed there through the startup window. If the worker can write a heartbeat somewhere,
alert on a missing heartbeat as a separate signal.</p>
<h2 id="further-reading" tabindex="-1"><a class="header-anchor" href="#further-reading">Further reading</a></h2>
<ul>
<li><a href="/docs/health-checks">How health checks work on Voroa</a>, including the startup timeout setting</li>
<li><a href="/docs/deploy-status">Deploy statuses explained</a>: Building, Starting, Live and Failed</li>
<li><a href="/docs/troubleshooting-deploys">Troubleshooting failed deploys</a></li>
<li><a href="/docs/deploy-background-worker">Deploying a background worker</a>, the portless case above</li>
<li>Kubernetes documentation on <a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/">liveness, readiness and startup probes</a></li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
