Serverless Workers on GCP Cloud Run - .NET SDK
On a GCP Cloud Run worker pool, you run a standard long-lived Temporal Worker. Register Workflows and Activities the same way you would with any other .NET Worker, and Temporal Cloud scales the pool up and down as work arrives and drains.
A Cloud Run Worker needs no Cloud Run-specific runtime or handler. The one addition to a standard Worker is Worker Versioning, which is required for Serverless Workers.
For the end-to-end deployment guide covering the Worker Pool, IAM, and compute configuration, see Deploy a Serverless Worker on GCP Cloud Run.
Create a versioned Worker
Build the Worker as you would any long-running .NET Worker, then set DeploymentOptions on TemporalWorkerOptions to declare the Worker Deployment Version and turn versioning on.
The following Worker reads its connection settings and Task Queue from the environment, so the same image can run against any Namespace:
using Temporalio.Client;
using Temporalio.Common;
using Temporalio.Worker;
var client = await TemporalClient.ConnectAsync(
new(Environment.GetEnvironmentVariable("TEMPORAL_ADDRESS")!)
{
Namespace = Environment.GetEnvironmentVariable("TEMPORAL_NAMESPACE")!,
ApiKey = Environment.GetEnvironmentVariable("TEMPORAL_API_KEY"),
Tls = new(),
});
var options = new TemporalWorkerOptions(
Environment.GetEnvironmentVariable("TEMPORAL_TASK_QUEUE")!)
{
DeploymentOptions = new(new("my-app", "build-1"), useWorkerVersioning: true)
{
DefaultVersioningBehavior = VersioningBehavior.Pinned,
},
};
options.AddWorkflow<GreetingWorkflow>();
options.AddAllActivities(typeof(GreetingActivities), null);
using var worker = new TemporalWorker(client, options);
await worker.ExecuteAsync(CancellationToken.None);
The two arguments to WorkerDeploymentVersion are the deployment name and the build ID, and together they identify the Worker Deployment Version. Both values must match the version you create with temporal worker deployment create-version in the deployment guide, or the Worker polls under a version the WCI does not manage.
Every Workflow needs a versioning behavior, either Pinned or AutoUpgrade.
Setting DefaultVersioningBehavior as shown above covers every Workflow on the Worker.
To set the behavior per Workflow instead, set VersioningBehavior on the Workflow attribute:
using Temporalio.Common;
using Temporalio.Workflows;
[Workflow(VersioningBehavior = VersioningBehavior.Pinned)]
public class GreetingWorkflow
{
[WorkflowRun]
public async Task<string> RunAsync(string name) => // ...
}
For general Worker setup and options that are not specific to Cloud Run, see Run a Worker.
Configure the Temporal connection
Read the Namespace, address, and Task Queue from environment variables you set on the Worker Pool, and mount the Temporal Cloud API key or TLS material from Secret Manager rather than passing it in plaintext.
The Worker above reads TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, TEMPORAL_API_KEY, and TEMPORAL_TASK_QUEUE, so the same image can run against any Namespace.
To load those values through the shared configuration format instead of reading them yourself, use ClientEnvConfig.LoadClientConnectOptions() from the Temporalio.Common.EnvConfig namespace.
For the full list of supported variables, the config file format, and profiles, see Environment configuration.
Package the Worker image
Publish the Worker and run it on a .NET runtime image:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /out
FROM mcr.microsoft.com/dotnet/runtime:9.0
WORKDIR /app
COPY /out ./
CMD ["dotnet", "MyWorker.dll"]
The Worker runs on a Rust core that reads TLS roots from the operating system's certificate store, so the runtime image must include one.
The Debian-based mcr.microsoft.com/dotnet/runtime images do.
Keep Activities safe across scale-in
The WCI decides when to remove an instance from Task Queue activity, not from what an individual instance is doing. An instance running a long Activity can be stopped mid-execution.
Use Activity Heartbeats so a retry resumes from the last recorded progress instead of starting over:
[Activity]
public static string Process(IReadOnlyList<string> items)
{
for (var i = 0; i < items.Count; i++)
{
ActivityExecutionContext.Current.Heartbeat(i);
// ... process items[i]
}
return "done";
}
For how scale-in decisions are made, see Serverless Workers on GCP Cloud Run.
Configure OpenTelemetry
Configure the .NET OpenTelemetry exporter and the Temporal Runtime to send telemetry by OTLP to localhost:4317. The following code sample configures the Worker only. You must separately run an OTLP-compatible receiver at that address. In a Cloud Run Worker Pool, run that receiver as a sidecar.
src/OpenTelemetry/CoreSdkForwarding/Program.cs
var resourceBuilder = ResourceBuilder.
CreateDefault().
AddService("TemporalioSamples.OpenTelemetry", serviceInstanceId: instanceId);
using var tracerProvider = Sdk.
CreateTracerProviderBuilder().
SetResourceBuilder(resourceBuilder).
AddSource(TracingInterceptor.ClientSource.Name, TracingInterceptor.WorkflowsSource.Name, TracingInterceptor.ActivitiesSource.Name).
AddOtlpExporter().
Build();
// Shared by the client and by Core SDK log forwarding below. The OpenTelemetry provider exports
// logs to the dashboard alongside the traces and metrics.
using var loggerFactory = LoggerFactory.Create(builder =>
builder.
AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ").
AddOpenTelemetry(options =>
{
options.SetResourceBuilder(resourceBuilder);
options.IncludeFormattedMessage = true;
options.IncludeScopes = true;
options.AddOtlpExporter();
}).
SetMinimumLevel(LogLevel.Information));
// Create a client to localhost on default namespace
var connectOptions = ClientEnvConfig.LoadClientConnectOptions();
connectOptions.TargetHost ??= "localhost:7233";
connectOptions.LoggerFactory = loggerFactory;
connectOptions.Interceptors = new[] { new TracingInterceptor() };
connectOptions.Runtime = new TemporalRuntime(new TemporalRuntimeOptions()
{
Telemetry = new TelemetryOptions()
{
Metrics = new MetricsOptions()
{
OpenTelemetry = new OpenTelemetryOptions()
{
Url = new Uri("http://localhost:4317"),
},
},
Logging = new LoggingOptions()
{
// Core SDK logs default to WARN; lowered here so there is more to see.
Filter = new TelemetryFilterOptions(core: TelemetryFilterOptions.Level.Info),
// The Core SDK writes its logs to the console itself unless Forwarding is set, in
// which case they go to this ILogger instead.
Forwarding = new LogForwardingOptions(loggerFactory.CreateLogger("Temporalio.Core")),
},
},
});
var client = await TemporalClient.ConnectAsync(connectOptions);
The OpenTelemetry sample shows the tracing, metrics, and log-export configuration. Its Docker Compose file runs the .NET Aspire Dashboard locally and exposes its OTLP endpoint on port 4317; it does not define a Cloud Run sidecar. Configure your Cloud Run sidecar to export the received telemetry to your backend.
Set a Worker identity
Use WorkerIdPlugin to identify each Worker instance as <instance-id>@<revision>. The plugin reads Cloud Run environment variables and instance metadata when the Client connects, then Workers created from that Client inherit the identity.
src/Gcp/CloudRun/WorkerId/Program.cs
var address = GetEnvironmentVariable("TEMPORAL_ADDRESS") ?? "localhost:7233";
var temporalNamespace = GetEnvironmentVariable("TEMPORAL_NAMESPACE") ?? "default";
var taskQueue = GetEnvironmentVariable("TEMPORAL_TASK_QUEUE") ?? "cloud-run-worker-sample";
using var loggerFactory = LoggerFactory.Create(builder => builder.
AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ").
SetMinimumLevel(LogLevel.Information));
var logger = loggerFactory.CreateLogger("CloudRunWorkerId");
// Register the Cloud Run plugin once on the client. At connect time it reads the Cloud Run instance
// id from the metadata server, and the worker pool / service name and revision from the environment,
// then sets the client Identity to the worker identity "{instanceId}@{revision}" (unless one was
// already configured). Every worker created from this client inherits that identity. The plugin only
// sets the worker identity; it does not configure anything else.
//
// NOTE: this requires the process to be running on a Cloud Run worker pool or service. Running it
// elsewhere throws at connect time because the metadata server is unreachable.
var clientOptions = new TemporalClientConnectOptions(address)
{
Namespace = temporalNamespace,
LoggerFactory = loggerFactory,
Plugins = new[] { new WorkerIdPlugin() },
};
var client = await TemporalClient.ConnectAsync(clientOptions);
The plugin requires the Cloud Run metadata server, so it fails when the Worker runs outside Cloud Run. For a complete example, see the .NET Cloud Run Worker Id sample.