<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ELIM</title>
    <description>The latest articles on DEV Community by ELIM (@elimio).</description>
    <link>https://dev.to/elimio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3821798%2F55dd0ded-909a-4538-9f58-4f98a83da8c4.png</url>
      <title>DEV Community: ELIM</title>
      <link>https://dev.to/elimio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elimio"/>
    <language>en</language>
    <item>
      <title>Moving from a VPS to Cloud Run: what each piece becomes</title>
      <dc:creator>ELIM</dc:creator>
      <pubDate>Wed, 14 Oct 2026 07:08:14 +0000</pubDate>
      <link>https://dev.to/elimio/moving-from-a-vps-to-cloud-run-what-each-piece-becomes-12ob</link>
      <guid>https://dev.to/elimio/moving-from-a-vps-to-cloud-run-what-each-piece-becomes-12ob</guid>
      <description>&lt;p&gt;&lt;em&gt;This guide was first published on &lt;a href="https://exodus.elim.io/blog/a-vps-to-cloud-run" rel="noopener noreferrer"&gt;exodus.elim.io&lt;/a&gt;. Prices are Google's list prices on the date in the text.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Moving from a VPS to Cloud Run puts the app in a container and turns every other piece of the server into a Google service: Postgres into Cloud SQL, uploads into Cloud Storage, crontab into Cloud Scheduler and jobs, nginx into Cloud Run's HTTPS. In our model the bill goes up, from $8.39 to $12.59 a month to $9.59 to $174.52, so move for the work saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  In short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On price the VPS wins at every size in our model: $8.39 to $12.59 a month on Hetzner against $9.59 to $174.52 on Cloud Run with Cloud SQL.&lt;/li&gt;
&lt;li&gt;The systemd unit becomes a Dockerfile; the app listens on PORT, 8080 by default, and finishes within 10 seconds of SIGTERM, where systemd gave 90.&lt;/li&gt;
&lt;li&gt;Postgres moves with pg_dump and pg_restore, or with Database Migration Service for versions 9.4 to 18 at no extra charge.&lt;/li&gt;
&lt;li&gt;Crontab lines become Cloud Run jobs started by Cloud Scheduler, $0.10 a job a month after 3 free, in the server's time zone rather than UTC.&lt;/li&gt;
&lt;li&gt;Requests stop at 32 MiB over HTTP/1 and 60 minutes, and outbound port 25 is blocked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prices exclude VAT and are list prices of October 11, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  When moving pays, and when it does not
&lt;/h2&gt;

&lt;p&gt;A VPS is cheap. Hetzner's CX23 in Germany or Finland (2 vCPUs, 4 GB of memory, a 40 GB disk) costs $6.49 a month since Hetzner's price change of June 15, 2026, plus $0.60 for its IPv4 address and 20% of the server price for daily backups, $8.39 in all. The CX33 with 4 vCPUs and 8 GB is $12.59 on the same terms. Both include 20 TB of outbound traffic a month. DigitalOcean's Basic droplet with 1 vCPU and 2 GiB is $12 with 2,000 GiB of traffic included ($14.40 with weekly backups); the one with 2 vCPUs and 4 GiB is $24 ($28.80).&lt;/p&gt;

&lt;p&gt;On Google Cloud the same app becomes at least three bills. Cloud Run, with request-based billing in a Tier 1 region such as europe-west1, charges $0.000024 per vCPU-second and $0.0000025 per GiB-second while requests are handled, and $0.40 per million requests, after a &lt;a href="https://exodus.elim.io/blog/the-cloud-run-free-tier-explained" rel="noopener noreferrer"&gt;free tier&lt;/a&gt; of 180,000 vCPU-seconds, 360,000 GiB-seconds and 2 million requests a month per billing account. Traffic to users in Europe is $0.105 per GiB from the first gigabyte (the &lt;a href="https://exodus.elim.io/blog/egress-on-google-cloud-explained" rel="noopener noreferrer"&gt;egress guide&lt;/a&gt; has the details). The database becomes Cloud SQL: the shared-core &lt;code&gt;db-f1-micro&lt;/code&gt; is $9.37 a month with 10 GiB of disk, the &lt;code&gt;db-g1-small&lt;/code&gt; $27.25 (the &lt;a href="https://exodus.elim.io/blog/cloud-sql-smallest-instances-and-what-they-cost" rel="noopener noreferrer"&gt;Cloud SQL guide&lt;/a&gt; explains both). Uploads go to Cloud Storage at $0.020 per GiB a month.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One web app with Postgres, on a Hetzner cloud server and on Cloud Run with Cloud SQL in europe-west1, list prices of October 11, 2026, before VAT&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The model is one web app with its database, three sizes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A quiet internal tool:&lt;/strong&gt; 30,000 requests and 2 GB out a month. The CX23 is $8.39. Cloud Run's part fits in the free tier, traffic is $0.20 and storage $0.02, but the Cloud SQL micro is $9.37: $9.59.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small app:&lt;/strong&gt; 1 million requests of 200 ms and 50 GB out. The CX23 handles it, $8.39. On Cloud Run, as an upper bound with no two requests sharing an instance: $0.48 of CPU past the free tier, $4.89 of traffic, the micro at $9.37 and $0.10 of storage, $14.84.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A busy app:&lt;/strong&gt; 10 million requests and 1 TB out, which still fits a CX33 and its 20 TB of traffic, $12.59. On Cloud Run: $43.68 of CPU, $1.60 of memory, $3.20 of requests, $97.79 of traffic, a &lt;code&gt;db-g1-small&lt;/code&gt; at $27.25 because 0.6 GB of memory is too little for a database at this load, and $1.00 of storage, $174.52.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So on price the VPS wins at every size. The database alone on Cloud SQL costs more than a whole Hetzner server, and traffic that the VPS includes is the largest line of the Cloud Run bill. If the bill is the reason to move, do not move.&lt;/p&gt;

&lt;p&gt;The reasons to move are the work the server needs: patching the operating system, renewing certificates, restarting after a crash, watching the disk, testing backups. If the one machine fails, the app is down until someone rebuilds it. Cloud Run spreads instances across the zones of a region and keeps running when one zone fails; a deploy creates a revision you can roll back to in one command; Cloud SQL patches Postgres and takes daily backups; a traffic spike starts more instances. Move when that work costs you more than the difference on the bill, when the app has outgrown one machine, or when the rest of your system already lives on Google Cloud. Stay when the load is steady, a lot of data goes out, the app depends on a local disk, or one person runs the server happily. &lt;a href="https://exodus.elim.io/blog/cloud-run-or-a-vps" rel="noopener noreferrer"&gt;Cloud Run or a VPS&lt;/a&gt; weighs that choice in full.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the server does for you
&lt;/h2&gt;

&lt;p&gt;Start with an inventory, on the server. These commands only read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl list-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;service &lt;span class="nt"&gt;--state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;running &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; app&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sudo ls&lt;/span&gt; /etc/cron.d
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-T&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'server_name|proxy_pass|root |client_max_body_size|proxy_read_timeout'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'#'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt;
&lt;span class="nb"&gt;sudo ls&lt;/span&gt; /etc/letsencrypt/live
timedatectl
&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /srv/app/uploads /var/log
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'\l+'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;app&lt;/code&gt; with the user the app runs as and &lt;code&gt;/srv/app&lt;/code&gt; with its directory. The services list shows the app and anything next to it (a worker, Redis). Timers and crontabs are the scheduled work. The nginx lines give the domain, the upstream port, the static directory, the largest upload and the longest wait. &lt;code&gt;timedatectl&lt;/code&gt; gives the time zone the crontab runs in, and the last two lines show how much data moves. We ran the crontab, nginx, &lt;code&gt;ss&lt;/code&gt; and &lt;code&gt;psql&lt;/code&gt; lines in Debian 13 and Postgres 17 containers set up like a small VPS.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Where each piece of a VPS goes on Google Cloud&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: the app in a container
&lt;/h2&gt;

&lt;p&gt;Cloud Run runs container images, so the app needs a Dockerfile. If the app already runs in Docker on the server, keep its Dockerfile and read this step for the four code changes; a stack under Compose is mapped service by service in &lt;a href="https://exodus.elim.io/blog/docker-compose-to-cloud-run" rel="noopener noreferrer"&gt;Docker Compose to Cloud Run&lt;/a&gt;. Otherwise the systemd unit tells you most of what the Dockerfile needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/shop.service&lt;/span&gt;
&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;app
&lt;span class="nt"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/srv/app
&lt;span class="nt"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/srv/app/.env
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/usr/bin/node server.mjs
&lt;span class="nt"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;always
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WorkingDirectory&lt;/code&gt; becomes &lt;code&gt;WORKDIR&lt;/code&gt;, &lt;code&gt;ExecStart&lt;/code&gt; becomes &lt;code&gt;CMD&lt;/code&gt;, &lt;code&gt;User&lt;/code&gt; becomes &lt;code&gt;USER&lt;/code&gt;. &lt;code&gt;EnvironmentFile&lt;/code&gt; does not go into the image; its values become Cloud Run settings in Step 4. &lt;code&gt;Restart=always&lt;/code&gt; has no line: Cloud Run replaces instances itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:24-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /srv/app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; node&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.mjs"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;.dockerignore&lt;/code&gt; with &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;uploads/&lt;/code&gt;, &lt;code&gt;logs/&lt;/code&gt;, &lt;code&gt;node_modules/&lt;/code&gt; and &lt;code&gt;.git/&lt;/code&gt;, so passwords and user files stay out of the image. Other languages follow the same pattern; the &lt;a href="https://exodus.elim.io/blog/buildpacks-or-a-dockerfile" rel="noopener noreferrer"&gt;buildpacks guide&lt;/a&gt; shows how to skip the Dockerfile.&lt;/p&gt;

