Cloud Functions is a serverless event-driven compute platform, while Cloud Run is a serverless container platform.

Let’s see them in action.

Imagine you have a new image uploaded to a Cloud Storage bucket. You want to automatically generate a thumbnail for it.

Cloud Functions Approach:

  1. Trigger: A Cloud Storage trigger is configured for the bucket. When a new object is created, it fires an event.
  2. Function: A Python Cloud Function listens for this event.
  3. Code: The function’s code uses the Cloud Storage client library to download the new image, an image processing library (like Pillow) to resize it, and then uploads the thumbnail back to a different bucket.
# main.py (Cloud Functions)
from google.cloud import storage
from PIL import Image
import io

storage_client = storage.Client()

def generate_thumbnail(event, context):
    """Triggered by a change to a Cloud Storage bucket."""
    file_data = event
    bucket_name = file_data['bucket']
    file_name = file_data['name']
    content_type = file_data.get('contentType', '')

    if not file_name.endswith(('jpg', 'jpeg', 'png')):
        print(f"Skipping non-image file: {file_name}")
        return

    # Download the original image
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(file_name)
    image_bytes = blob.download_as_bytes()

    # Generate thumbnail
    img = Image.open(io.BytesIO(image_bytes))
    img.thumbnail((128, 128)) # Resize to 128x128

    # Upload the thumbnail
    thumbnail_bucket_name = 'your-thumbnail-bucket' # Replace with your bucket name
    thumbnail_blob_name = f'thumbnails/{file_name}'
    thumbnail_bucket = storage_client.bucket(thumbnail_bucket_name)
    thumbnail_blob = thumbnail_bucket.blob(thumbnail_blob_name)

    thumbnail_bytes = io.BytesIO()
    img.save(thumbnail_bytes, format='PNG')
    thumbnail_blob.upload_from_string(thumbnail_bytes.getvalue(), content_type='image/png')

    print(f"Thumbnail for {file_name} uploaded to {thumbnail_bucket_name}/{thumbnail_blob_name}")

This function is simple, stateless, and directly addresses a single event.

Cloud Run Approach:

  1. Container: You build a Docker container that includes your application code and any dependencies. This application listens for HTTP requests.
  2. Trigger: You’d typically use a Pub/Sub trigger (e.g., a Pub/Sub topic that receives messages from Cloud Storage via notifications) or an HTTP trigger. For this example, let’s assume a Pub/Sub trigger.
  3. Service: A Cloud Run service is deployed using your container image. The Pub/Sub trigger sends messages to a Pub/Sub topic, which is then configured to invoke your Cloud Run service.
  4. Code: Your application code within the container receives the Pub/Sub message (which contains information about the new file), fetches the image from Cloud Storage (using the Cloud Storage client library), processes it, and uploads the thumbnail.
# app.py (Cloud Run)
import functions_framework
from google.cloud import storage
from PIL import Image
import io
import base64
import json

storage_client = storage.Client()

@functions_framework.cloud_event
def generate_thumbnail_run(cloud_event):
    """Triggered by a Pub/Sub message from Cloud Storage."""
    data = cloud_event.data.get("message", {}).get("data", "")
    if not data:
        print("No data in message.")
        return

    message_bytes = base64.b64decode(data)
    message_json = json.loads(message_bytes)

    bucket_name = message_json.get("bucket")
    file_name = message_json.get("name")
    content_type = message_json.get("contentType", "")

    if not file_name or not bucket_name:
        print("Missing bucket name or file name in message.")
        return

    if not file_name.lower().endswith(('jpg', 'jpeg', 'png')):
        print(f"Skipping non-image file: {file_name}")
        return

    # Download the original image
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(file_name)
    image_bytes = blob.download_as_bytes()

    # Generate thumbnail
    img = Image.open(io.BytesIO(image_bytes))
    img.thumbnail((128, 128)) # Resize to 128x128

    # Upload the thumbnail
    thumbnail_bucket_name = 'your-thumbnail-bucket' # Replace with your bucket name
    thumbnail_blob_name = f'thumbnails/{file_name}'
    thumbnail_bucket = storage_client.bucket(thumbnail_bucket_name)
    thumbnail_blob = thumbnail_bucket.blob(thumbnail_blob_name)

    thumbnail_bytes = io.BytesIO()
    img.save(thumbnail_bytes, format='PNG')
    thumbnail_blob.upload_from_string(thumbnail_bytes.getvalue(), content_type='image/png')

    print(f"Thumbnail for {file_name} uploaded to {thumbnail_bucket_name}/{thumbnail_blob_name}")

To run this in Cloud Run, you’d package it in a Dockerfile and deploy it as a service. The Pub/Sub trigger would then send messages to this service.

The Core Problem Solved: Both platforms let you run code without managing servers. They abstract away infrastructure, scaling, and patching. You focus on your application logic.

Cloud Functions excels at responding to specific events. It’s ideal for:

  • Single-purpose tasks: Responding to a Pub/Sub message, a Cloud Storage event, or an HTTP request.
  • Short-lived, event-driven workloads: Image processing on upload, sending notifications, simple API endpoints.
  • When you want a managed runtime: You don’t need to worry about the OS, language version, or dependencies beyond what the function environment provides. It has built-in support for Node.js, Python, Go, Java, .NET, and Ruby.

Cloud Run excels at running containerized applications. It’s ideal for:

  • Web applications and APIs: Any HTTP-based service, from simple Flask apps to complex microservices.
  • Long-running processes: Services that need to maintain state or handle longer requests.
  • When you need full control over your environment: You can use any language, library, or binary that can be packaged into a Docker container. This includes custom binaries or specific versions of runtimes.
  • Migrating existing containerized applications: Lift-and-shift of applications already running in containers.

Internal Mechanics:

  • Cloud Functions: You deploy code directly. GCP provisions a dedicated execution environment for each function instance when it’s invoked. It scales automatically based on incoming events. The environment is managed by GCP, and you have limited control over it.
  • Cloud Run: You deploy a container image. GCP spins up instances of your container to handle incoming HTTP requests (or events from Pub/Sub, etc.). It scales based on the concurrency settings you define for the service and the incoming request rate. You have full control over the container’s contents.

When the lines blur: Cloud Functions can be invoked via HTTP, making them seem like an API. Cloud Run can be triggered by events (like Pub/Sub), making them seem event-driven. The key differentiator remains the deployment unit: code vs. container.

A common misconception is that Cloud Run is always more expensive because you’re paying for a container. However, Cloud Run’s pricing is based on CPU, memory, and request time while your container is active. If your container is idle, it scales down to zero, and you pay nothing for compute. Cloud Functions also scales to zero, but its per-invocation cost and minimum billing duration can sometimes make it more expensive for very high-throughput, low-duration tasks where Cloud Run’s efficient container orchestration might edge it out.

The next step after choosing between these two is understanding how to manage their integrations with other GCP services, particularly for complex workflows involving multiple functions or services.

Want structured learning?

Take the full Cloud-functions course →