Cloud Scheduler includes job identification and schedule-time HTTP headers on HTTP and App Engine requests that let you deduplicate retried executions and make your target handlers idempotent. Because Cloud Scheduler provides at-least-once delivery, deduplicating requests prevents your handler or downstream Google Cloud APIs from processing the same scheduled run more than once when timeouts or transient network interruptions occur.
How request deduplication works
Duplicate job requests for the same scheduled run can occur in the following situations:
- Long-running targets: The target service takes longer to respond than the
job's attempt deadline (
attemptDeadline), causing Cloud Scheduler to time out and retry the job while the first attempt is still running. For HTTP targets,attemptDeadlinedefaults to 3 minutes and can be configured up to 30 minutes. AlignattemptDeadlinewith your target's timeout, or process long-running work asynchronously if execution can exceed 30 minutes. - Lost network acknowledgments or transient proxy retries: The target
receives and processes the request, but a transient network interruption or
proxy disconnect prevents Cloud Scheduler from receiving the HTTP
2xxresponse. Even if you setretryCountto0, at-least-once delivery means a target can still receive duplicate requests during transient transport failures.
To make your job handler idempotent and deduplicate retried requests, use the HTTP headers that Cloud Scheduler automatically includes on requests to HTTP and App Engine targets:
| Header | Description |
|---|---|
X-CloudScheduler-JobName |
The short job identifier (JOB_ID) from
projects/PROJECT_ID/locations/LOCATION_ID/jobs/JOB_ID. |
X-CloudScheduler-ScheduleTime |
For jobs specified in
This header isn't guaranteed for jobs that use the
alternative |
Require authentication on your target endpoint
The X-CloudScheduler-* headers aren't authenticated and can be set by any
caller. Don't use these headers for authentication. Instead, secure your
endpoint based on the target type:
- HTTP targets: Configure the job to send an OIDC or OAuth token and require authentication on your endpoint. For more information, see Use authentication with HTTP targets.
- App Engine targets: Restrict access to the handler by setting
login: adminin yourapp.yamlfile and configuring App Engine firewall rules.
Use case-insensitive header lookups
Header capitalization can vary across HTTP versions and infrastructure (for
example, X-Cloudscheduler-Jobname in HTTP/1.1 or lowercase
x-cloudscheduler-jobname and x-cloudscheduler-scheduletime in HTTP/2
environments such as Cloud Run). Always use case-insensitive header
lookups in your handler.
Disambiguate job IDs across projects or regions
Cloud Scheduler doesn't automatically send project ID or region
headers. A single handler endpoint might receive requests from multiple projects
or regions that share the same JOB_ID. In this case,
pass the project ID or region in
HttpTarget.headers,
AppEngineHttpTarget.headers,
or the target URL path or query parameters, and include those values in your
deduplication key.
Deduplicate requests in an HTTP handler
Before you implement request deduplication in your handler, complete the following setup prerequisites:
- Configure your job's schedule in
unix-cronformat (for example,*/5 * * * *instead ofevery 5 minutes) so that Cloud Scheduler sends theX-CloudScheduler-ScheduleTimeheader. - Set up a persistent datastore that supports transactions or atomic conditional
writes, such as Firestore,
Spanner, or Memorystore.
Configure a time-to-live (TTL) policy on the
collection or table (for example, on the
expire_atfield) to delete old deduplication records after the job's retry window (maxRetryDuration). Firestore TTL deletes expired records asynchronously, typically within 24-72 hours. To let retries reclaim runs from crashed instances before TTL cleanup, also store a lease expiration timestamp (such aslease_expires, set to at least your target's request timeout andattemptDeadline) and evaluate it in a transaction.
To process each scheduled run only once in your HTTP handler, complete the following steps:
- Read
X-CloudScheduler-JobNameandX-CloudScheduler-ScheduleTimefrom the incoming request headers and hash them into a fixed-length deduplication key (run_id). - Run a transaction in your datastore to check and claim the record for the
deduplication key:
- If the record exists and
statusiscompleted, return an HTTP200 OK(or204 No Content) status code immediately without running the job logic so that Cloud Scheduler stops retrying. - If the record exists,
statusisin_progress, and the lease (lease_expires) hasn't expired, return a retryable error status code such as409 Conflictso that Cloud Scheduler keeps its retry policy active in case the in-flight attempt fails. Avoid returning503 Service Unavailablefrom App Engine targets, because App Engine treats503as a system error and retries the request without counting it againstretryCount. - If the record doesn't exist or its lease has expired, write the record with
statusset toin_progress, a unique attempt ID (attempt_id), a lease expiration timestamp (lease_expires), and a TTL expiration timestamp (expire_at).
- If the record exists and
After claiming the record, run your job logic. In a transaction, verify that the record's
attempt_idstill matches the current attempt before modifying the record:- If the job logic succeeds, update
statustocompletedand return an HTTP200 OKstatus code. - If the job logic fails with an error that you can retry, delete the
deduplication record before returning a
5xxstatus code so that Cloud Scheduler can retry the run without waiting for the lease to expire.
- If the job logic succeeds, update
Example HTTP handler
The following Python example (for Cloud Run or Cloud Run functions) uses a Firestore transaction with a lease to claim runs and deduplicate retried job requests.
This sample isn't production-ready. Review and adjust it to your needs before deploying:
Prevent duplicate requests to Google Cloud API targets
When Cloud Scheduler invokes a Google Cloud API endpoint
(*.googleapis.com) directly over HTTP, the target API ignores the
X-CloudScheduler-JobName and X-CloudScheduler-ScheduleTime headers. For
example, calling workflowexecutions.googleapis.com to start a
Workflows execution,
run.googleapis.com to run a
Cloud Run job, or
cloudbuild.googleapis.com to run a
Cloud Build trigger starts a new run on
every retry attempt.
For production reliability, enforce idempotency in the most downstream service that modifies state. Deduplicating requests only at an upstream layer narrows the retry window, but if a downstream request succeeds and the HTTP response is lost over the network, the upstream caller can't determine whether the operation completed and might still retry the request.
To prevent duplicate executions when scheduling Google Cloud API targets, use the following approaches together for end-to-end idempotency:
Route requests through an idempotent proxy: Instead of calling the
*.googleapis.comendpoint directly from Cloud Scheduler, deploy your own Cloud Run service or Cloud Run function proxy and configure Cloud Scheduler to invoke that proxy. In the proxy, implement the Python and Firestore atomic transactional claim pattern described in this document, in Deduplicate requests in an HTTP handler.The proxy reads
X-CloudScheduler-JobNameandX-CloudScheduler-ScheduleTime, deduplicates retried requests, and passes the deduplication key as an idempotency token in the request payload (for example, in theargumentfield when starting a Workflows execution).Require authentication on the proxy (for example, deploy with
--no-allow-unauthenticated) and configure the Cloud Scheduler job to authenticate with an OIDC token. Run the proxy under a dedicated service account with only the minimum required Identity and Access Management (IAM) roles (such asroles/workflows.invokeron the target workflow androles/datastore.userfor the deduplication records).Enforce idempotency in the downstream services: Use the idempotency token to perform an atomic or transactional check in a datastore such as Firestore before running state-changing steps. Pass the token to any downstream HTTP endpoints or Cloud Run functions that your service invokes so that those targets can also deduplicate retries. For an end-to-end example using Workflows, Firestore transactions, and Cloud Run functions, see the Google Cloud blog post, Making API calls exactly once when using Workflows.