&lt;p&gt;Four things change for the code itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The port.&lt;/strong&gt; Cloud Run sends requests to the port in the &lt;code&gt;PORT&lt;/code&gt; variable, 8080 by default, on &lt;code&gt;0.0.0.0&lt;/code&gt;. An app that listens on &lt;code&gt;127.0.0.1:3000&lt;/code&gt; behind nginx gets &lt;code&gt;--port=3000&lt;/code&gt; at deploy time and must listen on all addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process managers.&lt;/strong&gt; pm2 in cluster mode or gunicorn with eight workers filled the machine's cores. On Cloud Run run one process, or one worker per vCPU of the instance, and let Cloud Run add instances; the &lt;a href="https://exodus.elim.io/blog/concurrency-on-cloud-run" rel="noopener noreferrer"&gt;concurrency guide&lt;/a&gt; explains the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shutdown.&lt;/strong&gt; Cloud Run sends &lt;code&gt;SIGTERM&lt;/code&gt; and kills the container 10 seconds later. systemd waits 90 seconds by default, so a slow shutdown that never mattered now gets cut off. Handle &lt;code&gt;SIGTERM&lt;/code&gt; by closing the server and finishing open requests; the &lt;a href="https://exodus.elim.io/blog/fly-to-cloud-run" rel="noopener noreferrer"&gt;Fly guide&lt;/a&gt; shows the difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CPU.&lt;/strong&gt; Cloud Run runs Linux x86_64 images. If you build on an ARM Mac, build with &lt;code&gt;--platform linux/amd64&lt;/code&gt;. Cloud Run's second generation execution environment (&lt;code&gt;--execution-environment=gen2&lt;/code&gt;, always used by jobs) has full Linux compatibility, so native modules that ran on the VPS run here too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then find everything the app writes to disk. Run the image with a read-only file system and a writable &lt;code&gt;/tmp&lt;/code&gt;, and every write outside &lt;code&gt;/tmp&lt;/code&gt; fails loudly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; shop:1 &lt;span class="nb"&gt;.&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--read-only&lt;/span&gt; &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /tmp &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 shop:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;A sample app run read-only in Docker on our machine, October 11, 2026; its image also creates logs/ and uploads/, as the server had them. Stack lines are cut&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Cloud Run's file system is writable, so the app would start there; the test only finds the writes, because each written file lives in the instance's memory and disappears with it. Our sample crashed on its log file and, once logging went to stdout, failed on an upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: files on disk
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Logs&lt;/strong&gt; go to stdout and stderr. Cloud Run sends them to Cloud Logging, which stores the first 50 GiB per project a month free and charges $0.50 per GiB after that. Remove the log files, the logrotate config and any log shipping agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uploads&lt;/strong&gt; go to Cloud Storage. The clean way is to change the code: write each upload to a bucket with Google's client library, and serve it either through the app or with signed URLs. If the code is large and paths are everywhere, Cloud Run can mount a bucket as a directory with Cloud Storage FUSE, once the service exists (Step 5):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud storage buckets create gs://PROJECT_ID-uploads &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west1
gcloud run services update shop &lt;span class="nt"&gt;--region&lt;/span&gt; europe-west1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;uploads,type&lt;span class="o"&gt;=&lt;/span&gt;cloud-storage,bucket&lt;span class="o"&gt;=&lt;/span&gt;PROJECT_ID-uploads &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-volume-mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;uploads,mount-path&lt;span class="o"&gt;=&lt;/span&gt;/srv/app/uploads
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google lists its limits: no file locking, two writes to the same file lose all but the last, and it is not a full POSIX file system. Uploads with unique names work; a SQLite database or a file several instances append to does not. The &lt;a href="https://exodus.elim.io/blog/files-and-uploads-with-cloud-storage" rel="noopener noreferrer"&gt;files guide&lt;/a&gt; compares this with signed URLs.&lt;/p&gt;

&lt;p&gt;Copy the existing files ahead of the move and again during the switch; &lt;code&gt;rsync&lt;/code&gt; copies only what changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud storage rsync /srv/app/uploads gs://PROJECT_ID-uploads &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That needs gcloud on the server; or &lt;code&gt;rsync&lt;/code&gt; the directory to your machine first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scratch files&lt;/strong&gt; in &lt;code&gt;/tmp&lt;/code&gt; count against the instance's memory, and filling it crashes the instance. An ephemeral disk for services is in Preview (1 to 100 GiB a volume, 10 GiB an instance by default, deleted with the instance). Files that must outlive a request go to the bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: the database
&lt;/h2&gt;

&lt;p&gt;Postgres on the server becomes Cloud SQL, a Postgres that Google runs, patches and backs up. Create the instance and the database first (the &lt;a href="https://exodus.elim.io/blog/cloud-sql-smallest-instances-and-what-they-cost" rel="noopener noreferrer"&gt;Cloud SQL guide&lt;/a&gt; has the command and the sizes). For a database of a few gigabytes, a dump and a restore during a short write freeze is enough. Postgres on a VPS usually listens only on localhost, so dump on the server and copy the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# on the server, after stopping writes&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres pg_dump &lt;span class="nt"&gt;-Fc&lt;/span&gt; &lt;span class="nt"&gt;--no-owner&lt;/span&gt; &lt;span class="nt"&gt;--no-acl&lt;/span&gt; shop &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/shop.dump
&lt;span class="c"&gt;# on your machine&lt;/span&gt;
scp app@SERVER:/tmp/shop.dump &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restore through the Cloud SQL Auth Proxy as the &lt;a href="https://exodus.elim.io/blog/digitalocean-app-platform-to-cloud-run" rel="noopener noreferrer"&gt;App Platform guide&lt;/a&gt; shows, and compare row counts. We ran the same dump and restore between two Postgres 17 containers and got the same 1,234 rows on both sides.&lt;/p&gt;

&lt;p&gt;For a larger database, or one that cannot stop for long, Google's Database Migration Service copies a self-managed Postgres (versions 9.4 to 18) and then keeps replicating changes until you switch. It needs the &lt;code&gt;pglogical&lt;/code&gt; extension on the server, some settings changed and a Postgres restart, and the server must accept connections from Google. For Postgres to Postgres it costs nothing extra.&lt;/p&gt;

&lt;p&gt;On Cloud Run the app connects through a Unix socket. Deploy with &lt;code&gt;--add-cloudsql-instances=PROJECT_ID:europe-west1:shop-db&lt;/code&gt;, give the service account the Cloud SQL Client role, and set &lt;code&gt;DATABASE_URL&lt;/code&gt; to &lt;code&gt;postgresql://shop:PASSWORD@/shop?host=/cloudsql/PROJECT_ID:europe-west1:shop-db&lt;/code&gt;. Two things differ from localhost: each query crosses a network, so pages with hundreds of small queries get slower, and every instance opens its own pool: the micro allows 25 connections, so ten instances with a pool of five already run out. Set &lt;code&gt;--max-instances&lt;/code&gt; with that in mind.&lt;/p&gt;

&lt;p&gt;Redis on the same server, if the inventory showed one, is a decision of its own. As a cache it can often go away; for queues see the &lt;a href="https://exodus.elim.io/blog/background-workers-with-cloud-tasks" rel="noopener noreferrer"&gt;background workers guide&lt;/a&gt;, for the rest the &lt;a href="https://exodus.elim.io/blog/patterns-that-break-on-cloud-run" rel="noopener noreferrer"&gt;patterns guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: secrets and settings
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file splits in two. Values that are not secret (&lt;code&gt;NODE_ENV&lt;/code&gt;, a bucket name, a public URL) become &lt;code&gt;--set-env-vars&lt;/code&gt;. Passwords and keys go to Secret Manager, copied by a pipe so they never appear on the screen or in the shell history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^STRIPE_KEY='&lt;/span&gt; .env | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-f2-&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | gcloud secrets create STRIPE_KEY &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the value is in quotes in the file, add &lt;code&gt;sed 's/^"//; s/"$//'&lt;/code&gt; before &lt;code&gt;tr&lt;/code&gt;. Check lengths, never values: &lt;code&gt;gcloud secrets versions access latest --secret=STRIPE_KEY | wc -c&lt;/code&gt;. We checked the pipe on a sample &lt;code&gt;.env&lt;/code&gt; byte by byte.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt; changes, since it now points at the socket. Type the new value without echo and pipe it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-rs&lt;/span&gt; URL &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | gcloud secrets create DATABASE_URL &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service account needs &lt;code&gt;roles/secretmanager.secretAccessor&lt;/code&gt; on each secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: deploy
&lt;/h2&gt;

&lt;p&gt;Create a service account for the app, then build the image once and deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts create shop
gcloud artifacts repositories create shop &lt;span class="nt"&gt;--repository-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west1
gcloud builds submit &lt;span class="nt"&gt;--tag&lt;/span&gt; europe-west1-docker.pkg.dev/PROJECT_ID/shop/shop:1
gcloud run deploy shop &lt;span class="nt"&gt;--region&lt;/span&gt; europe-west1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; europe-west1-docker.pkg.dev/PROJECT_ID/shop/shop:1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shop@PROJECT_ID.iam.gserviceaccount.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512Mi &lt;span class="nt"&gt;--max-instances&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-cloudsql-instances&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PROJECT_ID:europe-west1:shop-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-env-vars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production,UPLOAD_BUCKET&lt;span class="o"&gt;=&lt;/span&gt;PROJECT_ID-uploads &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-secrets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DATABASE_URL:latest,STRIPE_KEY&lt;span class="o"&gt;=&lt;/span&gt;STRIPE_KEY:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service account needs &lt;code&gt;roles/cloudsql.client&lt;/code&gt; and access to the bucket. The output ends with an HTTPS URL on &lt;code&gt;run.app&lt;/code&gt;; test everything there before DNS changes. Building from source with &lt;code&gt;gcloud run deploy --source .&lt;/code&gt; also works, as the &lt;a href="https://exodus.elim.io/blog/vercel-to-cloud-run" rel="noopener noreferrer"&gt;Vercel guide&lt;/a&gt; shows, but a tagged image is what the jobs in the next step reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: cron, timers and the processes next to the app
&lt;/h2&gt;

&lt;p&gt;On a VPS, cron runs a command on the same machine. On Google Cloud the schedule moves to Cloud Scheduler, and the command runs as a job. Take two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;15 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /srv/app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; node &lt;span class="nb"&gt;jobs&lt;/span&gt;/cleanup.mjs &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; logs/cron.log 2&amp;gt;&amp;amp;1
&lt;span class="k"&gt;*&lt;/span&gt;/5 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; http://127.0.0.1:3000/tasks/run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A command&lt;/strong&gt; becomes a Cloud Run job, from the same image, and a Scheduler job that starts it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run &lt;span class="nb"&gt;jobs &lt;/span&gt;deploy cleanup &lt;span class="nt"&gt;--region&lt;/span&gt; europe-west1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; europe-west1-docker.pkg.dev/PROJECT_ID/shop/shop:1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node &lt;span class="nt"&gt;--args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;jobs&lt;/span&gt;/cleanup.mjs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shop@PROJECT_ID.iam.gserviceaccount.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-cloudsql-instances&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PROJECT_ID:europe-west1:shop-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-secrets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DATABASE_URL:latest
gcloud scheduler &lt;span class="nb"&gt;jobs &lt;/span&gt;create http cleanup-nightly &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"15 3 * * *"&lt;/span&gt; &lt;span class="nt"&gt;--time-zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Europe/Berlin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://run.googleapis.com/v2/projects/PROJECT_ID/locations/europe-west1/jobs/cleanup:run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--oauth-service-account-email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shop@PROJECT_ID.iam.gserviceaccount.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The account Scheduler uses needs &lt;code&gt;roles/run.invoker&lt;/code&gt; on the job. Set &lt;code&gt;--time-zone&lt;/code&gt; to what &lt;code&gt;timedatectl&lt;/code&gt; printed: Scheduler defaults to UTC, and a crontab runs in the server's zone. Run the job by hand once with &lt;code&gt;gcloud run jobs execute cleanup --region europe-west1 --wait&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A call to localhost&lt;/strong&gt; becomes a Scheduler HTTP job that calls the service's URL. On the VPS, &lt;code&gt;/tasks/run&lt;/code&gt; answered only on localhost, but a public Cloud Run service answers everyone on every path. Either move the task into a job like the one above, or have Scheduler send an OIDC token (&lt;code&gt;--oidc-service-account-email&lt;/code&gt;) and make the app verify it before it does anything.&lt;/p&gt;

