Scheduling recurring tasks in App Engine using cron.yaml — configuration, schedule syntax, retry behavior, and best practices.
What are App Engine Cron Jobs?
The App Engine Cron Service lets you configure regularly scheduled tasks that run at defined times or intervals. Cron jobs make HTTP GET requests to URLs within your application on a schedule you define.
You specify the schedule in a cron.yaml file, deploy it alongside your app, and App Engine handles the rest.
Key Insight: App Engine cron jobs are HTTP requests, not shell commands. Your handler must be an HTTP endpoint that executes the task when called. This is different from traditional Linux cron.
Limits
| Limit | Free Apps | Paid Apps |
|---|---|---|
| Maximum scheduled tasks | 20 | 250 |
| Request timeout | 60 minutes | 60 minutes |
| Minimum interval | 1 second | 1 second |
cron.yaml Reference
Place cron.yaml alongside your app.yaml file.
cron:
- description: "Daily summary report"
url: /tasks/summary
schedule: every 24 hours
- description: "Weekly Monday email"
url: /mail/weekly
schedule: every monday 09:00
timezone: America/New_York
- description: "Cache warmup during business hours"
url: /tasks/warmup
schedule: every 10 mins from 08:00 to 18:00
timezone: America/Chicago
- description: "Quarterly report"
url: /tasks/quarterly
schedule: 1 of january,april,july,october 00:00
- description: "Retry example"
url: /tasks/retry
schedule: every 10 mins
retry_parameters:
job_retry_limit: 5
min_backoff_seconds: 2.5
max_doublings: 5
target: worker-serviceFields
| Field | Required | Description |
|---|---|---|
url | Yes | URL path to call (e.g., /tasks/summary). Must be within your app. |
schedule | Yes | Schedule definition (see syntax below). |
description | No | Human-readable description. Shown in the Console. |
timezone | No | Timezone name (default: UTC). Use IANA names like America/New_York. |
target | No | Target service name. Routes the cron request to a specific service. |
retry_parameters | No | Retry configuration for failed attempts. |
Note: Cron jobs can only call endpoints within the same App Engine application. They cannot call external URLs. For external scheduling, use Cloud Scheduler instead.
Schedule Syntax
Sub-daily intervals (end-time)
The job runs at the specified interval after the previous run completes:
schedule: every 5 minutes
schedule: every 30 mins
schedule: every 2 hours
schedule: every 12 hoursSub-daily intervals (start-time range)
The job runs at the specified interval within a time window:
schedule: every 5 minutes from 10:00 to 14:00
schedule: every 1 hours from 08:00 to 16:00
schedule: every 2 hours synchronized # Must divide 24 evenlyCustom intervals
# Daily at a specific time
schedule: every day 00:00
# Specific day of the week
schedule: every monday 09:00
schedule: every friday 17:00
# Specific day of the month
schedule: 1st,8th,15th,22nd of month 09:00
# Specific day and month
schedule: 1st monday of september,october,november 09:00
schedule: 1 of january,april,july,october 00:00
# Ordinal day of week in a month
schedule: 2nd wednesday of march 17:00
schedule: 1st,2nd monday,wednesday,friday of may 10:00Schedule keywords
| Keyword | Example | Meaning |
|---|---|---|
every N minutes | every 5 minutes | Every N minutes |
every N hours | every 2 hours | Every N hours |
every day | every day 00:00 | Once daily |
every DAY | every monday 09:00 | Once per week on that day |
Nth of MONTH | 1 of january 00:00 | Specific date |
from X to Y | from 08:00 to 18:00 | Only during time range |
synchronized | every 6 hours synchronized | Aligned to midnight (must divide 24) |
Retry Parameters
When a cron job fails (returns a non-200 HTTP status), App Engine can retry it automatically:
retry_parameters:
job_retry_limit: 5 # Max retry attempts (0-5, default: 0)
job_age_limit: 5d # Give up after 5 days
min_backoff_seconds: 2.5 # Wait at least 2.5s before first retry
max_backoff_seconds: 60 # Wait at most 60s between retries
max_doublings: 5 # Max times to double the intervalRetry behavior
| Parameter | Default | Description |
|---|---|---|
job_retry_limit | 0 (no retry) | Maximum number of retry attempts |
job_age_limit | None | Time limit for retries (e.g., 5d, 12h) |
min_backoff_seconds | 0.1 | Minimum wait before first retry |
max_backoff_seconds | 300 | Maximum wait between retries |
max_doublings | 5 | Max times to double the backoff interval |
The retry uses exponential backoff: starting at min_backoff_seconds, doubling each time up to max_doublings, capped at max_backoff_seconds.
Validating Cron Requests
Cron requests are regular HTTP requests to your app. To prevent unauthorized access, validate that requests come from App Engine’s cron service:
Headers
App Engine cron requests include:
# Python (Flask)
from flask import request, abort
@app.route("/tasks/summary")
def daily_summary():
if request.headers.get("X-Appengine-Cron") != "true":
abort(403)
# Task logic here
return "OK"Origin IP
Cron requests originate from the IP address 0.1.0.2 (or 0.1.0.1 for older gcloud versions).
Tip: Always validate cron requests. Without validation, anyone who knows your
/tasks/*URLs could trigger them by visiting the URL directly.
Deployment
# Deploy cron jobs
gcloud app deploy cron.yaml
# Delete all cron jobs (set cron.yaml to just "cron:")
gcloud app deploy cron.yamlYou can also manage cron jobs via the Google Cloud Console under App Engine > Cron Jobs.
Gotchas
| Gotcha | Detail |
|---|---|
| No traffic splitting | Cron jobs always route to a single version, ignoring traffic splits |
| dispatch.yaml precedence | If both dispatch.yaml and target are set, dispatch.yaml wins |
| At-least-once delivery | Cron may call your handler more than once. Make handlers idempotent |
| Only GET requests | Cron jobs send HTTP GET requests, not POST |
| Internal only | Cannot call external URLs, only endpoints within your app |
| Target routing | The target field routes to a service’s traffic-serving version |
| No guarantee of exact time | Cron may fire slightly after the scheduled time during high load |
Warning: Cron jobs deliver at-least-once. Your handler might be called multiple times for the same scheduled execution. Design your tasks to be idempotent — running them twice should produce the same result as running them once.
Cron vs Cloud Scheduler
| Feature | App Engine Cron | Cloud Scheduler |
|---|---|---|
| Scope | App Engine endpoints only | Any HTTP endpoint, Pub/Sub, etc. |
| Configuration | cron.yaml in your project | Separate GCP resource |
| Auth | Automatic (X-Appengine-Cron header) | Configurable (OIDC, OAuth) |
| Limits | 20 (free) / 250 (paid) | 3 (free) / higher quotas |
| Target | App Engine services only | Any URL, Pub/Sub topic, App Engine |
| HTTP method | GET only | GET, POST, PUT, DELETE, etc. |
Use App Engine Cron for simple scheduled tasks within your app. Use Cloud Scheduler for cross-product or cross-project scheduling, or when you need POST requests.
TL;DR
- App Engine cron jobs send HTTP GET requests to your app on a schedule.
- Configure jobs in cron.yaml with
url,schedule, and optionaltimezone,target, andretry_parameters. - Supports intervals, specific times, day-of-week, day-of-month, and time-range schedules.
- Always validate cron requests using the
X-Appengine-Cronheader. - Handlers must be idempotent — at-least-once delivery means possible duplicates.
- Cron jobs ignore traffic splitting and always route to a single version.
- Limits: 20 tasks (free), 250 tasks (paid).
Resources
Scheduling Jobs with cron.yaml Official cron.yaml reference and schedule syntax.
Cloud Scheduler Documentation Alternative scheduling service for cross-product and HTTP targets.
Cron Jobs in the Console Manage cron jobs from the Google Cloud Console.
app.yaml Configuration Service configuration file reference.
Creating a Service How services relate to cron job targeting.