How Lambda and Cloud Run differ on concurrency per instance, timeouts, memory, packaging, billing and event triggers, which workloads suit each, and how Encore runs a backend as Cloud Run services from TypeScript or Go code.

AWS Lambda made it normal to deploy a backend without thinking about servers, one function per route or event, and it is still the default answer on AWS for glue code and event handlers. Google Cloud Run comes at the same promise from the container side, so the comparison usually comes up when a team is choosing a cloud, or when a Lambda-based API starts paying for one execution environment per concurrent request and wonders whether a container that serves many at once would fit better.
This guide compares the two on concurrency, limits, packaging, billing and triggers, and shows how Encore runs a backend on Cloud Run from the TypeScript or Go code that defines it.
Encore is infrastructure from code for TypeScript and Go: services, databases, secrets and schedules are declared in the application, and on deploy the Encore platform creates them in your own GCP project, with each service running on Cloud Run under its own service account, as the GCP infrastructure docs describe. Each service is a long-running process with its own connection pool, which suits the APIs that tend to outgrow one-environment-per-request on Lambda.

Run in your terminal to get started locally.
A currency rates service is a typical Lambda candidate that ends up as two functions, one behind API Gateway for lookups and one on a schedule to refresh the data. In Encore both live in one service, declared with an encore.service.ts file in the rates directory:
// rates/encore.service.ts
import { Service } from "encore.dev/service";
export default new Service("rates");
The lookup endpoint, the refresh job, the database and the provider key then sit in one file:
// rates/rates.ts
import { api, APIError } from "encore.dev/api";
import { secret } from "encore.dev/config";
import { CronJob } from "encore.dev/cron";
import { SQLDatabase } from "encore.dev/storage/sqldb";
// Cloud SQL on GCP, RDS on AWS, local Postgres with `encore run`.
const db = new SQLDatabase("rates", { migrations: "./migrations" });
// Stored in Secret Manager on GCP; set with `encore secret set --type prod FxApiKey`.
const fxApiKey = secret("FxApiKey");
interface Rate {
quote: string;
rate: number;
fetchedAt: string;
}
// Many concurrent lookups share one instance and its connection pool.
export const get = api(
{ expose: true, method: "GET", path: "/rates/:quote" },
async ({ quote }: { quote: string }): Promise<Rate> => {
const row = await db.queryRow<{ quote: string; rate: number; fetched_at: Date }>`
SELECT quote, rate, fetched_at FROM rates WHERE quote = ${quote.toUpperCase()}
`;
if (!row) throw APIError.notFound(`no rate for ${quote}`);
return { quote: row.quote, rate: row.rate, fetchedAt: row.fetched_at.toISOString() };
},
);
// Pulls the latest EUR rates. Upserts, so running it twice is harmless.
export const refresh = api({ expose: false }, async (): Promise<{ updated: number }> => {
const res = await fetch("https://fx.example.com/v1/latest?base=EUR", {
headers: { Authorization: `Bearer ${fxApiKey()}` },
});
if (!res.ok) throw APIError.unavailable(`fx provider returned ${res.status}`);
const body = (await res.json()) as { rates: Record<string, number> };
let updated = 0;
for (const [quote, rate] of Object.entries(body.rates)) {
await db.exec`
INSERT INTO rates (quote, rate, fetched_at) VALUES (${quote}, ${rate}, now())
ON CONFLICT (quote) DO UPDATE SET rate = EXCLUDED.rate, fetched_at = EXCLUDED.fetched_at
`;
updated++;
}
return { updated };
});
// Triggered by the Encore platform; no scheduler resource to create.
const _ = new CronJob("refresh-rates", {
title: "Refresh FX rates",
every: "1h",
endpoint: refresh,
});
The table behind it is a single migration:
-- rates/migrations/1_create_rates.up.sql
CREATE TABLE rates (
quote TEXT PRIMARY KEY,
rate DOUBLE PRECISION NOT NULL,
fetched_at TIMESTAMPTZ NOT NULL
);
encore run starts Postgres locally and applies the migration, and the refresh endpoint can be called by hand from the local dashboard, since cron jobs do not run locally or in preview environments. In the cloud the Encore platform calls refresh on schedule with signed requests and needs no Cloud Scheduler job. The API key is set per environment with encore secret set and kept in Secret Manager, never in the repository, per the secrets docs. Connecting AWS instead of GCP runs the same code on Fargate with RDS, as the AWS infrastructure docs explain.
When a coding agent adds a second scheduled task or a new table, it edits this file and a migration, checks the result against local Postgres, and the next deploy creates whatever the change needs. The own cloud guide covers connecting a GCP project.
Best fit: TypeScript and Go teams on GCP who want an API, its database and its scheduled work running as Cloud Run services in their own project, defined in one codebase they can run locally.
Lambda runs a handler in response to an event: an HTTP request through API Gateway or a function URL, a message from SQS, an object written to S3, a DynamoDB stream record or a schedule. The Lambda quotas page lists a 15-minute timeout, 128 MB to 10,240 MB of memory with CPU allocated in proportion, zip packages up to 250 MB unzipped or container images up to 10 GB, and 1,000 concurrent executions per region by default.
The default compute type gives each execution environment one invocation at a time, so 200 simultaneous requests means 200 environments, each with its own cold start and its own database connection. AWS added Lambda Managed Instances, which run functions on EC2 instances in your account and allow several concurrent invocations per environment, aimed at steady, high-volume workloads. Billing on the default type counts requests and duration by configured memory, measured in milliseconds, which is cheap for spiky, short handlers and less so for APIs that spend most of their time waiting on a database.
Lambda's strength is its trigger catalog: event source mappings for SQS and streams, S3 notifications and EventBridge rules connect a function to almost anything in AWS without a running server, at the cost of a backend split into many functions, each with its own IAM role, environment variables, timeout and memory setting, usually described in SAM, CDK or Terraform.
Best fit: Event handlers and glue code on AWS, short tasks triggered by S3, SQS or streams, and traffic that is idle most of the day.
Cloud Run runs any container that listens on $PORT, scales instances with traffic and can scale to zero. One instance serves many requests at once: the concurrency docs give a default of 80 per instance in the console, 80 times the number of vCPUs when deployed with gcloud or Terraform, and a maximum of 1,000. Requests time out after 5 minutes by default and can be extended to 60 minutes, per the request timeout docs, and instances take up to 32 GiB of memory according to the memory limits page.
With request-based billing, the first of two billing modes, you pay for instance time only while requests are being processed, and concurrent requests on one instance share that time. Instance-based billing charges for the whole lifetime of an instance and keeps CPU available between requests, which Google recommends for background work, as the billing settings docs explain.
Cloud Run receives events over HTTP, from Pub/Sub push subscriptions, Eventarc or Cloud Scheduler, and work that does not serve requests runs as Cloud Run jobs. Each of those connections is a separate resource with its own service account permissions, and a private database means Direct VPC egress and Cloud SQL configuration around the service.
Best fit: HTTP APIs and services with steady or bursty traffic on GCP, anything that needs more than 15 minutes or 10 GB per request, and teams that already ship containers.
Pick Encore when you are building TypeScript or Go services on GCP and want Cloud Run, Cloud SQL, secrets and scheduled jobs set up from the code that uses them, with the same code able to run on Fargate if you connect AWS. Pick Lambda for event handlers and glue on AWS where its native triggers do most of the work and idle time is common. Pick Cloud Run directly when you already build containers for other languages or need full control of the image, the concurrency setting and the billing mode per service.
Lambda runs a function per event, and on its default compute type each execution environment handles one invocation at a time. Cloud Run runs a container that serves HTTP requests, and one instance can handle up to 1,000 concurrent requests, with a default of 80 in the console. Lambda invocations stop at 15 minutes, while Cloud Run requests can run for up to 60 minutes.
Cost follows traffic shape: Lambda bills each invocation and its duration by configured memory, so many concurrent requests mean many billed environments. Cloud Run with request-based billing charges for instance time while requests are being processed, and concurrent requests on one instance share that time, which tends to favor I/O-bound APIs with steady traffic.
Cloud Run services receive events over HTTP, for example from Pub/Sub push subscriptions or Eventarc triggers, and Cloud Run jobs handle batch work that does not serve requests. Lambda still has the widest set of native AWS triggers, such as S3, DynamoDB Streams and SQS event source mappings.
With Encore, declare the database with new SQLDatabase, a secret with secret("FxApiKey") and a schedule with new CronJob in TypeScript, then connect your GCP project. Encore deploys each service to Cloud Run with its own service account, provisions Cloud SQL and Secret Manager, and triggers cron jobs from 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.