&lt;p&gt;systemd timers move the same way. The &lt;a href="https://exodus.elim.io/blog/cron-and-timers-with-cloud-scheduler" rel="noopener noreferrer"&gt;cron and timers guide&lt;/a&gt; has Scheduler's price, retries and overlap traps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A second process,&lt;/strong&gt; such as a worker reading a queue under its own systemd unit, becomes a Cloud Run worker pool with no URL (the &lt;a href="https://exodus.elim.io/blog/ecs-fargate-to-cloud-run" rel="noopener noreferrer"&gt;ECS guide&lt;/a&gt; has the command), or a Cloud Tasks queue (the &lt;a href="https://exodus.elim.io/blog/background-workers-with-cloud-tasks" rel="noopener noreferrer"&gt;background workers guide&lt;/a&gt; compares them). Work the app does after the response needs instance-based billing; the &lt;a href="https://exodus.elim.io/blog/cloud-run-services-jobs-and-worker-pools" rel="noopener noreferrer"&gt;services, jobs and worker pools guide&lt;/a&gt; explains why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: nginx, the network and the switch
&lt;/h2&gt;

&lt;p&gt;nginx did several jobs, and each one goes somewhere else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TLS and certbot.&lt;/strong&gt; Cloud Run serves HTTPS with a certificate Google manages. Remove certbot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The proxy.&lt;/strong&gt; Requests reach the container directly. For nginx rules you depend on (rewrites, basic auth), Google documents nginx as a second container in the service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static files&lt;/strong&gt; from &lt;code&gt;root /srv/app/public&lt;/code&gt; come from the app, or from a bucket behind a CDN if they are large.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload size&lt;/strong&gt; (&lt;code&gt;client_max_body_size&lt;/code&gt;). Cloud Run accepts requests up to 32 MiB over HTTP/1. Larger uploads go straight from the browser to Cloud Storage with a &lt;a href="https://exodus.elim.io/blog/files-and-uploads-with-cloud-storage" rel="noopener noreferrer"&gt;signed URL&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Waiting time&lt;/strong&gt; (&lt;code&gt;proxy_read_timeout&lt;/code&gt;). Cloud Run's request timeout is 5 minutes by default and 60 at most, set with &lt;code&gt;--timeout&lt;/code&gt;. WebSocket and SSE connections nginx kept open are covered in &lt;a href="https://exodus.elim.io/blog/websockets-and-sse-on-cloud-run" rel="noopener noreferrer"&gt;WebSockets and SSE on Cloud Run&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two VPS habits need a decision. &lt;strong&gt;A fixed outbound address:&lt;/strong&gt; if a partner's API allows only your server's IP, note that Cloud Run's address changes. A static one needs a VPC network and a Cloud NAT gateway, and Google warns that with Direct VPC egress this can delay cold starts by 30 seconds or more. &lt;strong&gt;Email on port 25:&lt;/strong&gt; Google Cloud blocks outbound connections to port 25 by default. Send mail through a provider on port 587 or its API.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What changes in a move, from Hetzner's, systemd's and Google's pages of October 11, 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The domain.&lt;/strong&gt; Google offers three ways to put it on Cloud Run: a global external Application Load Balancer (the one Google recommends), Firebase Hosting, or Cloud Run domain mapping, which is in Preview. The &lt;a href="https://exodus.elim.io/blog/vercel-to-cloud-run" rel="noopener noreferrer"&gt;Vercel guide&lt;/a&gt; compares them and gives the order of the switch: lower the DNS TTL a day before, test on the &lt;code&gt;run.app&lt;/code&gt; URL, change DNS, keep the old setup for a week. A VPS adds five steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put the app on the server into maintenance mode, so nothing writes.&lt;/li&gt;
&lt;li&gt;Take the final dump and restore it, or let Database Migration Service catch up and promote the new database.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;gcloud storage rsync&lt;/code&gt; for the uploads one last time.&lt;/li&gt;
&lt;li&gt;Change DNS, then comment out the crontab and stop the timers on the server, so nightly jobs do not run twice.&lt;/li&gt;
&lt;li&gt;Keep the server for a week as the way back. Then take a last backup or snapshot, download it, and delete the server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What breaks most often
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Files written next to the code (logs, uploads, a cache, a SQLite file) that vanish with every new instance.&lt;/li&gt;
&lt;li&gt;Code that listens on &lt;code&gt;127.0.0.1&lt;/code&gt; or ignores &lt;code&gt;PORT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A shutdown that systemd gave 90 seconds and Cloud Run gives 10.&lt;/li&gt;
&lt;li&gt;A crontab moved to Scheduler without its time zone, running an hour or two off.&lt;/li&gt;
&lt;li&gt;A localhost-only endpoint that is now public.&lt;/li&gt;
&lt;li&gt;Too many database connections once several instances each open a pool.&lt;/li&gt;
&lt;li&gt;A partner API that only accepts the old server's IP, or mail sent to port 25.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these show up on the &lt;code&gt;run.app&lt;/code&gt; URL if you test the jobs, an upload and a sign-in there. The &lt;a href="https://exodus.elim.io/blog/patterns-that-break-on-cloud-run" rel="noopener noreferrer"&gt;patterns that break on Cloud Run&lt;/a&gt; guide covers the rest. For a machine with many services, Google's Migrate to Containers tool turns a Linux VM into container images; for one app, a hand-written Dockerfile is shorter. A Compute Engine VM has its own steps in &lt;a href="https://exodus.elim.io/blog/compute-engine-vm-to-cloud-run" rel="noopener noreferrer"&gt;Compute Engine VM to Cloud Run&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Exodus comes in
&lt;/h2&gt;

&lt;p&gt;A VPS rarely keeps its setup in the repository. Exodus's CLI reads what is there (the code, a Dockerfile, deploy scripts, systemd units and nginx configs if committed), and your coding agent reads the code for what the server did implicitly: paths it writes to, &lt;code&gt;localhost&lt;/code&gt; calls, port 25, pool sizes. The report lists each piece from the map above with what it becomes, and puts the month on Cloud Run with Cloud SQL next to what the server costs now, traffic included. When the server is cheaper and nothing argues for moving, it says to stay.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Cloud Run cheaper than a VPS?
&lt;/h3&gt;

&lt;p&gt;Usually not. In our model a Hetzner server costs $8.39 to $12.59 a month with backups and 20 TB of traffic, while the same app on Cloud Run with Cloud SQL costs $9.59 to $174.52. Cloud SQL alone costs more than the whole server, and traffic is the largest line at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I move cron jobs from a VPS to Cloud Run?
&lt;/h3&gt;

&lt;p&gt;Each command becomes a Cloud Run job from the app's image, started by a Cloud Scheduler job at $0.10 a month after the first 3 per billing account. Set the time zone the server's crontab used, because Scheduler defaults to UTC, and stop the crontab on the server at the switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a Cloud Run app keep uploaded files on its disk?
&lt;/h3&gt;

&lt;p&gt;No. Cloud Run's file system lives in the instance's memory and disappears with the instance. Uploads go to a Cloud Storage bucket at $0.020 per GiB a month, through the client library or a Cloud Storage FUSE mount, which has no file locking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Cloud Run send email on port 25?
&lt;/h3&gt;

&lt;p&gt;No. Google Cloud blocks outbound connections to port 25 by default. Send mail through a provider on port 587 or through its API, and check this before the switch, together with any partner API that allowlists the old server's IP address.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does Cloud Run wait for an app to shut down?
&lt;/h3&gt;

