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.

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.
Google documents these numbers on its functions comparison, request timeout and memory limits pages.
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.
Run in your terminal to get started locally.
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 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.
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.
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.
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.
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.
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.
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.