The problem with default storage triggers
A typical file processing workflow on Google Cloud Storage looks like this:
- A user uploads a file to
gs://bucket/videos/video.mp4. - A Cloud Function or Cloud Run service processes the file and generates a preview image.
- The function saves the image back to
gs://bucket/thumbnails/video_thumbnail.png.
Wiring this workflow to a standard google.cloud.storage.object.v1.finalized trigger creates an instant loop.
Why default GCS triggers loop
The default finalized trigger fires on every object creation in the bucket:
type: google.cloud.storage.object.v1.finalizedbucket: my-media-bucketWhen videos/video.mp4 lands in the bucket, your function runs and writes thumbnails/video_thumbnail.png. Because that thumbnail is also a new object in the same bucket, Cloud Storage fires another finalized event. The function runs again, generates another file, and loops until memory limits, execution timeouts, or concurrency caps stop it.
A single upload can trigger hundreds of billable function invocations in minutes.
Warning - Infinite loop risk
Unfiltered recursive triggers can burn through Cloud Functions quotas and rack up billing charges within hours.
Why code-level checks fall short
The standard workaround checks the object path in code and exits early:
if (objectName.startsWith('thumbnails/')) { return;}Or filters by extension:
if (!objectName.endsWith('.mp4')) { return;}Early returns stop your heavy processing logic, but the function still boots up. You still pay for the invocation, cold starts consume resources, and logs fill with junk events.
You want the platform to drop or filter the event before your compute runs. You have two practical ways to do that: Eventarc path patterns through Cloud Audit Logs, or Cloud Storage bucket notifications via Pub/Sub.
Option 1: Filter paths with Cloud Audit Logs and Eventarc
Eventarc lets you filter on path patterns if you route events through Cloud Audit Logs.
Instead of listening to google.cloud.storage.object.v1.finalized, you create an Eventarc trigger for google.cloud.audit.log.v1.written events. You set a resource path pattern targeting objects/videos/*. When your function writes output to thumbnails/, Eventarc ignores the event and never invokes your code.
Architecture
graph TD
User(["User<br/>uploads video"])
GCS_Videos[("GCS Bucket<br/>/videos")]
AuditLogs["Cloud Audit Logs<br/>storage.objects.create"]
Eventarc{"Eventarc Trigger<br/>path filter: /videos/**"}
Compute["Cloud Run / Cloud Function<br/>generates thumbnail"]
Thumb(["Thumbnail<br/>ready"])
GCS_Thumbs[("GCS Bucket<br/>/thumbnails")]
User -->|"1. HTTPS upload"| GCS_Videos
GCS_Videos -.->|"2. Emits audit log event"| AuditLogs
AuditLogs -->|"3. Forwards event"| Eventarc
Eventarc -->|"4. Matches /videos/** only, /thumbnails ignored"| Compute
Compute -->|"5. Processes frame"| Thumb
Thumb -->|"6. Stores output"| GCS_Thumbs
style GCS_Videos fill:#1A73E8,stroke:#0D47A1,color:#fff
style GCS_Thumbs fill:#1A73E8,stroke:#0D47A1,color:#fff
style AuditLogs fill:#EA4335,stroke:#B31412,color:#fff
style Eventarc fill:#F9AB00,stroke:#E37400,color:#1a1a1a
style Compute fill:#34A853,stroke:#1E7E34,color:#fff
style Thumb fill:#34A853,stroke:#1E7E34,color:#fff
style User fill:#E8F0FE,stroke:#4285F4,color:#1a1a1a
Set up Eventarc path filtering
-
Enable Data Write audit logs for Cloud Storage. Go to IAM & Admin > Audit Logs in Google Cloud Console. Select Google Cloud Storage, check Data Write, and save.
-
Create the Eventarc trigger using gcloud.
gcloud eventarc triggers create videos-folder-trigger \ --location=${REGION} \ --destination-run-service=thumbnail-service \ --destination-run-region=${REGION} \ --event-filters="type=google.cloud.audit.log.v1.written" \ --event-filters="serviceName=storage.googleapis.com" \ --event-filters="methodName=storage.objects.create" \ --event-filters-path-pattern="resourceName=projects/_/buckets/bucket-name/objects/videos/*" \ --service-account=YOUR_EVENTARC_SERVICE_ACCOUNT- Handle the event in your function.
const functions = require('@google-cloud/functions-framework');
functions.cloudEvent('thumbnailTrigger', (event) => { const payload = event.data?.protoPayload || {};
console.log('Event ID:', event.id); console.log('Method Name:', payload.methodName); console.log('Resource Name:', payload.resourceName);});Option 2: Filter folder prefixes with GCS bucket notifications
Cloud Storage includes a built-in notification system that publishes directly to Pub/Sub topics. Unlike default Eventarc bucket triggers, bucket notifications support prefix filtering natively with --object-prefix.
When files land under trigger/ or videos/, Cloud Storage publishes an event to your topic. Writes to thumbnails/ or other directories never hit Pub/Sub, so downstream consumers never wake up.
1. Create the bucket notification
Run the gcloud storage command to attach a notification configuration to your bucket:
gcloud storage buckets notifications create gs://bucket-name \ --topic=folder_trigger \ --object-prefix="trigger/" \ --event-types="OBJECT_FINALIZE"If Cloud Storage lacks publish permissions on your topic, grant the publisher role to the Cloud Storage service account:
GCS_SERVICE_ACCOUNT=$(gcloud storage service-agent)
gcloud pubsub topics add-iam-policy-binding folder_trigger \ --member="serviceAccount:${GCS_SERVICE_ACCOUNT}" \ --role="roles/pubsub.publisher"2. Consume events in Firebase Functions
If you use Firebase Functions v2, subscribe to the topic with onMessagePublished:
import { logger } from "firebase-functions/logger";import { onMessagePublished } from "firebase-functions/v2/pubsub";
export const folder_trigger = onMessagePublished({ topic: "folder_trigger", region: "us-central1",}, async (event) => { const messageData = event.data.message.json;
const bucket = messageData.bucket; const filePath = messageData.name; const size = messageData.size;
logger.info(`Processing file upload: ${bucket}/${filePath} (${size} bytes)`);
return null; });Firebase parses the incoming Pub/Sub JSON payload in event.data.message.json, giving you direct access to bucket, name, and size.
3. Consume events in Cloud Functions (2nd gen) or Cloud Run
For standard Cloud Functions (2nd gen) with the Functions Framework, decode the message payload from base64:
const functions = require('@google-cloud/functions-framework');
functions.cloudEvent('folderTrigger', (cloudEvent) => { const base64Data = cloudEvent.data.message.data; const messageData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
const bucket = messageData.bucket; const filePath = messageData.name; const size = messageData.size;
console.log(`Processing file upload: ${bucket}/${filePath} (${size} bytes)`);});Deploy the function with a Pub/Sub trigger:
gcloud functions deploy folder-trigger \ --gen2 \ --runtime=nodejs20 \ --region=us-central1 \ --source=. \ --entry-point=folderTrigger \ --trigger-topic=folder_triggerYou can also point an Eventarc Pub/Sub trigger to the folder_trigger topic if your service runs on Cloud Run.
Compare the approaches
| Feature | Default GCS finalized | Eventarc + Audit Logs | GCS Pub/Sub notifications |
|---|---|---|---|
| Bucket filtering | Yes | Yes | Yes |
| Folder filtering | No | Yes (wildcards and globs) | Yes (prefix string) |
| Early return boilerplate | Required | Not needed | Not needed |
| Redundant invocations | Many | None | None |
| Recursive loop risk | High | None | None |
| Setup complexity | Low | Medium (needs Audit Logs) | Low (one gcloud command) |
| Good fit | Single-folder buckets | Complex glob patterns | Pub/Sub pipelines and Firebase |
Cost and volume considerations
- GCS Data Write audit logs write to Cloud Logging. Google Cloud gives you 50 GiB of free logging per project each month. At 10,000 uploads a month, the audit trail generates under 30 MiB of logs.
- Pub/Sub offers 10 GiB of free message ingestion and delivery per month. For moderate workloads, GCS bucket notifications stay within the free tier.
Traps to avoid
- Directory placeholder objects. Creating folders in the Cloud Storage console writes zero-byte objects ending with a trailing slash (
trigger/orvideos/). Addif (filePath.endsWith('/')) return;at the start of your handler to skip them. - Payload format differences. Audit Log events store resource names in
event.data.protoPayload.resourceName. Pub/Sub bucket notifications pass object metadata directly (bucket,name,size) in the JSON message body. Keep the handler schema aligned with the method you choose. - Idempotency. Eventarc and Pub/Sub both deliver messages at least once. If network hiccups or function timeouts happen, your handler can receive duplicate events. Make your processing step idempotent so re-processing the same file produces identical results without side effects.