&lt;p&gt;Cloud Run sends SIGTERM and kills the container 10 seconds later, where systemd waits 90 seconds by default. Handle SIGTERM by closing the server and finishing open requests, so a slow shutdown is not cut off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://docs.hetzner.com/general/infrastructure-and-availability/price-adjustment" rel="noopener noreferrer"&gt;Hetzner price adjustment of June 15, 2026, Hetzner docs (cloud servers in Germany and Finland, excluding VAT and IPv4: CX23 $6.49 a month, CX33 $9.99; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.hetzner.com/general/infrastructure-and-availability/ipv4-pricing/" rel="noopener noreferrer"&gt;IP pricing, Hetzner docs (cloud primary IPv4 $0.60 a month, IPv6 free; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.hetzner.com/cloud/billing/faq/" rel="noopener noreferrer"&gt;Cloud billing FAQ, Hetzner docs (backups are a flat 20% of the server price, 7 backup slots; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hetzner.com/cloud/cost-optimized" rel="noopener noreferrer"&gt;Cost-Optimized cloud servers, Hetzner (CX23: 2 vCPUs, 4 GB, 40 GB NVMe; at least 20 TB of traffic included in the EU; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.hetzner.com/robot/general/traffic/" rel="noopener noreferrer"&gt;Traffic, Hetzner docs (CX, CPX and CAX cloud servers include 20 TB in the EU; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.digitalocean.com/pricing/droplets" rel="noopener noreferrer"&gt;Droplet pricing, DigitalOcean (Basic: 1 vCPU and 2 GiB $12 a month with 2,000 GiB of transfer; 2 vCPUs and 4 GiB $24 with 4,000 GiB; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.digitalocean.com/products/backups/details/pricing" rel="noopener noreferrer"&gt;Backups pricing, DigitalOcean docs (weekly 20%, daily 30% of the Droplet's cost; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.digitalocean.com/platform/billing/bandwidth" rel="noopener noreferrer"&gt;Bandwidth billing, DigitalOcean docs (outbound transfer past the allowance $0.01 per GiB, pooled per team; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/run/pricing" rel="noopener noreferrer"&gt;Cloud Run pricing, Google (request-based billing in Tier 1: $0.000024 per vCPU-second, $0.0000025 per GiB-second, $0.40 per million requests; 180,000 vCPU-seconds, 360,000 GiB-seconds and 2 million requests free a month per billing account; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/sql/pricing" rel="noopener noreferrer"&gt;Cloud SQL pricing, Google (db-f1-micro $0.0105 an hour, db-g1-small $0.035; SSD storage $0.17 per GB a month; shared-core machines not covered by the SLA; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/storage/pricing" rel="noopener noreferrer"&gt;Cloud Storage pricing, Google (Standard storage in a region such as europe-west1 $0.000027397 per GiB-hour, $0.020 a GiB-month; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/products/observability/pricing" rel="noopener noreferrer"&gt;Google Cloud Observability pricing, Google (log storage $0.50 per GiB, first 50 GiB per project a month free; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/scheduler/pricing" rel="noopener noreferrer"&gt;Cloud Scheduler pricing, Google ($0.10 a job per 31 days; 3 jobs free per billing account; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/container-contract" rel="noopener noreferrer"&gt;Container runtime contract, Cloud Run (in-memory file system that counts against the instance's memory; SIGTERM, then SIGKILL after 10 seconds; executables for Linux x86_64; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/execution-environments" rel="noopener noreferrer"&gt;Execution environments, Cloud Run (second generation: full Linux compatibility, all system calls, network file systems; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/quotas" rel="noopener noreferrer"&gt;Cloud Run quotas and limits, Google (request timeout up to 60 minutes; HTTP/1 request and response size 32 MiB; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/request-timeout" rel="noopener noreferrer"&gt;Configure request timeout for services, Cloud Run (5 minutes by default; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/services/cloud-storage-volume-mounts" rel="noopener noreferrer"&gt;Cloud Storage volume mounts for services, Cloud Run (Cloud Storage FUSE; no file locking, the last write wins; not fully POSIX; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/services/ephemeral-disk" rel="noopener noreferrer"&gt;Configure an ephemeral disk for Cloud Run services, Google (Preview; 1 to 100 GiB a volume, 10 GiB an instance by default, deleted when the instance stops; second generation only; page of Oct 9, read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/sql/docs/postgres/connect-run" rel="noopener noreferrer"&gt;Connect from Cloud Run, Cloud SQL for PostgreSQL (--add-cloudsql-instances, a Unix socket at /cloudsql/INSTANCE_CONNECTION_NAME, the Cloud SQL Client role; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/database-migration/docs/postgres/configure-source-database" rel="noopener noreferrer"&gt;Configure your source, Database Migration Service (continuous migrations from self-managed PostgreSQL 9.4 to 18 with pglogical; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/database-migration" rel="noopener noreferrer"&gt;Database Migration Service, Google (homogeneous migrations at no additional charge; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/execute/jobs-on-schedule" rel="noopener noreferrer"&gt;Execute jobs on a schedule, Cloud Run (a Cloud Scheduler job that calls the job's run endpoint; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/internet-proxy-nginx-sidecar" rel="noopener noreferrer"&gt;Frontend proxying using Nginx, Cloud Run (nginx as a second container in the same service; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/mapping-custom-domains" rel="noopener noreferrer"&gt;Map custom domains, Cloud Run (global external Application Load Balancer recommended; Firebase Hosting; Cloud Run domain mapping in Preview, not production-ready; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/static-outbound-ip" rel="noopener noreferrer"&gt;Static outbound IP address, Cloud Run (route egress through a VPC network with a Cloud NAT gateway holding a static address; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/vpc-direct-vpc" rel="noopener noreferrer"&gt;Direct VPC egress with a VPC network, Cloud Run (with Cloud NAT, cold starts can be delayed by 30 seconds or more; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/firewall/docs/firewalls" rel="noopener noreferrer"&gt;VPC firewall rules, Google (egress to TCP port 25 of external addresses blocked by default; 465 and 587 not blocked; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/zonal-redundancy" rel="noopener noreferrer"&gt;Understand zonal redundancy, Cloud Run (traffic load balanced across zones in a region; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/migrate/containers/docs/getting-started" rel="noopener noreferrer"&gt;Migrate to Containers overview, Google (containerizes Linux VMs for GKE or Cloud Run; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://exodus.elim.io/blog/egress-on-google-cloud-explained" rel="noopener noreferrer"&gt;Egress on Google Cloud explained, Exodus (Cloud Run to users in Europe at $0.105 per GiB; Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://exodus.elim.io/blog/cloud-sql-smallest-instances-and-what-they-cost" rel="noopener noreferrer"&gt;Cloud SQL's smallest instances and what they cost, Exodus (db-f1-micro $9.37 and db-g1-small $27.25 a month with 10 GiB of SSD; 25 and 50 connections; daily backups on by default; Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.freedesktop.org/software/systemd/man/systemd-system.conf.html" rel="noopener noreferrer"&gt;systemd-system.conf, systemd manual (DefaultTimeoutStopSec defaults to 90 s; read Oct 11, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Exodus team: a sample Node app containerized from its systemd unit and run read-only in Docker 28, the inventory commands run in a Debian 13 container with nginx and cron, pg_dump and pg_restore between two Postgres 17 containers, the .env pipe checked byte by byte, gcloud 587.0.0 help output; run on Oct 11, 2026; the cost model and its arithmetic&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;We wrote this guide while building &lt;a href="https://exodus.elim.io" rel="noopener noreferrer"&gt;Exodus&lt;/a&gt;, a free tool that works with your coding agent: it audits an app on your machine, compares today's bill with Google Cloud Run at list prices, and plans the move. The CLI is open source: &lt;a href="https://github.com/elim-io/exodus" rel="noopener noreferrer"&gt;github.com/elim-io/exodus&lt;/a&gt;. A sample report: &lt;a href="https://exodus.elim.io/demo" rel="noopener noreferrer"&gt;exodus.elim.io/demo&lt;/a&gt;. Exodus is on Product Hunt today: &lt;a href="https://www.producthunt.com/products/exodus-by-elim-io?launch=exodus-by-elim-io" rel="noopener noreferrer"&gt;Exodus by elim.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>devops</category>
      <category>docker</category>
      <category>cloud</category>
    </item>
    <item>
      <title>What Cloud Run costs, with worked examples</title>
      <dc:creator>ELIM</dc:creator>
      <pubDate>Wed, 14 Oct 2026 07:07:38 +0000</pubDate>
      <link>https://dev.to/elimio/what-cloud-run-costs-with-worked-examples-378i</link>
      <guid>https://dev.to/elimio/what-cloud-run-costs-with-worked-examples-378i</guid>
      <description>&lt;p&gt;&lt;em&gt;This guide was first published on &lt;a href="https://exodus.elim.io/blog/what-cloud-run-costs" rel="noopener noreferrer"&gt;exodus.elim.io&lt;/a&gt;. Prices are Google's list prices on the date in the text.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Cloud Run costs $0.000024 per vCPU-second, $0.0000025 per GiB-second and $0.40 per million requests on its default request-based billing in Tier 1 regions, after a monthly free tier of 180,000 vCPU-seconds, 360,000 GiB-seconds and 2 million requests per billing account. A small app at 2,000 visits a day, with a worker and Cloud SQL, costs $11.09 a month, $1.19 of it on Cloud Run.&lt;/p&gt;

&lt;h2&gt;
  
  
  In short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Request-based Cloud Run in Tier 1 costs $0.000024 per vCPU-second, $0.0000025 per GiB-second and $0.40 per million requests; Tier 2 regions cost 1.4 times as much.&lt;/li&gt;
&lt;li&gt;The free tier, 180,000 vCPU-seconds, 360,000 GiB-seconds and 2 million requests a month, belongs to the billing account and is shared by every project on it.&lt;/li&gt;
&lt;li&gt;A small app at 2,000 visits a day costs $11.09 a month, and Cloud SQL's &lt;code&gt;db-f1-micro&lt;/code&gt; with 10 GiB is $9.37 of it.&lt;/li&gt;
&lt;li&gt;One always-busy vCPU with 512 MiB costs $44.71 a month on instance-based billing; for that load a VPS usually costs less.&lt;/li&gt;
&lt;li&gt;A minimum instance kept warm adds about $9.86 a month, and a revision scales to 100 instances unless you set a maximum.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prices are Google's list prices in US dollars for Tier 1 regions, read on October 12, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a line of the bill is computed
&lt;/h2&gt;

&lt;p&gt;Cloud Run's bill is arithmetic. Every part of an app uses some number of vCPU-seconds, GiB-seconds, requests, hours or gigabytes in a month; each has a list price; the free tier comes off the top. Exodus prices a report the same way, with plain code and Google's published prices, so every line can be checked by hand. Every line follows one rule: take what you used, subtract what is free, and multiply by the price, never going below zero. The free part belongs to the billing account, not to the app, and resets on the first of the month. A month here is 730 hours for anything always on, and 30 days for anything counted per request.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The web service from the example below, line by line. Two of its three lines are inside the free tier.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two details decide how much a service uses. First, request-based billing counts an instance's time while at least one request is open on it, rounded up to 100 ms, so two requests that overlap on one instance are billed once. The examples below count every request's time separately, as if none overlapped, so they are an upper bound: with concurrency, the real line is lower, never higher. Second, memory is billed for the same seconds as CPU, at the size you set, so 512 MiB is half a GiB for every billed second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prices
&lt;/h2&gt;

&lt;p&gt;The prices below are Tier 1, the cheaper of Cloud Run's two price groups. Tier 1 holds, for example, &lt;code&gt;us-central1&lt;/code&gt; in Iowa, &lt;code&gt;us-east1&lt;/code&gt; in South Carolina, &lt;code&gt;europe-west1&lt;/code&gt; in Belgium, &lt;code&gt;europe-west4&lt;/code&gt; in the Netherlands and &lt;code&gt;asia-northeast1&lt;/code&gt; in Tokyo. In Tier 2 regions, such as London, Frankfurt, Zurich, Singapore and Sydney, request-based Cloud Run costs 1.4 times as much and instance-based 1.2 times; &lt;a href="https://exodus.elim.io/blog/choosing-a-cloud-run-region" rel="noopener noreferrer"&gt;choosing a region&lt;/a&gt; has the whole list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Run, request-based billing&lt;/strong&gt;, the default, where you pay while a request is in flight, while an instance starts and while it shuts down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU $0.000024 per vCPU-second, memory $0.0000025 per GiB-second, $0.40 per million requests.&lt;/li&gt;
&lt;li&gt;Free every month, per billing account: 180,000 vCPU-seconds, 360,000 GiB-seconds and 2 million requests.&lt;/li&gt;
&lt;li&gt;A minimum instance kept warm bills its idle time at $0.0000025 per vCPU-second and per GiB-second.&lt;/li&gt;
&lt;li&gt;Requests refused by IAM, because the caller had no right to call, are not billed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cloud Run, instance-based billing and jobs&lt;/strong&gt;, where you pay for every second an instance exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU $0.000018 per vCPU-second, memory $0.000002 per GiB-second, no charge per request.&lt;/li&gt;
&lt;li&gt;Free every month: 240,000 vCPU-seconds and 450,000 GiB-seconds.&lt;/li&gt;
&lt;li&gt;A job's instance is billed for its whole life, with a minimum of one minute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two free tiers are separate pools. Do not add them up: a service on request-based billing gets 180,000 vCPU-seconds, not 420,000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Around it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud SQL for PostgreSQL &lt;code&gt;db-f1-micro&lt;/code&gt;, a shared-core machine with 0.6 GiB of memory: $0.0105 an hour. SSD storage $0.17 per GiB a month, backups $0.08 per GiB a month of what they hold.&lt;/li&gt;
&lt;li&gt;Cloud Tasks: the first million operations a month free, then $0.40 per million.&lt;/li&gt;
&lt;li&gt;Cloud Scheduler: 3 jobs free per billing account, then $0.10 per job a month.&lt;/li&gt;
&lt;li&gt;Artifact Registry, where your images live: 0.5 GiB free, then $0.10 per GiB a month.&lt;/li&gt;
&lt;li&gt;Secret Manager: 6 active secret versions free, then $0.06 each a month; 10,000 reads free, then $0.03 per 10,000.&lt;/li&gt;
&lt;li&gt;Cloud Build, which builds the image from source: 2,500 build-minutes a month free on its default machine, then $0.006 a minute.&lt;/li&gt;
&lt;li&gt;Cloud Logging: the first 50 GiB of logs per project a month free, then $0.50 per GiB.&lt;/li&gt;
&lt;li&gt;Internet egress, the bytes your responses send out, Premium tier: $0.12 per GiB after the first free GiB, up to 1 TiB a month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can read Cloud Run's prices the way the bill reads them, from Google's Cloud Billing Catalog. Here is the API's answer for a few of Cloud Run's SKUs, the billing codes behind each line:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cloud Run's prices from the Billing Catalog API, effective Oct 12, 2026. Tier 2 is 1.4 times Tier 1 for request-based CPU and 1.2 times for instance-based.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The catalog needs an access token in a header file, never on the command line. &lt;a href="https://exodus.elim.io/blog/reading-your-first-google-cloud-bill" rel="noopener noreferrer"&gt;Reading your first Google Cloud bill&lt;/a&gt; shows how the same SKUs appear on an invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  A web service at 2,000 visits a day
&lt;/h2&gt;

