<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Abhinay Gokul - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Abhinay Gokul - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 14 Oct 2026 12:49:57 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/abhinaygokul/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Keep a File for Minutes, Not Years, on Cloud Run ]]>
                </title>
                <description>
                    <![CDATA[ A print job, a one-time download, or a support attachment typically only needs to exist until someone finishes with it. But most backends still store it like Google Drive: a named object, no expiry, a ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-keep-a-file-for-minutes-not-years-on-cloud-run/</link>
                <guid isPermaLink="false">6acf003cd00b1413f617c32e</guid>
                
                    <category>
                        <![CDATA[ google cloud ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CloudComputing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cybersecurity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Redis ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abhinay Gokul ]]>
                </dc:creator>
                <pubDate>Wed, 14 Oct 2026 04:08:28 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/5b5c4c33-45b1-4e68-88d7-2d3ff82b0961.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A print job, a one-time download, or a support attachment typically only needs to exist until someone finishes with it. But most backends still store it like Google Drive: a named object, no expiry, and a database row that can search it later.</p>
<p>This tutorial will teach you a smaller pattern I use in production for a print relay. The ideas aren't specific to printing. You can use the same pattern for any short job: accept bytes, hold them briefly, hand them to an authorized caller, then delete them.</p>
<p>You won't get a full app. You'll get the decisions that stop a queue from turning into a document library.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-youll-build">What You'll build</a></p>
</li>
<li><p><a href="#heading-what-you-need">What You Need</a></p>
</li>
<li><p><a href="#heading-one-service-not-a-fleet">One Service, Not a Fleet</a></p>
</li>
<li><p><a href="#heading-give-the-object-a-boring-name">Give the Object a Boring Name</a></p>
</li>
<li><p><a href="#heading-encrypt-for-storage-and-be-honest-about-decrypt">Encrypt for Storage, and Be Honest About Decrypt</a></p>
</li>
<li><p><a href="#heading-put-the-queue-in-memory-that-expires">Put the Queue in Memory That Expires</a></p>
</li>
<li><p><a href="#heading-delete-on-purpose-then-sweep-what-you-missed">Delete on Purpose, Then Sweep What You Missed</a></p>
</li>
<li><p><a href="#heading-what-not-to-claim">What Not to Claim</a></p>
</li>
<li><p><a href="#heading-where-to-go-next">Where to Go Next</a></p>
</li>
</ul>
<h2 id="heading-what-youll-build">What You'll build</h2>
<p>The contract is small:</p>
<ol>
<li><p>A client sends a file for one job.</p>
</li>
<li><p>Your API writes ciphertext to object storage and a short-lived job record to Redis.</p>
</li>
<li><p>An authorized caller can fetch the file once.</p>
</li>
<li><p>When the job ends, or when a timer fires, you delete the object and the job record.</p>
</li>
</ol>
<p>Success is an empty bucket, not a searchable archive.</p>
<pre><code class="language-text">Client
  |
  |  HTTPS
  v
Cloud Run (one Node service)
  |-- Cloud Storage     cipher-text object
  |-- Cloud KMS         wraps the per-file key
  |-- Redis             job JSON with a time to live (TTL)
  |
Authorized caller  &lt;--  stream, then delete
</code></pre>
<h2 id="heading-what-you-need">What You Need</h2>
<p>You should already know:</p>
<ul>
<li><p>A little Node.js and Express</p>
</li>
<li><p>What HTTPS and JSON are</p>
</li>
<li><p>That object storage (Amazon S3, Google Cloud Storage) keeps files, and Redis keeps fast key-value data</p>
</li>
</ul>
<p>You don't need Kubernetes or a second "print server." If you're on Google Cloud, then Cloud Run, Cloud Storage, Cloud Key Management Service (Cloud KMS), and any Redis host are enough. The same split works on other clouds if you rename the products.</p>
<h2 id="heading-one-service-not-a-fleet">One Service, Not a Fleet</h2>
<p>A file that lives for twenty minutes doesn't need an always-on virtual machine, a cluster, and a box at the customer's office.</p>
<p>Put the website and the API in one Cloud Run service. Cloud Run bills CPU while requests run. When nobody is uploading, you aren't paying a queue machine to sit there.</p>
<p>That choice has a trap. Cloud Run can run more than one instance. If each instance keeps jobs in its own memory, instance A won't see the job that instance B accepted. The user then refreshes and the job is "gone," or worse, you print the wrong one.</p>
<p>So:</p>
<ul>
<li><p>The process can be stateless.</p>
</li>
<li><p>The queue can't.</p>
</li>
</ul>
<p>Readiness should fail when Redis is down. A liveness check that only proves "Node is up" will send traffic to a service that can't tell the truth about the queue. Fail closed. Don't fall back to a local disk on whichever replica got the request.</p>
<pre><code class="language-js">app.get("/up", (_req, res) =&gt; {
  res.status(200).send("ok");
});

app.get("/health", async (_req, res) =&gt; {
  const redisUp = await pingRedis();
  if (!redisUp) return res.status(503).send("queue down");
  res.status(200).send("ok");
});
</code></pre>
<p>Use <code>/health</code> for the load balancer or Cloud Run probe. Use <code>/up</code> only to see that the container can answer.</p>
<h2 id="heading-give-the-object-a-boring-name">Give the Object a Boring Name</h2>
<p>If the object key is <code>aadhaar-ramesh.pdf</code>, you already built a filing cabinet. The next feature request will be search.</p>
<p>Store bytes under a random id:</p>
<pre><code class="language-js">const { randomUUID } = require("crypto");

function objectName() {
  return `jobs/${randomUUID()}.bin`;
}
</code></pre>
<p>Keep the human filename, if you need it at all, in the job record with a short time to live. Don't put it in the object name. Don't copy the bucket to a second continent "for safety" if your success condition is delete. You'd be paying to replicate data you intend to throw away.</p>
<p>Cap how many files one visit can add. A hard limit, such as five files before the person has to start a new visit, bounds storage, key operations, and queue size. Unlimited upload is how a print counter becomes a junk drawer.</p>
<h2 id="heading-encrypt-for-storage-and-be-honest-about-decrypt">Encrypt for Storage, and Be Honest About Decrypt</h2>
<p>Two different jobs get mixed up in privacy posts:</p>
<ul>
<li><p><strong>Zero-knowledge</strong> means the server never has the plaintext or the key.</p>
</li>
<li><p><strong>Short retention</strong> means the server doesn't keep the file as a library.</p>
</li>
</ul>
<p>A printer, a preview, or a "download this once" button usually needs the second, not the first. Someone authorized has to see the bytes. Say that in the article and in the product. Don't write "the server never sees the file" if your API unwraps it.</p>
<p>Here's a practical pattern:</p>
<ol>
<li><p>The browser encrypts with AES-GCM before upload, so a casual network capture isn't a readable PDF.</p>
</li>
<li><p>On the server, unwrap that layer, check the magic bytes (PDF, JPEG, PNG), then encrypt again with a new data key for storage.</p>
</li>
<li><p>Ask Cloud KMS (or your cloud's key service) to wrap that data key. Store the wrapped key next to the job, not in the object name.</p>
</li>
<li><p>On an authorized read, unwrap the data key, decrypt, and stream.</p>
</li>
</ol>
<p>Checking magic bytes is why you unwrap on the way in. If you skip that, people upload HTML or junk and you find out at the worst moment.</p>
<p>Here's a sketch, not a full crypto library:</p>
<pre><code class="language-js">const crypto = require("crypto");

function encryptForStorage(plainBuffer) {
  const dataKey = crypto.randomBytes(32);
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", dataKey, iv);
  const ciphertext = Buffer.concat([cipher.update(plainBuffer), cipher.final()]);
  const tag = cipher.getAuthTag();
  return { ciphertext, iv, tag, dataKey };
}
</code></pre>
<p>Send <code>dataKey</code> to KMS and persist only the wrapped result. AES-GCM's auth tag matters: if someone flips bits in the bucket, decrypt fails instead of returning a corrupted PDF that looks half-real.</p>
<p>KMS calls are cheap next to a bucket that never expires. Skipping encryption to save pennies is how a leaked bucket becomes a leaked neighbourhood.</p>
<p>The person at the counter still sees the page. Paper can be photographed. Architecture removes the forever-copy. It doesn't make the printer blind.</p>
<h2 id="heading-put-the-queue-in-memory-that-expires">Put the Queue in Memory That Expires</h2>
<p>Redis holds the live job, not the file.</p>
<pre><code class="language-js">const JOB_TTL_SECONDS = 60 * 60;

async function saveJob(redis, job) {
  await redis.set(
    `job:${job.id}`,
    JSON.stringify({
      id: job.id,
      objectName: job.objectName,
      shopId: job.shopId,
      status: "queued",
      createdAt: Date.now()
    }),
    "EX",
    JOB_TTL_SECONDS
  );
}
</code></pre>
<p>One hour is a product choice, not a law. Pick a TTL that matches the real visit. A photocopy doesn't need a row you can query next year.</p>
<p>Also store:</p>
<ul>
<li><p>A short cache of file metadata</p>
</li>
<li><p>A counter for "files in this visit"</p>
</li>
<li><p>A lock key so two Cloud Run instances don't run cleanup at the same time</p>
</li>
</ul>
<p>Don't store the PDF in Redis. Redis is what is in line. Cloud Storage is the bytes. When the TTL hits zero, the job record disappears even if you forgot to delete it yourself. That's the point.</p>
<h2 id="heading-delete-on-purpose-then-sweep-what-you-missed">Delete on Purpose, Then Sweep What You Missed</h2>
<p>When the authorized caller finishes:</p>
<pre><code class="language-js">async function completeJob({ storage, redis, bucket, job }) {
  await storage.bucket(bucket).file(job.objectName).delete({ ignoreNotFound: true });
  await redis.del(`job:${job.id}`);
}
</code></pre>
<p>Delete the object first, then the pointer. If you delete the pointer and the object delete fails, you can lose the only map to an orphan file. If the object delete fails, keep the job and retry.</p>
<p>People close tabs. Requests die. A sweeper should run every few minutes:</p>
<ul>
<li><p>If the job is already done, delete anything left.</p>
</li>
<li><p>If the object is older than your window (about 30 minutes is enough for a stuck job), delete it.</p>
</li>
<li><p>Take a Redis lock so two instances don't double-delete.</p>
</li>
</ul>
<pre><code class="language-js">async function withLock(redis, fn) {
  const ok = await redis.set("lock:sweep", "1", "NX", "EX", 55);
  if (ok !== "OK") return;
  try {
    await fn();
  } finally {
    await redis.del("lock:sweep");
  }
}
</code></pre>
<p>Firestore, Postgres, or any other database can keep the shop, the owner, and a usage counter. It shouldn't keep a second copy of the PDF. If the file bytes and the customer list live in the same table, you'll grow a content system. Usage can say "this visit happened." It shouldn't be a thumbnail gallery.</p>
<h2 id="heading-what-not-to-claim">What Not to Claim</h2>
<p>Write these limits into the README so you don't have to walk them back later:</p>
<ul>
<li><p>The server can see plaintext at ingest (to re-encrypt and validate) and at the authorized download. That's not zero-knowledge.</p>
</li>
<li><p>A deleted object is gone from your bucket. It's not gone from the recipient's screen, printer, or camera.</p>
</li>
<li><p>Redis going down should stop new jobs. Serving a fake local queue is worse than a 503.</p>
</li>
<li><p>A random object name isn't access control. You still need an auth check before streaming bytes.</p>
</li>
</ul>
<h2 id="heading-where-to-go-next">Where to Go Next</h2>
<p>Build the smallest loop on one Cloud Run service:</p>
<ol>
<li><p><code>POST /jobs</code> writes ciphertext and a Redis record with <code>EX</code>.</p>
</li>
<li><p><code>GET /jobs/:id</code> checks auth, streams plaintext, then deletes.</p>
</li>
<li><p>A sweep removes anything older than your window.</p>
</li>
<li><p><code>/health</code> returns 503 when Redis is down.</p>
</li>
</ol>
<p>If that loop is boring, you did it right. Boring is what keeps a five-minute file from becoming a document company.</p>
<p>I use this pattern in Docshy, a print relay I build in India: one Cloud Run service in Mumbai (<code>asia-south1</code>), Cloud Storage, Cloud KMS, and Redis.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
