Migrate to RunxBuild today!! Deploy More while Spending Less

Calculate your savings
unxBuild

Cloud Functions on GCP: A Practical Cloud Run Functions Tutorial

Sean

Platform Writer

Sep 30, 2026
11 min read

Cloud Functions on GCP is Google’s function-as-a-service, and today it is branded Cloud Run functions: you write a single function, deploy it with gcloud functions deploy —gen2, and Google runs it on Cloud Run infrastructure in response to an HTTP request, a Pub/Sub message or a Cloud Storage event.

Cloud Functions on GCP: A Practical Cloud Run Functions Tutorial

The naming changed, the commands mostly did not, and a lot of tutorials mix the old first generation with the current second. This walkthrough sticks to gen2: what it is, the three triggers you will actually use, how to deploy and configure a function, what cold starts and timeouts do to you, and the point at which a plain long-running service is the better tool.

Table of contents

Cloud Functions, Cloud Run functions and gen2: untangling the names

Google’s product page for Cloud Run functions describes it as a way to treat Google and third-party services as building blocks and extend them with code. The product used to be called Cloud Functions, and the second generation was built directly on Cloud Run and Eventarc. In practice the two names now refer to the same lineage, and the commands still say functions.

The first generation is the legacy runtime. It still exists, but new projects should use gen2 for a simple reason: a gen2 function is really a Cloud Run service with a function-shaped wrapper. You get larger instance sizes, longer request timeouts, concurrent requests on one instance and the same underlying infrastructure as Cloud Run, which is what everything else in Google’s serverless line converges on.

A function is one entry point in your source. You do not manage a server, a container or a port. You export a handler, choose a runtime such as Node.js, Python, Go or Java, choose a trigger, and deploy. Google builds the container for you from source. That is the appeal, and also the constraint: you get the shape Google chose, unless you step up to Cloud Run itself. Our GCP overview places the pieces relative to each other.

Your first HTTP function

Start with the trigger everyone uses: HTTP. This function uses the Functions Framework for Node.js, which is what the runtime calls to invoke your code, and it also lets you run the same thing locally.

// index.js
const functions = require('@google-cloud/functions-framework');

functions.http('helloHttp', (req, res) => {
  const name = req.query.name || 'world';
  res.status(200).json({ message: `hello ${name}` });
});
npm init -y && npm install @google-cloud/functions-framework
npx functions-framework --target=helloHttp   # run locally on :8080

gcloud functions deploy hello-http \
  --gen2 \
  --runtime=nodejs22 \
  --region=us-central1 \
  --source=. \
  --entry-point=helloHttp \
  --trigger-http \
  --allow-unauthenticated

The deploy uploads your source, builds it with Cloud Build, and prints a URL. The —allow-unauthenticated flag makes the function public. Leave it off for anything that should require IAM authentication, which is the safer default: a function without it can only be invoked by callers with the invoker role.

Two practical notes. The first deploy asks to enable several APIs, including Cloud Build, Artifact Registry and Cloud Run, and it can take a few minutes because it is building an image. And the runtime version is pinned at deploy time, so a function deployed on an old Node version keeps running on it until you redeploy. Google publishes deprecation dates for runtimes, and putting those in a calendar is worth the effort.

Event triggers: Pub/Sub and Cloud Storage

HTTP is request and response. The other reason to use functions is reacting to events, where something happens in Google Cloud and your code runs because of it. Gen2 delivers those events through Eventarc, and the deploy flags change to describe what to listen for.

A Pub/Sub trigger runs your function for each message published to a topic. The message arrives as a CloudEvent, and the payload is base64-encoded.

functions.cloudEvent('onMessage', (event) => {
  const data = Buffer.from(event.data.message.data, 'base64').toString();
  console.log('received', data);
});
gcloud functions deploy on-message \
  --gen2 --runtime=nodejs22 --region=us-central1 \
  --source=. --entry-point=onMessage \
  --trigger-topic=orders

# Cloud Storage: fire when an object is finalized in a bucket
gcloud functions deploy on-upload \
  --gen2 --runtime=nodejs22 --region=us-central1 \
  --source=. --entry-point=onUpload \
  --trigger-event-filters="type=google.cloud.storage.object.v1.finalized" \
  --trigger-event-filters="bucket=my-uploads-bucket"

Event delivery is at-least-once, which means your function will occasionally see the same event twice. Make the handler idempotent: check whether the work is already done before doing it, or key writes on the event ID. Also note that the bucket must be in the same region as the function, or in a compatible multi-region, or the trigger will not deploy.

Environment variables and secrets