&lt;p&gt;Say each visit makes 10 requests and each request takes 0.35 s of one vCPU, with 512 MiB of memory. That is a typical server-rendered app or an API behind a single-page front end.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests: 2,000 x 10 x 30 = 600,000 a month, inside the free 2 million: &lt;strong&gt;$0.00&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;CPU: 600,000 x 0.35 s = 210,000 vCPU-seconds. The free tier takes 180,000, so 30,000 x $0.000024 = &lt;strong&gt;$0.72&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Memory: 210,000 s x 0.5 GiB = 105,000 GiB-seconds, inside the free 360,000: &lt;strong&gt;$0.00&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The web service costs 72 cents a month. Memory is the line people worry about and the one that matters least: at 512 MiB it is 5% of the price of a billed second.&lt;/p&gt;

&lt;p&gt;The same service at other levels of traffic, with the same assumptions:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The web service by traffic, free tier applied, overlap not counted. Above 2,000 visits a day the CPU line grows with the traffic; requests start to bill above about 6,700 visits a day.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At 20,000 visits a day the CPU line alone is 1.92 million billable vCPU-seconds, about 533 hours of a busy vCPU. That is where it is worth looking at concurrency and the real overlap of your requests, and at the comparison below. &lt;a href="https://exodus.elim.io/blog/cloud-run-costs-worked-examples-by-traffic" rel="noopener noreferrer"&gt;Cloud Run costs by traffic&lt;/a&gt; works through more levels, with real request durations.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worker turned into Cloud Tasks
&lt;/h2&gt;

&lt;p&gt;Say the app sends 1,300 background jobs a day, each 0.5 s of CPU: emails, webhooks, a thumbnail. On a PaaS that is a worker process waiting on a Redis queue all day. On Cloud Run each job becomes an HTTP request from Cloud Tasks to a handler on the same service, as &lt;a href="https://exodus.elim.io/blog/background-workers-with-cloud-tasks" rel="noopener noreferrer"&gt;background workers with Cloud Tasks&lt;/a&gt; shows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU: 1,300 x 30 x 0.5 s = 19,500 vCPU-seconds. The web line has used the free tier, so 19,500 x $0.000024 = &lt;strong&gt;$0.47&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Memory: 9,750 GiB-seconds, and with the web line's 105,000 still inside the free 360,000: &lt;strong&gt;$0.00&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Requests and tasks: 39,000 a month each, inside Cloud Run's free 2 million and Cloud Tasks' free million: &lt;strong&gt;$0.00&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a PaaS the same worker is a second always-on process, billed every hour whether it works or waits, plus the Redis it waits on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres on Cloud SQL
&lt;/h2&gt;

&lt;p&gt;The smallest shared-core instance, up all month, with 10 GiB of SSD:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;730 hours x $0.0105 = $7.67&lt;/li&gt;
&lt;li&gt;10 GiB x $0.17 = $1.70&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together &lt;strong&gt;$9.37 a month&lt;/strong&gt;, plus a few cents for backups of a small database. The database is usually the largest line of a small app's bill, and it is the one line that does not scale to zero.&lt;/p&gt;

&lt;p&gt;Three things to know about this machine. It shares a CPU and has 0.6 GiB of memory, which is enough for a small app's database and not for a busy one. Google does not cover shared-core machines with the Cloud SQL SLA. And if you stay on a Postgres major version past the end of its standard support, Google adds extended support at $0.018 an hour for &lt;code&gt;db-f1-micro&lt;/code&gt;, more than the machine itself. &lt;a href="https://exodus.elim.io/blog/cloud-sql-smallest-instances-and-what-they-cost" rel="noopener noreferrer"&gt;The smallest Cloud SQL instances&lt;/a&gt; compares the next sizes up, and &lt;a href="https://exodus.elim.io/blog/cloud-sql-firestore-or-serverless-postgres" rel="noopener noreferrer"&gt;Cloud SQL, Firestore or serverless Postgres&lt;/a&gt; covers the options that cost less when the database is small or idle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole app
&lt;/h2&gt;

&lt;p&gt;Add the rest: three schedules in Cloud Scheduler (free), 1 GiB of images in Artifact Registry (0.5 GiB over the free tier: $0.05), five secrets (free), and 4 GiB of egress at $0.12 ($0.48).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The small app's month at 2,000 visits a day. The database is about 85% of the bill; everything on Cloud Run itself is $1.19.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web service: $0.72&lt;/li&gt;
&lt;li&gt;Worker as Cloud Tasks: $0.47&lt;/li&gt;
&lt;li&gt;Cloud SQL, db-f1-micro with 10 GiB: $9.37&lt;/li&gt;
&lt;li&gt;Cloud Scheduler, 3 jobs: $0.00&lt;/li&gt;
&lt;li&gt;Artifact Registry, 1 GiB: $0.05&lt;/li&gt;
&lt;li&gt;Secret Manager, 5 secrets: $0.00&lt;/li&gt;
&lt;li&gt;Egress, 4 GiB: $0.48&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total: $11.09 a month.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The egress line is on the high side. Google's network pricing page lists $0.12 a GiB, but Cloud Run's own SKU for traffic from Europe to Europe is $0.105 in the Billing Catalog, and that is what our bills show.&lt;/p&gt;

&lt;p&gt;The free tiers are per billing account, so this example gives all of them to one app. If you run several apps or projects in one account, they share them, and the order in which they use them up decides which project gets billed. Our own bills showed it: secrets on two of our services billed a few cents a month only because other projects on the account had already used the six free versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lines that surprise a first bill
&lt;/h2&gt;

&lt;p&gt;The example leaves out a few settings that are easy to switch on and bill every hour of the month, whatever the traffic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A minimum instance.&lt;/strong&gt; One instance of 1 vCPU and 512 MiB kept warm bills its idle time at $0.0000025 per vCPU-second and per GiB-second: 2,628,000 seconds x $0.00000375 = about $9.86 a month, as much as the database. &lt;a href="https://exodus.elim.io/blog/min-instances-and-startup-cpu-boost" rel="noopener noreferrer"&gt;Min instances and startup CPU boost&lt;/a&gt; shows when it is worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High availability on Cloud SQL.&lt;/strong&gt; A standby in a second zone doubles the machine: &lt;code&gt;db-f1-micro&lt;/code&gt; goes from $0.0105 to $0.021 an hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A load balancer for a custom domain.&lt;/strong&gt; It bills per hour and per gigabyte, apart from Cloud Run; &lt;a href="https://exodus.elim.io/blog/custom-domains-and-https-on-cloud-run" rel="noopener noreferrer"&gt;custom domains and HTTPS&lt;/a&gt; compares it with Firebase Hosting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fixed outbound address&lt;/strong&gt;, for a partner that allows only known IPs. It needs Cloud NAT, which bills per hour; &lt;a href="https://exodus.elim.io/blog/egress-on-google-cloud-explained" rel="noopener noreferrer"&gt;egress on Google Cloud&lt;/a&gt; has the numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them is needed for a small app on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Cloud Run costs more
&lt;/h2&gt;

&lt;p&gt;Request billing rewards idle time. A process that keeps a CPU busy most of the day, such as an image resizer under constant load or a worker that never waits, has none to give. Run it on instance-based billing, one vCPU and 512 MiB, up all month:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU: 730 h x 3,600 s = 2,628,000 vCPU-seconds, less 240,000 free, x $0.000018 = $42.98&lt;/li&gt;
&lt;li&gt;Memory: 1,314,000 GiB-seconds, less 450,000 free, x $0.000002 = $1.73&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is &lt;strong&gt;$44.71 a month&lt;/strong&gt; for one always-busy core, and $49.93 once the free tier is used by something else. On request-based billing the same instance, busy all day, would cost $61.14; the two cross at about 18 busy hours a day, as the chart in &lt;a href="https://exodus.elim.io/blog/what-cloud-run-is" rel="noopener noreferrer"&gt;what Cloud Run is&lt;/a&gt; shows. A one-year commitment takes 17% off Cloud Run's prices, which helps a steady service and not a spiky one.&lt;/p&gt;

&lt;p&gt;For steady load around the clock, a fixed-price VM or VPS usually costs less than any Cloud Run setting, and &lt;a href="https://exodus.elim.io/blog/cloud-run-or-a-vps" rel="noopener noreferrer"&gt;Cloud Run or a VPS&lt;/a&gt; compares them. In this case Exodus's verdict is to stay, and the report says why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What our own services cost
&lt;/h2&gt;

&lt;p&gt;Exodus team: these are our own small services on Cloud Run in &lt;code&gt;europe-west1&lt;/code&gt;, October 2026, from steady days of the billing export.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Rust API on a document database, with a scheduled job every minute: $0.13 a month. Its Cloud Run usage, $1.79 at list prices, was all inside the free tier; the 13 cents were lines outside Cloud Run, such as a secret.&lt;/li&gt;
&lt;li&gt;A Rust API, a web app and a worker on Cloud SQL &lt;code&gt;db-f1-micro&lt;/code&gt; with 10 GiB: $9.79 a month, of which the database was $9.34. The estimate made from list prices, as above, was $9.64; the gap was storage reads, traffic between zones and free tiers used by other projects.&lt;/li&gt;
&lt;li&gt;Five tiny services sharing one project: $0.69 a month.&lt;/li&gt;
&lt;li&gt;A one-page static site behind nginx: about a cent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first days after a move cost more than a steady month, because the first deploys build on Google: Cloud Build charged $2.10 on the first day of one of ours.&lt;/p&gt;

&lt;h2&gt;
  
  
  There is no hard cap
&lt;/h2&gt;

&lt;p&gt;Google bills what runs. A bug that loops, or a crawler that hammers an endpoint, scales the service up and the bill with it, to 100 instances per revision by default. Set a maximum number of instances on every service; for a small app, 3 to 10 is a sane start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run services update my-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; europe-west1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-instances&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set a budget alert on the billing account. A spend cap budget can also pause Cloud Run in one project when its estimated cost passes an amount; it acts on estimated costs, not instantly, and anything used before it acts is billed as usual. &lt;a href="https://exodus.elim.io/blog/budget-alerts-and-the-billing-export" rel="noopener noreferrer"&gt;Budget alerts and the billing export&lt;/a&gt; sets up both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Exodus comes in
&lt;/h2&gt;

&lt;p&gt;An Exodus report does this arithmetic for your app: it reads the processes, the database and the schedules from your repository, asks for your traffic, and prices each part on today's host and on Cloud Run from these list prices, line by line, with the date the prices were read. Once the app runs on Google, &lt;code&gt;exodus bill&lt;/code&gt; reads the real month from your project's billing export in BigQuery, so the estimate can be checked against the invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Cloud Run free for a small app?
&lt;/h3&gt;

