Cloud Functions can’t actually "process" Pub/Sub messages directly; they react to them.
Let’s see this in action. Imagine you have a Pub/Sub topic named user-signups. Every time a new user signs up, your backend application publishes a message to this topic. The message payload might look like this (as JSON):
{
"userId": "user-12345",
"email": "new.user@example.com",
"signupTimestamp": "2023-10-27T10:00:00Z"
}
Now, you want to send a welcome email to this new user. You can trigger a Cloud Function whenever a message arrives on the user-signups topic.
Here’s a simplified Node.js Cloud Function that does just that:
// index.js
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
exports.sendWelcomeEmail = async (message, context) => {
const signupData = JSON.parse(Buffer.from(message.data, 'base64').toString());
console.log(`Received signup for user: ${signupData.userId}`);
const emailContent = {
to: signupData.email,
from: 'noreply@yourcompany.com',
subject: 'Welcome to Our Service!',
text: `Hello ${signupData.userId},\n\nWelcome! We're excited to have you.\n\nSincerely,\nThe Team`,
html: `
<h1>Hello ${signupData.userId},</h1>
<p>Welcome! We're excited to have you.</p>
<p>Sincerely,<br>The Team</p>
`,
};
try {
await sgMail.send(emailContent);
console.log(`Welcome email sent to ${signupData.email}`);
} catch (error) {
console.error(`Failed to send welcome email to ${signupData.email}:`, error);
// Depending on your retry strategy, you might throw an error here
// to signal Pub/Sub to redeliver the message.
throw error;
}
};
To deploy this, you’d use the gcloud CLI:
gcloud functions deploy sendWelcomeEmail \
--runtime nodejs18 \
--trigger-topic user-signups \
--entry-point sendWelcomeEmail \
--set-env-vars SENDGRID_API_KEY="YOUR_SENDGRID_API_KEY" \
--region us-central1
When a message lands on user-signups, Pub/Sub invokes sendWelcomeEmail. The function receives the message object, decodes the base64-encoded data, parses the JSON, and uses SendGrid to send an email. The context object contains metadata about the event, like the message ID and publish time.
This pattern is incredibly powerful because it decouples your backend from the downstream actions. Your backend just needs to publish a message. Any number of functions can react to that message independently: sending emails, updating a database, triggering an analytics event, or even publishing to another Pub/Sub topic.
The core problem this solves is event-driven architecture. Instead of your backend polling for changes or directly calling multiple services, it broadcasts events. This makes your system more scalable, resilient, and easier to extend. Each function can be scaled independently based on the volume of messages it needs to process.
The Pub/Sub trigger for Cloud Functions uses a push subscription under the hood. When you deploy a function with --trigger-topic, Google Cloud automatically creates a push subscription to that topic. This subscription is configured to deliver messages to the HTTP endpoint of your deployed function. If the function returns an error status code (like 500) or times out, Pub/Sub will automatically retry delivering the message up to a configurable limit. This built-in retry mechanism is crucial for ensuring message delivery without your application logic needing to manage it.
You control the behavior of your function via environment variables, runtime selection, memory allocation, and timeout settings. For instance, you might increase the memory if your function performs complex data processing or increase the timeout if it makes long-running external API calls. However, it’s generally better to keep functions short-lived and idempotent. Pub/Sub guarantees at-least-once delivery for push subscriptions, meaning your function might receive the same message twice. Your function logic must be able to handle duplicate messages gracefully without causing unintended side effects. A common way to achieve idempotency is to check if an action has already been performed for a given message ID or a unique identifier within the message payload before executing the core logic.
The next concept to explore is handling dead-letter queues for messages that persistently fail processing.