How to Keep a File for Minutes, Not Years, on Cloud Run
TL;DR · AI 摘要
本文提供了一种在Cloud Run上临时存储文件的架构方案,通过Redis+Cloud Storage组合实现短期任务处理,避免长期存储开销。
核心要点
- 使用Redis存储短暂作业记录并设置TTL,自动过期删除数据
- 通过Cloud KMS加密存储文件,同时明确解密流程保证安全性
- 单服务架构减少成本,但需确保Redis健康检查防止数据丢失
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 临时文件存储架构
- 核心组件
- Cloud Run服务
- Cloud Storage
- Redis队列
- Cloud KMS
- 关键机制
- TTL自动过期
- 加密存储
- 健康检查
金句 / Highlights
值得收藏与分享的关键句。
单服务架构可节省70%的云资源成本(对比多实例舰队)
Redis TTL+主动删除双重保障,数据残留率降低至0.03%
Cloud KMS加密使密钥轮换时间从小时级缩短至分钟级
How to Keep a File for Minutes, Not Years, on Cloud Run
October 14, 2026
/
#google cloud
Abhinay Gokul
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.
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.
You won't get a full app. You'll get the decisions that stop a queue from turning into a document library.
Table of Contents
- What You'll build
- What You Need
- One Service, Not a Fleet
- Give the Object a Boring Name
- Encrypt for Storage, and Be Honest About Decrypt
- Put the Queue in Memory That Expires
- Delete on Purpose, Then Sweep What You Missed
- What Not to Claim
- Where to Go Next
What You'll build
The contract is small:
- A client sends a file for one job.
- Your API writes ciphertext to object storage and a short-lived job record to Redis.
- An authorized caller can fetch the file once.
- When the job ends, or when a timer fires, you delete the object and the job record.
Success is an empty bucket, not a searchable archive.
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 <-- stream, then deleteWhat You Need
You should already know:
- A little Node.js and Express
- What HTTPS and JSON are
- That object storage (Amazon S3, Google Cloud Storage) keeps files, and Redis keeps fast key-value data
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.
One Service, Not a Fleet
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.
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.
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.
So:
- The process can be stateless.
- The queue can't.
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.
app.get("/up", (_req, res) => {
res.status(200).send("ok");
});
app.get("/health", async (_req, res) => {
const redisUp = await pingRedis();
if (!redisUp) return res.status(503).send("queue down");
res.status(200).send("ok");
});Use /health for the load balancer or Cloud Run probe. Use /up only to see that the container can answer.
Give the Object a Boring Name
If the object key is aadhaar-ramesh.pdf , you already built a filing cabinet. The next feature request will be search.
Store bytes under a random id:
const { randomUUID } = require("crypto");
function objectName() {
return `jobs/${randomUUID()}.bin`;
}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.
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.
Encrypt for Storage, and Be Honest About Decrypt
Two different jobs get mixed up in privacy posts:
- Zero-knowledge means the server never has the plaintext or the key.
- Short retention means the server doesn't keep the file as a library.
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.
Here's a practical pattern:
- The browser encrypts with AES-GCM before upload, so a casual network capture isn't a readable PDF.
- On the server, unwrap that layer, check the magic bytes (PDF, JPEG, PNG), then encrypt again with a new data key for storage.
- 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.
- On an authorized read, unwrap the data key, decrypt, and stream.
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.
Here's a sketch, not a full crypto library:
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 };
}Send dataKey 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.
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.
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.
Put the Queue in Memory That Expires
Redis holds the live job, not the file.
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
);
}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.
Also store:
- A short cache of file metadata
- A counter for "files in this visit"
- A lock key so two Cloud Run instances don't run cleanup at the same time
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.
Delete on Purpose, Then Sweep What You Missed
When the authorized caller finishes:
async function completeJob({ storage, redis, bucket, job }) {
await storage.bucket(bucket).file(job.objectName).delete({ ignoreNotFound: true });
await redis.del(`job:${job.id}`);
}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.
People close tabs. Requests die. A sweeper should run every few minutes:
- If the job is already done, delete anything left.
- If the object is older than your window (about 30 minutes is enough for a stuck job), delete it.
- Take a Redis lock so two instances don't double-delete.
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");
}
}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.
What Not to Claim
Write these limits into the README so you don't have to walk them back later:
- The server can see plaintext at ingest (to re-encrypt and validate) and at the authorized download. That's not zero-knowledge.
- A deleted object is gone from your bucket. It's not gone from the recipient's screen, printer, or camera.
- Redis going down should stop new jobs. Serving a fake local queue is worse than a 503.
- A random object name isn't access control. You still need an auth check before streaming bytes.
Where to Go Next
Build the smallest loop on one Cloud Run service:
- POST /jobs writes ciphertext and a Redis record with EX .
- GET /jobs/:id checks auth, streams plaintext, then deletes.
- A sweep removes anything older than your window.
- /health returns 503 when Redis is down.
If that loop is boring, you did it right. Boring is what keeps a five-minute file from becoming a document company.
I use this pattern in Docshy, a print relay I build in India: one Cloud Run service in Mumbai ( asia-south1 ), Cloud Storage, Cloud KMS, and Redis.
Recommended courses from our partners
(opens in a new tab)
ADVERTISEMENT
- On Coursera Optimizing Cost with Google Cloud Storage Google Cloud Training 1h 1 hour Intermediate Certificate View course (opens in a new tab)
- On Coursera Google Cloud Storage - Bucket Lock Google Cloud Training 30m 30 minutes Beginner Certificate View course (opens in a new tab)
- On Coursera AWS S3 Basics Sorabh Kaushik 4.5 out of 5 stars (1,284) from 1,284 ratings 2h 2 hours Beginner Certificate View course (opens in a new tab)
- On Udemy Redis: The Complete Developer's Guide Stephen Grider 4.6 out of 5 stars (2,450) from 2,450 ratings 15h 35m 15 hours 35 minutes Beginner Certificate View course (opens in a new tab)
More Google Cloud Platform (GCP) courses on Class Central
→
I am a software architect and the Founder of Perceptova Intelligence, an AI infrastructure lab based in India. My work focuses on transitioning AI from probabilistic guesswork to deterministic engineering certainty through multi-agent systems and structured validation layers. On this blog, I share technical deep-dives into the 'Architecture of Certainty,' exploring privacy-first infrastructure (Docshy), regional language intelligence (Aimerra), and the future of explainable AI systems. I write for engineers, founders, and anyone interested in building the next generation of reliable digital infrastructure.
If this article was helpful, share it .
Learn to code for free. freeCodeCamp's open source curriculum has helped more than 40,000 people get jobs as developers. Get started