&lt;p&gt;Cloud Run itself is free for a small app while its usage stays inside the monthly free tier of 180,000 vCPU-seconds, 360,000 GiB-seconds and 2 million requests per billing account; the web service in this guide costs $0 at 500 visits a day. The database does not scale to zero: Cloud SQL's smallest instance with 10 GiB is $9.37 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Cloud Run charge when there is no traffic?
&lt;/h3&gt;

&lt;p&gt;On request-based billing, the default, a Cloud Run service is billed only while a request is open on an instance, while it starts and while it shuts down. A few settings bill every hour whatever the traffic: a minimum instance, about $9.86 a month at 1 vCPU and 512 MiB, high availability on Cloud SQL, a load balancer and Cloud NAT.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Cloud Run cheaper than a VPS?
&lt;/h3&gt;

&lt;p&gt;Cloud Run costs less than a VPS for an app that is idle most of the day, because request billing rewards idle time. For steady load around the clock, a fixed-price VM or VPS usually costs less: one always-busy vCPU with 512 MiB costs $44.71 a month on instance-based billing and $61.14 on request-based.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does a Cloud Run job cost?
&lt;/h3&gt;

&lt;p&gt;A Cloud Run job is billed at instance-based prices, $0.000018 per vCPU-second and $0.000002 per GiB-second in Tier 1, for the whole life of its instance, with a minimum of one minute. Jobs draw on the instance-based free tier of 240,000 vCPU-seconds and 450,000 GiB-seconds a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I set a spending limit on Cloud Run?
&lt;/h3&gt;

