Nov 13, 20267 min read

Cloud Run vs Cloud Functions: Services or Functions on Google Cloud?

How Cloud Run services and Cloud Run functions (formerly Cloud Functions) differ on packaging, triggers, limits and project structure, and how Encore runs a backend on Cloud Run from code.

Illustration for Cloud Run vs Cloud Functions: Services or Functions on Google Cloud?

Cloud Functions was Google Cloud's answer to Lambda: write one function, pick a trigger, deploy, and never touch a container. In 2024 it was renamed Cloud Run functions, and current functions are built from source into containers and deployed as Cloud Run services, so the question is no longer which platform runs your code but how you want to package it. One function per handler is pleasant for the first three, and it gets harder when twenty functions share a database schema, a set of secrets and a deploy order.

This guide compares Cloud Run services and Cloud Run functions on packaging, triggers, limits and project structure, and shows how Encore runs a backend with endpoints, Pub/Sub and a scheduled job as Cloud Run services from TypeScript code.

Cloud Run vs Cloud Functions at a glance

Encore on GCPCloud Run functionsCloud Run services
Unit of deploymentTypeScript or Go servicesOne function per entry pointOne container per service
PackagingBuilt from your code on deploySource built with buildpacks and the Functions FrameworkAny container image, or source
Request timeoutCloud Run request timeoutUp to 60 min for HTTP, 9 min for event-driven (v2 API)5 min default, up to 60 min
MemoryCloud Run limitsUp to 16 GiB with 4 vCPUUp to 32 GiB with 8 vCPU
ConcurrencyCloud Run concurrencyUp to 1,000 per instance (1 on 1st gen)Up to 1,000 per instance
Event triggersPub/Sub subscriptions declared in codeEventarc and Pub/Sub triggers set at deployEventarc or Pub/Sub push you configure
DatabaseCloud SQL provisioned from codeCloud SQL you connectCloud SQL you connect
Local developmentencore run with Postgres and Pub/SubFunctions Framework locallyRun the container locally

Google documents these numbers on its functions comparison, request timeout and memory limits pages.

How does Encore run a backend on Cloud Run?

Encore

Encore takes the infrastructure-from-code route: the endpoints, topics, subscriptions, schedules and databases a backend needs are declared in TypeScript or Go, and Encore deploys each service to Cloud Run and provisions the rest in your own GCP project. What would be three functions, an HTTP function, a Pub/Sub-triggered function and a scheduled one, becomes one service with one deploy and shared database access.

Set up the app

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create invoices-app --example=ts/hello-world$ cd invoices-app && mkdir -p invoices/migrations

The migration creates the invoices table, with emailed_at used to make the subscriber safe to run twice:

-- invoices/migrations/1_create_invoices.up.sql CREATE TABLE invoices ( id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL, amount_cents BIGINT NOT NULL CHECK (amount_cents > 0), due_date DATE NOT NULL, status TEXT NOT NULL DEFAULT 'open', emailed_at TIMESTAMPTZ );

The service holds all three entry points: create is a public HTTP endpoint, the subscription reacts to new invoices, and markOverdue is an internal endpoint called by a cron job every morning:

// invoices/invoices.ts import { api, APIError } from "encore.dev/api"; import { Topic, Subscription } from "encore.dev/pubsub"; import { CronJob } from "encore.dev/cron"; import { SQLDatabase } from "encore.dev/storage/sqldb"; import log from "encore.dev/log"; // Cloud SQL for PostgreSQL on GCP, Postgres from `encore run` locally. const db = new SQLDatabase("invoices", { migrations: "./migrations" }); interface InvoiceCreated { invoiceId: number; email: string; } // Google Pub/Sub on GCP, in memory locally. export const invoiceCreated = new Topic<InvoiceCreated>("invoice-created", { deliveryGuarantee: "at-least-once", }); interface CreateInvoice { email: string; amountCents: number; dueDate: string; // YYYY-MM-DD } // HTTP endpoint: what would be an HTTP-triggered function. export const create = api( { expose: true, method: "POST", path: "/invoices" }, async (req: CreateInvoice): Promise<{ id: number }> => { if (req.amountCents <= 0) throw APIError.invalidArgument("amountCents must be positive"); const row = await db.queryRow<{ id: number }>` INSERT INTO invoices (email, amount_cents, due_date) VALUES (${req.email}, ${req.amountCents}, ${req.dueDate}::date) RETURNING id`; await invoiceCreated.publish({ invoiceId: row!.id, email: req.email }); return { id: row!.id }; }, ); // Subscriber: what would be an event-driven function on a Pub/Sub trigger. const _ = new Subscription(invoiceCreated, "send-invoice-email", { handler: async (event) => { // Only the first delivery flips the flag, so retries do not send twice. const claimed = await db.queryRow<{ id: number }>` UPDATE invoices SET emailed_at = now() WHERE id = ${event.invoiceId} AND emailed_at IS NULL RETURNING id`; if (!claimed) return; log.info("sending invoice", { to: event.email, invoiceId: event.invoiceId }); }, }); // Scheduled job: what would be Cloud Scheduler calling a function. export const markOverdue = api({}, async (): Promise<void> => { await db.exec` UPDATE invoices SET status = 'overdue' WHERE status = 'open' AND due_date < current_date`; }); const __ = new CronJob("mark-overdue", { title: "Mark unpaid invoices overdue", schedule: "0 6 * * *", endpoint: markOverdue, });