Configuration goes in environment variables, set at deploy time or updated later. Plain settings such as a feature flag or an API base URL are fine as variables. Credentials are not, since anyone who can view the function configuration can read them.

gcloud functions deploy hello-http --gen2 --region=us-central1 --source=. \
  --runtime=nodejs22 --entry-point=helloHttp --trigger-http \
  --set-env-vars=LOG_LEVEL=info,REGION_LABEL=eu \
  --set-secrets=STRIPE_KEY=stripe-key:latest

The set-secrets flag mounts a value from Secret Manager into the function as an environment variable, so the secret lives in one place with its own access control and version history. The function’s service account needs permission to read that secret, and a missing permission is the most common reason a fresh deploy fails at startup with a message that looks unrelated.

Give each function its own service account with only the roles it needs, rather than the default account that often has broad project access. It is a five-minute change that limits the damage if a dependency or an input is ever abused.

Cold starts, concurrency and timeouts

A function that has not run recently has no instance waiting. The first request pays for starting one: pulling the image, booting the runtime and loading your code. That is a cold start, and it ranges from a couple hundred milliseconds for a lean Go function to several seconds for a heavy Java or Python one with large dependencies.

  • Keep dependencies small and initialise expensive clients lazily, once, outside the handler.
  • Set —min-instances=1 to keep a warm instance, at the cost of paying for it while idle.
  • Pick a lightweight runtime for latency-sensitive endpoints.
  • Use —concurrency to let one instance handle several requests at once on gen2.

Timeouts are the other limit. Functions have a default timeout and a configurable maximum, set with the —timeout flag. Because gen2 sits on Cloud Run, the ceiling is far higher than it was in gen1: Google’s Cloud Run documentation gives a default request timeout of 5 minutes for services and a maximum of 60 minutes. Event-driven functions can have tighter caps, so check the current limits for the trigger you use before you design around them.

Retries interact with timeouts in an ugly way. If an event function times out and retries are enabled, it runs again, and a slow job can loop until it eats your budget. Enable retries deliberately, and only on handlers you have made idempotent.

When a long-running service fits better

Functions are excellent for glue: resizing an image on upload, reacting to a queue message, a webhook that writes a row. They are a poor fit for anything with state in memory, persistent connections, background threads that outlive a request, or startup costs you would rather pay once. A WebSocket server, a job that polls, an app with an in-memory cache and a framework such as Express or Django serving many routes all want a process that stays up.

The tell is when you start fighting the platform: keeping instances warm to hide cold starts, stitching many functions together to make an app, or passing state through a database only because the function forgets everything. At that point, the simpler shape is a normal service in a container that starts once and serves requests for days. Google’s own answer is Cloud Run, and we walk through it in gcloud run deploy.

The same reasoning holds outside Google. A Node or Python app deployed as a service from a repository gives you routes, background work, connection pools and predictable latency without dividing the code into handlers.

How this fits the rest of the stack

Before you split an app into functions, price the plain version. The RunxBuild hosting calculator shows what a Node or Python service, plus a managed Postgres or MySQL database, costs on a fixed plan. On RunxBuild a Node service deploys from a GitHub repository with build logs, environment variables, runtime logs and rollback, and it keeps running between requests so there is no cold start to design around. The Node services docs walk through it, or start in the dashboard.

Useful related references:

FAQ

Is Cloud Functions the same as Cloud Run functions?

Yes, in lineage. Google rebranded Cloud Functions as Cloud Run functions, and the second generation is built on Cloud Run and Eventarc. The gcloud functions commands still work, and new projects should use gen2 rather than the legacy first generation.

How do I deploy a Cloud Function with gcloud?

Run gcloud functions deploy NAME with —gen2, —runtime, —region, —source, —entry-point and a trigger flag such as —trigger-http. Google builds the container from your source with Cloud Build and prints the function’s URL when the deployment finishes.

What triggers can a Google Cloud function use?

The common ones are HTTP requests, Pub/Sub messages and Cloud Storage object events, with gen2 delivering events through Eventarc. Other Google Cloud events, such as Firestore changes and audit log entries, are also supported.

How do I reduce cold starts on Cloud Functions?

Keep dependencies small, initialise clients once outside the handler, choose a lightweight runtime, and set a minimum instance count so a warm instance is always available. Minimum instances cost money while idle, so use them for latency-sensitive endpoints only.

When should I use Cloud Run instead of Cloud Functions?

Use Cloud Run when you need a whole web app or API, a custom container, long-lived connections or in-memory state, and functions when you want small single-purpose handlers reacting to events. Gen2 functions run on Cloud Run anyway, so moving over is straightforward.

#cloud functions gcp#Cloud Run functions#gcloud functions deploy#serverless#Pub/Sub trigger