&lt;p&gt;Google bills what runs, so there is no hard cap. Set a maximum number of instances on every service, 3 to 10 for a small app instead of the default 100 per revision, and a budget alert on the billing account. A spend cap budget can pause Cloud Run in one project, but it acts on estimated costs, not instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/run/pricing" rel="noopener noreferrer"&gt;Cloud Run pricing, Google (request-based: $0.000024 per vCPU-second, $0.0000025 per GiB-second, $0.40 per million requests, free each month per billing account 180,000 vCPU-seconds, 360,000 GiB-seconds, 2 million requests; instance-based and jobs: $0.000018 and $0.000002, free 240,000 and 450,000; idle minimum instance $0.0000025; usage rounded up to 100 ms; requests denied by IAM are not billed; jobs billed for the whole life of an instance with a minimum of 1 minute; outbound traffic at networking prices with 1 GiB a month free within North America; Tier 1 and Tier 2 region lists; list prices read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/billing/docs/how-to/get-pricing-information-api" rel="noopener noreferrer"&gt;Cloud Billing Catalog API, Cloud Run service 152E-C115-5142 (Tier 2 request-based CPU $0.0000336 and memory $0.0000035, 1.4 times Tier 1; instance-based CPU in europe-west2 $0.0000216, 1.2 times europe-west1; Cloud Run internet egress Europe to Europe $0.105 per GiB in its first tier; prices effective Oct 12, 2026, queried Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/cud" rel="noopener noreferrer"&gt;Compute flexible committed use discounts, Cloud Run, Google (17% off with a one-year commitment; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/sql/pricing" rel="noopener noreferrer"&gt;Cloud SQL pricing, Google (db-f1-micro $0.0105 an hour, shared core, 0.6 GiB, not covered by the Cloud SQL SLA; SSD storage $0.000232877 per GiB-hour, $0.17 a GiB-month; backups $0.08 a GiB-month; extended support for db-f1-micro $0.018 an hour in years 1 and 2; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/tasks/pricing" rel="noopener noreferrer"&gt;Cloud Tasks pricing, Google (first 1 million billable operations a month free per billing account, then $0.40 per million; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/scheduler/pricing" rel="noopener noreferrer"&gt;Cloud Scheduler pricing, Google (3 jobs a month free per billing account, not per project; then $0.10 per job per 31 days, counted per day; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/artifact-registry/pricing" rel="noopener noreferrer"&gt;Artifact Registry pricing, Google (0.5 GiB of storage free per billing account, then $0.000136986 per GiB-hour, $0.10 a GiB-month; transfer within a continent free; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/secret-manager/pricing" rel="noopener noreferrer"&gt;Secret Manager pricing, Google (6 active secret versions free, then $0.06 a version a month; 10,000 access operations free, then $0.03 per 10,000; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/vpc/network-pricing" rel="noopener noreferrer"&gt;Network pricing, Google (Premium Tier internet data transfer: the first GiB free, then $0.12 per GiB to 1 TiB to North America and to Europe; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/build/pricing" rel="noopener noreferrer"&gt;Cloud Build pricing, Google (2,500 free build-minutes a month per billing account on e2-standard-2 in the default pool, then $0.006 a minute; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/products/observability/pricing" rel="noopener noreferrer"&gt;Cloud Logging pricing, Google (ingestion $0.50 per GiB with 30 days of storage, the first 50 GiB per project a month free; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/max-instances" rel="noopener noreferrer"&gt;Set maximum instances for services, Cloud Run, Google (by default Cloud Run sets 100 instances for each revision; updated Oct 9, 2026, read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/billing/docs/how-to/budgets-spend-caps" rel="noopener noreferrer"&gt;Manage spend cap budgets, Cloud Billing, Google (Cloud Run is eligible; enforced on gross estimated costs, not instantly, and usage before it is billed as normal; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Exodus team: our own Cloud Run services in October 2026, europe-west1, monthly figures from steady days and the billing export (a Rust API on a document database $0.13; a Rust API, web app and worker on Cloud SQL db-f1-micro with 10 GB, $9.79 of which the database $9.34; five tiny services in one project $0.69; a one-page static site about $0.01; one-off builds on Cloud Build $2.10 in the first days of a move; Cloud Run egress billed $0.105 a GiB in Europe)&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;We wrote this guide while building &lt;a href="https://exodus.elim.io" rel="noopener noreferrer"&gt;Exodus&lt;/a&gt;, a free tool that works with your coding agent: it audits an app on your machine, compares today's bill with Google Cloud Run at list prices, and plans the move. The CLI is open source: &lt;a href="https://github.com/elim-io/exodus" rel="noopener noreferrer"&gt;github.com/elim-io/exodus&lt;/a&gt;. A sample report: &lt;a href="https://exodus.elim.io/demo" rel="noopener noreferrer"&gt;exodus.elim.io/demo&lt;/a&gt;. Exodus is on Product Hunt today: &lt;a href="https://www.producthunt.com/products/exodus-by-elim-io?launch=exodus-by-elim-io" rel="noopener noreferrer"&gt;Exodus by elim.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>serverless</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>The patterns that break on Cloud Run, and their replacements</title>
      <dc:creator>ELIM</dc:creator>
      <pubDate>Wed, 14 Oct 2026 07:07:02 +0000</pubDate>
      <link>https://dev.to/elimio/the-patterns-that-break-on-cloud-run-and-their-replacements-2m5a</link>
      <guid>https://dev.to/elimio/the-patterns-that-break-on-cloud-run-and-their-replacements-2m5a</guid>
      <description>&lt;p&gt;&lt;em&gt;This guide was first published on &lt;a href="https://exodus.elim.io/blog/patterns-that-break-on-cloud-run" rel="noopener noreferrer"&gt;exodus.elim.io&lt;/a&gt;. Prices are Google's list prices on the date in the text.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Six patterns break on Cloud Run: a worker waiting on a queue, work after the response, timers inside the app, files on local disk, sessions in memory and long-lived connections. Each assumes a process that runs on its own, owns a disk and keeps its memory. Their replacements are Cloud Tasks, Cloud Scheduler, Cloud Storage, signed cookies, and sockets that reconnect.&lt;/p&gt;

&lt;h2&gt;
  
  
  In short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;With request-based billing, an instance has CPU only while a request is open, and an idle one is stopped within 15 minutes with SIGTERM and 10 seconds.&lt;/li&gt;
&lt;li&gt;After the free tiers, a queue worker kept on at 1 vCPU and 512 MiB costs $44.71 a month, a worker pool $25.95, and 100,000 Cloud Tasks jobs of 0.5 s fit in the free tier.&lt;/li&gt;
&lt;li&gt;Cloud Scheduler gives 3 jobs free per billing account, then $0.10 per job per 31 days, and each job's time zone defaults to UTC.&lt;/li&gt;
&lt;li&gt;Cloud Storage Standard costs $0.02 per GiB a month in europe-west1, and a signed upload URL is valid for at most 7 days.&lt;/li&gt;
&lt;li&gt;exodus 0.1.3 found 5 of the 6 patterns in our sample app; work after the response needs a look by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prices are Google's Tier 1 list prices, read on October 12, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one fact behind all six
&lt;/h2&gt;

&lt;p&gt;Most apps that run on a platform such as Heroku, Railway or Render run on Cloud Run with a handful of changes, and every item in that handful comes from one fact about how Cloud Run runs a service. A product &lt;a href="https://exodus.elim.io/blog/planning-a-new-product-for-cloud-run" rel="noopener noreferrer"&gt;planned for Cloud Run from the first commit&lt;/a&gt; never picks them up. A Cloud Run service runs your container on instances. Each instance takes requests, and with request-based billing Cloud Run allocates its CPU only while at least one request is in flight. Between requests the instance still exists, with its memory, but its CPU is taken away, so any code that runs on its own, a loop, a timer, a promise still resolving after the response, makes no progress. After a quiet spell Cloud Run keeps an idle instance for up to 15 minutes and then shuts it down, sending SIGTERM and allowing 10 seconds before the process is killed. When traffic grows it starts more instances, and a request can land on any of them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One instance of a request-based service: CPU only inside requests, so work left over after the response stalls; an idle instance is stopped within 15 minutes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The filesystem adds the last piece. A Cloud Run instance's disk lives in its memory: files written there count against the memory limit, are invisible to other instances and vanish when the instance stops.&lt;/p&gt;

&lt;p&gt;Instance-based billing changes the first part, because the instance keeps its CPU for its whole life, but it does not keep one instance alive forever and it does not give you a disk. It also bills every second the instance exists, which turns some of these patterns from broken into expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding them in your code
&lt;/h2&gt;

&lt;p&gt;The Exodus CLI looks for all six. We wrote a small Express app that has every one of them, a BullMQ worker, a receipt sent after the response, a node-cron timer, Multer writing uploads to a folder, express-session's default memory store and a WebSocket server, and ran &lt;code&gt;exodus check&lt;/code&gt; on it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;exodus 0.1.3 on the sample app found five of the six patterns with their file and line; the receipt sent after the response was not flagged, so check for that one by hand.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It found five, each with its file and line. It did not flag the sixth, an &lt;code&gt;await sendReceipt()&lt;/code&gt; after &lt;code&gt;res.json()&lt;/code&gt;, so that one needs a look by hand. A rough search covers the common forms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"bullmq|sidekiq|celery|rq worker|node-cron|setInterval|apscheduler|whenever"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.js'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.ts'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.py'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.rb'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node_modules
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"multer|diskStorage|writeFile|:local|FileSystemStorage|sqlite"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.js'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.ts'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.py'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.rb'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.yml'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node_modules
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"MemoryStore|express-session|WebSocketServer|socket.io|EventSource"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.js'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*.ts'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;Procfile&lt;/code&gt;, &lt;code&gt;railway.json&lt;/code&gt; or &lt;code&gt;render.yaml&lt;/code&gt; with more than one process is a second hint: anything that is not the web process is probably a worker or a clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A worker waiting on a queue
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; a BullMQ, Sidekiq, Celery or RQ worker, often its own process in a Procfile or its own service, pulling jobs from Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it breaks:&lt;/strong&gt; the worker is a loop that waits for the next job. On a request-based service it gets no CPU between requests, so jobs stall. On instance-based billing it runs, but bills every second whether there is work or not, and it still has no request to keep Cloud Run from scaling it down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The replacement: Cloud Tasks.&lt;/strong&gt; Each job becomes an HTTP request to your own service. Where the code pushed a job to Redis, it creates a task, and Cloud Tasks calls a handler with the job's body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTask&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queuePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PROJECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;europe-west1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emails&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;httpRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;httpMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/tasks/send-email`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;oidcToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;serviceAccountEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;INVOKER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler at &lt;code&gt;/tasks/send-email&lt;/code&gt; does the job and answers with a 2xx status, which deletes the task. Any other answer, or no answer in time, is retried with backoff. The handler has a dispatch deadline of up to 30 minutes, so longer jobs belong in a Cloud Run job. Two rules keep it safe: check the OIDC token, so only the queue can call the handler, and make the handler safe to run twice, because Cloud Tasks delivers at least once and in rare cases runs a task more than once.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A worker becomes a handler: the service creates a task, Cloud Tasks calls the same service with an OIDC token, and the queue keeps the retries, the rate and the backoff.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The limits: a task's body is at most 1 MiB, a task waits in its queue for at most 31 days, and a queue dispatches up to 500 tasks a second. The first million Cloud Tasks operations a month are free, then $0.40 per million.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What waiting costs.&lt;/strong&gt; Kept as it is, a worker on a service with instance-based billing, 1 vCPU and 512 MiB, costs $44.71 a month after the free tier, whether it handles one job or a million. A worker pool, Cloud Run's resource for processes that pull work, bills $0.000011244 per vCPU-second and comes to $25.95 for the same instance at its default price. The same work as Cloud Tasks, say 100,000 jobs of half a second each, is $1.30 at list price and fits in the free tier if nothing else uses it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What a waiting worker costs a month after the free tiers: $44.71 kept always on, $25.95 as a worker pool, nothing as 100,000 Cloud Tasks jobs of 0.5 s.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Google's own worker pool example gives $11.61 for one instance of the same size for a month. That figure matches the pool's three-year committed price, $0.000006072 per vCPU-second, less the free tier valued at the default price; at the default price, which is what you pay without a commitment, it is the $25.95 above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to keep a loop:&lt;/strong&gt; a consumer of a stream, a job that holds one connection for hours, or a pull subscription that cannot become a push. Worker pools are generally available since April 14, 2026, run without an endpoint and do not scale on their own: you set the number of instances. Use them for that exception, not for an ordinary job queue. The &lt;a href="https://exodus.elim.io/blog/background-workers-with-cloud-tasks" rel="noopener noreferrer"&gt;Cloud Tasks guide&lt;/a&gt; has the queue, the IAM roles, the handler and the retries in full.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Work after the response
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; &lt;code&gt;res.send()&lt;/code&gt; followed by &lt;code&gt;await sendEmail()&lt;/code&gt;, a promise nobody awaits, a "fire and forget" call to an analytics API, a goroutine started by a handler that has already returned, FastAPI's &lt;code&gt;BackgroundTasks&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it breaks:&lt;/strong&gt; once the response is sent, the request is over, and on a request-based service the CPU goes with it. The leftover work stalls until the next request happens to wake the instance, and if the instance is shut down first, it never finishes. Nothing fails loudly: the email arrives an hour late, or not at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The replacement:&lt;/strong&gt; finish the work before you answer, if it is short, or hand it to Cloud Tasks as above and answer at once. For a receipt email, a task is the better choice: the user gets the response without waiting, and the queue retries the email if the mail API is down.&lt;/p&gt;

&lt;p&gt;This is the one pattern the CLI did not flag in our sample, and the one most often missed in review, because the code looks correct and works on a server. Read every handler for code after the line that sends the response. The &lt;a href="https://exodus.elim.io/blog/work-after-the-response" rel="noopener noreferrer"&gt;work after the response guide&lt;/a&gt; shows the forms it takes in Node, Python, Ruby and Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Timers and cron inside the app
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; &lt;code&gt;node-cron&lt;/code&gt;, &lt;code&gt;setInterval&lt;/code&gt;, APScheduler, Celery beat, &lt;code&gt;whenever&lt;/code&gt;, or a separate "clock" process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it breaks:&lt;/strong&gt; a service scaled to zero has no process to run the timer, and a running one gets no CPU between requests. With several instances, the timer fires once per instance, so a daily email goes out three times on a busy day and zero times on a quiet one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The replacement: Cloud Scheduler.&lt;/strong&gt; It sends an HTTP request on a cron schedule. For short work it calls an endpoint of the service, &lt;code&gt;POST /cron/daily-digest&lt;/code&gt;, with an OIDC token. For long work it starts a Cloud Run job, the same image with another command, which runs for 10 minutes by default and up to 168 hours (7 days).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud scheduler &lt;span class="nb"&gt;jobs &lt;/span&gt;create http daily-digest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0 7 * * *"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--time-zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Europe/Berlin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE&lt;/span&gt;&lt;span class="s2"&gt;/cron/daily-digest"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--oidc-service-account-email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details bite. The time zone: each Scheduler job has its own, and it defaults to UTC, while node-cron used the server's. Overlap: if a run is still going when the next fires, both run, so long work needs a lock or a job. Retries: a Scheduler job does not retry a failed attempt unless you set a retry count, and an HTTP attempt has a deadline between 15 seconds and 30 minutes. Each billing account gets three Scheduler jobs free, then $0.10 per job per 31 days. The &lt;a href="https://exodus.elim.io/blog/cron-and-timers-with-cloud-scheduler" rel="noopener noreferrer"&gt;Cloud Scheduler guide&lt;/a&gt; covers jobs, retries and locks.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Files on local disk
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; Multer's &lt;code&gt;dest&lt;/code&gt;, Active Storage's &lt;code&gt;:local&lt;/code&gt; service, Django's &lt;code&gt;FileSystemStorage&lt;/code&gt;, &lt;code&gt;fs.writeFile&lt;/code&gt; into a &lt;code&gt;data/&lt;/code&gt; folder, SQLite on disk, logs written to a file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it breaks:&lt;/strong&gt; the filesystem of a Cloud Run instance lives in its memory. A file counts against the memory limit, so a few large uploads can crash the instance; it is invisible to the other instances, so an upload saved on one is a 404 on the next; and it disappears when the instance stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The replacement: a Cloud Storage bucket.&lt;/strong&gt; For uploads, let the browser send the file straight to the bucket with a signed URL, so the bytes never pass through your service and never touch its memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`uploads/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;write&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service signs a PUT URL for one object, valid for 15 minutes here and for at most 7 days, the browser puts the file there, and the service stores only the key. A file the app generates goes to the bucket through the client library in the same way.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Uploads with a signed URL: the service signs, the browser sends the bytes straight to the bucket, and the service keeps only the object's key.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A temporary file inside one request, for a resize or an unzip, can stay in &lt;code&gt;/tmp&lt;/code&gt; as long as it fits in memory and is deleted before the response. Logs go to stdout, where Cloud Logging collects them. Standard storage costs $0.02 per GiB a month in europe-west1, so 5 GiB of uploads cost about $0.10.&lt;/p&gt;

&lt;p&gt;SQLite is the hard case, because it needs a real disk that one process owns. Move the data to Postgres before the move. The &lt;a href="https://exodus.elim.io/blog/files-and-uploads-with-cloud-storage" rel="noopener noreferrer"&gt;Cloud Storage guide&lt;/a&gt; has the bucket, the CORS rule and the download side.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Sessions and caches in memory
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; express-session's default MemoryStore, Flask-Session's filesystem backend, an in-process cache of logged-in users, or a Redis that holds only sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it breaks:&lt;/strong&gt; the next request can land on another instance, which has never seen the session, and scaling to zero drops them all, so people are logged out at random. Session affinity, which Cloud Run offers as an option, is best effort: it is broken as soon as the instance stops or is full, so it cannot hold a login. Keeping a Redis only for sessions works, but bills all month for a few kilobytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The replacement: signed cookies.&lt;/strong&gt; &lt;code&gt;cookie-session&lt;/code&gt; in Express, the default cookie store in Rails, signed cookies in Django. Sign them with a key from Secret Manager, keep them small (browsers guarantee at least 4,096 bytes per cookie, enough for a user id, a role and an expiry), and set &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;HttpOnly&lt;/code&gt; and &lt;code&gt;SameSite=Lax&lt;/code&gt;. If you need "log out everywhere", keep a session version on the user's row and check it on each request; when the cookie is not enough, a sessions table in the database the app already has does the job.&lt;/p&gt;

&lt;p&gt;An in-process cache is fine as long as the app works without it: each instance warms its own, and a new instance starts cold. The &lt;a href="https://exodus.elim.io/blog/sessions-without-redis" rel="noopener noreferrer"&gt;sessions guide&lt;/a&gt; covers both options.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Long-lived connections
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; WebSockets, server-sent events, a long poll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it is a problem:&lt;/strong&gt; Cloud Run supports WebSockets, but a socket is a request and lasts no longer than the request timeout, 5 minutes by default and 60 minutes at most, so clients must reconnect. An instance with any open WebSocket is billed as instance-based for as long as the socket is open; one instance held open all month at 1 vCPU and 512 MiB is the $44.71 from the chart above. Messages between users connected to different instances need a shared store or a pub/sub, because there is no single process that sees everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The replacement:&lt;/strong&gt; for a few sockets, keep them, raise the timeout and let clients reconnect. For a chat or a live dashboard with thousands of connections open all day, a managed realtime service, or staying on an always-on host, usually costs less. The &lt;a href="https://exodus.elim.io/blog/websockets-and-sse-on-cloud-run" rel="noopener noreferrer"&gt;WebSockets and SSE guide&lt;/a&gt; works out where the line is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small ones
&lt;/h2&gt;

&lt;p&gt;A few smaller habits do not break the app outright but make the first deploy fail. The server must listen on the port in &lt;code&gt;$PORT&lt;/code&gt;, 8080 by default, on 0.0.0.0. Logs go to stdout and stderr, not a file. The process must handle SIGTERM and finish its requests within 10 seconds. Secrets come from Secret Manager, not a &lt;code&gt;.env&lt;/code&gt; file baked into the image. The &lt;a href="https://exodus.elim.io/blog/node-python-rails-and-go-on-cloud-run" rel="noopener noreferrer"&gt;stack guide&lt;/a&gt; shows each of these for Node, Python, Rails and Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist before the move
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Every process other than the web process has a destination: Cloud Tasks, Cloud Scheduler, a job or a worker pool.&lt;/li&gt;
&lt;li&gt;No handler does work after it sends the response.&lt;/li&gt;
&lt;li&gt;Every timer is a Scheduler job with its time zone set and an endpoint or job that tolerates overlap.&lt;/li&gt;
&lt;li&gt;Nothing is written to the local disk except temporary files inside one request.&lt;/li&gt;
&lt;li&gt;Sessions live in signed cookies or the database; the app works if any instance disappears.&lt;/li&gt;
&lt;li&gt;Long-lived connections reconnect, and their cost has been worked out.&lt;/li&gt;
&lt;li&gt;Each change is deployed and tested on the current host first, before anything moves.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last point matters most. Every replacement here, a Cloud Tasks queue, a bucket, a Scheduler job, a signed cookie, works from an app still running on Heroku, Railway or Render. Making the changes there means the move itself only changes where the container runs; the rest of it is in &lt;a href="https://exodus.elim.io/blog/heroku-to-cloud-run" rel="noopener noreferrer"&gt;Moving from Heroku&lt;/a&gt; and &lt;a href="https://exodus.elim.io/blog/railway-or-render-to-cloud-run" rel="noopener noreferrer"&gt;Moving from Railway or Render&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Exodus comes in
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;exodus audit&lt;/code&gt; lists every place these patterns occur, with the file and line, and the report turns each one into a task in the plan, in the order to do them. &lt;code&gt;exodus check&lt;/code&gt; runs the same rules in CI and fails the build when a new one appears. As our sample showed, it does not catch every case of work after the response, so read your handlers for that one.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can a Cloud Run service keep working after it sends the response?
&lt;/h3&gt;

&lt;p&gt;Not on request-based billing, the default: the CPU goes with the request, so the leftover work stalls and is lost if the instance stops first. Finish short work before answering, or hand it to Cloud Tasks and answer at once. Instance-based billing keeps the CPU but bills every second the instance exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use node-cron or setInterval on Cloud Run?
&lt;/h3&gt;

&lt;p&gt;No. A service scaled to zero has no process to run the timer, and with several instances it fires once per instance. Cloud Scheduler sends an HTTP request on a cron schedule instead; each billing account gets three jobs free, then $0.10 per job per 31 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a Cloud Run service write files to disk?
&lt;/h3&gt;

&lt;p&gt;Only temporary ones. The filesystem lives in the instance's memory, counts against the memory limit, is invisible to other instances and vanishes when the instance stops. A temporary file inside one request is fine; uploads and generated files go to a Cloud Storage bucket, $0.02 per GiB a month in europe-west1.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does session affinity keep users logged in on Cloud Run?
&lt;/h3&gt;

&lt;p&gt;No. Session affinity is best effort and breaks as soon as the instance stops or is full, and scaling to zero drops every in-memory session. Keep sessions in signed cookies, which browsers support up to at least 4,096 bytes, or in a table of the database the app already has.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do WebSockets work on Cloud Run?
&lt;/h3&gt;

&lt;p&gt;Yes, but a socket is a request and lasts no longer than the request timeout, 5 minutes by default and 60 at most, so clients must reconnect. An instance with an open socket is billed as instance-based, $44.71 a month at 1 vCPU and 512 MiB if held open all month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/billing-settings" rel="noopener noreferrer"&gt;Billing settings, Cloud Run docs, Google (request-based: CPU only during request processing; instance-based: CPU for the instance's whole life; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/about-instance-autoscaling" rel="noopener noreferrer"&gt;About instance autoscaling, Cloud Run docs, Google (idle instances kept up to 15 minutes, 10 for GPUs; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/container-contract" rel="noopener noreferrer"&gt;Container runtime contract, Cloud Run docs, Google (in-memory filesystem that uses the instance's memory; SIGTERM, then 10 s; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/run/pricing" rel="noopener noreferrer"&gt;Cloud Run pricing, Google (Tier 1 request-based, instance-based and worker pool prices and free tiers; worker pool example 6 $11.61; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/tasks/docs/creating-http-target-tasks" rel="noopener noreferrer"&gt;Creating HTTP target tasks, Cloud Tasks docs, Google (OIDC token; a 2xx deletes the task; dispatch deadline up to 30 minutes; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/tasks/docs/quotas" rel="noopener noreferrer"&gt;Cloud Tasks quotas and limits, Google (task size 1 MiB, retention 31 days, 500 dispatches a second per queue; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/tasks/docs/dual-overview" rel="noopener noreferrer"&gt;Cloud Tasks overview, Google (at-least-once delivery; a task can run more than once; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/tasks/pricing" rel="noopener noreferrer"&gt;Cloud Tasks pricing, Google (first 1 million operations a month free, then $0.40 per million; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/deploy-worker-pools" rel="noopener noreferrer"&gt;Deploy worker pools, Cloud Run docs, Google (pull-based workloads, no endpoint, manual scaling; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/release-notes" rel="noopener noreferrer"&gt;Cloud Run release notes, Google (worker pools generally available April 14, 2026; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/scheduler/pricing" rel="noopener noreferrer"&gt;Cloud Scheduler pricing, Google ($0.10 per job per 31 days, 3 jobs free per billing account; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/config-connector/docs/reference/resource-docs/cloudscheduler/cloudschedulerjob" rel="noopener noreferrer"&gt;Cloud Scheduler job reference, Google (time zone defaults to UTC; HTTP attempt deadline 15 s to 30 minutes; retry count defaults to 0; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/task-timeout" rel="noopener noreferrer"&gt;Set task timeout for jobs, Cloud Run docs, Google (10 minutes by default, up to 168 hours, 1 hour with a GPU; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/storage/pricing" rel="noopener noreferrer"&gt;Cloud Storage pricing, Google (Standard storage $0.000027397 per GiB-hour in europe-west1, $0.02 per GiB-month; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/storage/docs/access-control/signed-urls" rel="noopener noreferrer"&gt;Signed URLs, Cloud Storage docs, Google (V4 signed URLs valid for at most 604,800 s, 7 days; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/session-affinity" rel="noopener noreferrer"&gt;Set session affinity for services, Cloud Run docs, Google (off by default; best effort; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6265" rel="noopener noreferrer"&gt;RFC 6265, HTTP State Management Mechanism, IETF (browsers support at least 4,096 bytes per cookie; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/triggering/websockets" rel="noopener noreferrer"&gt;Using WebSockets, Cloud Run docs, Google (subject to the request timeout; an instance with an open WebSocket is billed as instance-based; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/run/docs/configuring/request-timeout" rel="noopener noreferrer"&gt;Request timeout, Cloud Run docs, Google (5 minutes by default, up to 60 minutes; read Oct 12, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Exodus team: exodus 0.1.3 check and audit on a sample app with all six patterns, Oct 12, 2026&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;We wrote this guide while building &lt;a href="https://exodus.elim.io" rel="noopener noreferrer"&gt;Exodus&lt;/a&gt;, a free tool that works with your coding agent: it audits an app on your machine, compares today's bill with Google Cloud Run at list prices, and plans the move. The CLI is open source: &lt;a href="https://github.com/elim-io/exodus" rel="noopener noreferrer"&gt;github.com/elim-io/exodus&lt;/a&gt;. A sample report: &lt;a href="https://exodus.elim.io/demo" rel="noopener noreferrer"&gt;exodus.elim.io/demo&lt;/a&gt;. Exodus is on Product Hunt today: &lt;a href="https://www.producthunt.com/products/exodus-by-elim-io?launch=exodus-by-elim-io" rel="noopener noreferrer"&gt;Exodus by elim.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>serverless</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Exodus: move an app to Google Cloud Run with the coding agent you already use</title>
      <dc:creator>ELIM</dc:creator>
      <pubDate>Wed, 14 Oct 2026 07:06:57 +0000</pubDate>
      <link>https://dev.to/elimio/exodus-move-an-app-to-google-cloud-run-with-the-coding-agent-you-already-use-1d3</link>
      <guid>https://dev.to/elimio/exodus-move-an-app-to-google-cloud-run-with-the-coding-agent-you-already-use-1d3</guid>
      <description>&lt;p&gt;Exodus is on Product Hunt today: &lt;a href="https://www.producthunt.com/products/exodus-by-elim-io?launch=exodus-by-elim-io" rel="noopener noreferrer"&gt;Exodus by elim.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Exodus is a free web cabinet and an open-source CLI. Together with the coding agent you already use, such as Claude Code, Codex, Cursor or Gemini CLI, it audits an app on your machine. It shows what the app costs today and on Google Cloud Run, lists every change the move needs with its file and line, and gives the agent a plan to work through. It can also design a new product for Cloud Run from an idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we built it
&lt;/h2&gt;

&lt;p&gt;Cloud Run suits most web apps: it scales to zero, bills only for the time your code runs, and includes 2 million requests a month in its free tier. Moving an app there from a platform such as Heroku, Railway or Render is rarely about the container. It is about four habits a platform with long-running processes lets you keep and Cloud Run does not.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Timers inside the process.&lt;/strong&gt; An instance has CPU only while it answers a request, and there may be several instances, so a cron in your server fires late, twice or never. Each schedule moves to Cloud Scheduler: short work calls an endpoint, long work starts a Cloud Run job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A worker waiting on a queue.&lt;/strong&gt; For the same reason the jobs stall between requests. Each job becomes an HTTP task in Cloud Tasks that posts to a handler, and the queue keeps the retries and the rate limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files on local disk.&lt;/strong&gt; The disk lives in the instance's memory and disappears when the instance stops; other instances never see it. Uploads go to a Cloud Storage bucket through a signed URL, and the app keeps only the key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sessions in memory.&lt;/strong&gt; The next request lands on another instance and the user is signed out. The session goes into a signed cookie or a table of the database the app already has.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We met all four while moving our own apps and wrote 59 guides about them along the way. Exodus is those guides turned into a tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Sign up at &lt;a href="https://app.exodus.elim.io" rel="noopener noreferrer"&gt;app.exodus.elim.io&lt;/a&gt; and install the CLI once: &lt;code&gt;curl -fsSL exodus.elim.io/install | sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Give your agent the one line from the cabinet. It runs &lt;code&gt;exodus init&lt;/code&gt; and &lt;code&gt;exodus audit&lt;/code&gt; in your repository, answers the questions it can from the code and asks you the rest.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exodus send&lt;/code&gt; uploads the findings and the answers, and the cabinet builds the report: today's bill against Cloud Run at Google's list prices, the changes, and a plan. Exodus never receives your source code.&lt;/li&gt;
&lt;li&gt;The agent works through the plan task by task, and each new send updates the report.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A sample report
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://exodus.elim.io/demo" rel="noopener noreferrer"&gt;exodus.elim.io/demo&lt;/a&gt; opens the report of a sample project, lumen-api: a Rust API with a worker, timers and uploads on Railway, $48.00 a month today against $10.83 on Cloud Run after 4 changes, with 4 of 9 plan tasks done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Site and guides: &lt;a href="https://exodus.elim.io" rel="noopener noreferrer"&gt;exodus.elim.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CLI, Apache-2.0: &lt;a href="https://github.com/elim-io/exodus" rel="noopener noreferrer"&gt;github.com/elim-io/exodus&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product Hunt: &lt;a href="https://www.producthunt.com/products/exodus-by-elim-io?launch=exodus-by-elim-io" rel="noopener noreferrer"&gt;Exodus by elim.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We would like to hear where the audit gets your app wrong. Comments here or on Product Hunt both reach us.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>devops</category>
      <category>opensource</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