encore run starts Postgres and an in-memory Pub/Sub locally, applies the migration, and delivers the event to the subscriber in the same process, so the whole flow runs on a laptop without emulators. Cron jobs do not run locally or in preview environments, so markOverdue is tested by calling it from the local development dashboard.

On deploy, Encore builds the service into a container, deploys it to Cloud Run with its own service account, provisions Cloud SQL and Pub/Sub, and grants the service account access to the resources it uses, as described in the GCP infrastructure docs. Subscriptions are delivered to the Cloud Run service as Pub/Sub push subscriptions, and the Encore platform runs the cron job with no extra cloud infrastructure. The Pub/Sub docs cover retries and dead-lettering.

With the trigger, the handler and the database in one codebase, a coding agent adding a "payment received" event writes the topic, the subscriber and the migration in one change and runs them together under encore run, rather than creating a new function and its trigger in the console.

Best fit: GCP backends with several endpoints, background work and a shared database that should run on Cloud Run in your own project, with everything declared in code.

Cloud Run functions

Cloud Run functions are the right size for single-purpose handlers. You write a function against the Functions Framework, deploy it from source, and attach a trigger such as an HTTP URL, a Pub/Sub topic or an Eventarc event from Cloud Storage or Firestore. Google builds the container, so there is no Dockerfile, and since functions now run as Cloud Run services they get concurrency up to 1,000, traffic splitting and Direct VPC egress.

As the count grows, each function is still deployed separately, with its own runtime settings, service account and environment variables, so shared code is copied or published as a package, and a schema change that touches four functions means four coordinated deploys. Event-driven functions created through the Cloud Functions v2 API are capped at 9 minutes, and Cloud SQL, Pub/Sub topics and secrets are still created and permissioned outside the function, usually in Terraform.

Best fit: small, single-purpose handlers reacting to GCP events, where one function per job is the whole application.

Cloud Run services

A Cloud Run service runs any container listening on a port, so an Express, NestJS, Gin or chi app runs unchanged, with up to 1,000 concurrent requests per instance, a timeout of up to 60 minutes and up to 32 GiB of memory. You write the Dockerfile or use source deploys, and for events you create a Pub/Sub push subscription or an Eventarc trigger that targets the service, with an invoker service account for each.

That suits a full backend better than a pile of functions, and it leaves the surrounding setup to you: Cloud SQL with the connector or Direct VPC egress, Secret Manager entries mounted into the service, Pub/Sub topics and subscriptions with dead-letter topics, Cloud Scheduler jobs for schedules, and IAM bindings between them.

Best fit: containerized apps you already have, where the team is happy to manage the GCP resources around them in Terraform.

How to choose

Pick Encore when you want a GCP backend on Cloud Run with endpoints, Pub/Sub, cron jobs and Cloud SQL declared in TypeScript or Go and provisioned in your own project, with the same setup running locally. Pick Cloud Run functions for a handful of single-purpose event handlers. Pick plain Cloud Run services when you have an existing container and are comfortable owning the infrastructure around it.

Frequently asked questions

What is the difference between Cloud Run and Cloud Functions?

Cloud Functions has been renamed Cloud Run functions, and the current version deploys each function as a Cloud Run service built from source code with the Functions Framework. A Cloud Run service runs any container you give it. The functions model gives you one entry point per function and event triggers wired at deploy time, while a service is a whole HTTP application in one container.

Is Cloud Functions being replaced by Cloud Run?

In August 2024 Google renamed Cloud Functions to Cloud Run functions, and functions are managed through Cloud Run. The original version continues as Cloud Run functions (1st gen), with one concurrent request per instance, up to 9 minutes per call and up to 8 GB of memory.

When should I use Cloud Run functions instead of a Cloud Run service?

Functions fit small, single-purpose handlers such as reacting to a Cloud Storage upload or a Firestore change, where writing a function and choosing a trigger is all you want to manage. Once you have several related endpoints, shared database access and background work, a service keeps that code in one application with one deploy.

How do I deploy a backend with HTTP endpoints, Pub/Sub and cron jobs to Cloud Run?

With Encore, define the endpoints with api(), the topic and subscriber with Topic and Subscription, and the schedule with CronJob in TypeScript, then connect your GCP project and push. Encore deploys the service to Cloud Run and provisions Cloud SQL and Pub/Sub in your project, and the cron job is managed by the Encore platform.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders