aws gcp azure serverless cloud

Serverless Cron: Lambda, Cloud Functions & Cloud Scheduler

by Sinthuyan Arulselvam  · June 13, 2026

Running a scheduled job used to mean owning a daemon. You'd provision a server, configure crond, write log-rotation rules, set up alerting for when the process died, and babysit the whole thing indefinitely. Serverless scheduling inverts that model: you write a function, attach a schedule, and let the cloud provider handle every layer beneath it. The trade-off is real — you gain operational simplicity and pay only for invocations, but you inherit cold starts, strict timeout ceilings, and statefulness constraints that a long-running daemon never imposed.

This guide walks through the concrete mechanics of serverless cron across the three major clouds — AWS, GCP, and Azure — then covers the operational concerns that apply to all of them: cold start behaviour, timeout limits, retry semantics, idempotency, and cost modelling.

Why Serverless for Cron

The traditional argument for always-on compute is simplicity: one server, one crontab, straightforward debugging. The argument for serverless scheduling flips each of those points:

  • No daemon to manage. There is no crond process that can silently die, no server to patch, no SSH access required to update a schedule. The schedule lives in infrastructure-as-code and is version-controlled alongside the function itself.
  • Pay-per-invocation pricing. A job that runs once a day on a dedicated EC2 instance pays for 86,400 seconds of compute. On Lambda, it pays for the duration of that single invocation — often measured in seconds or milliseconds.
  • Auto-scaling without configuration. If a backlog of jobs needs to run in parallel, serverless functions spin up concurrently without capacity planning. A cron-driven ETL pipeline that occasionally needs to process ten jobs simultaneously does not require pre-provisioned concurrency headroom beyond what the platform provides by default.
  • Cold starts as the primary trade-off. The runtime is not kept warm between invocations. When a schedule fires, the platform must initialise the execution environment — downloading the function package, starting the runtime, running any initialisation code outside the handler — before your actual job logic begins. For infrequently-scheduled jobs (hourly or less), cold start latency is nearly guaranteed on every invocation.

Cold starts do not matter if your job's execution time is measured in seconds and strict wall-clock precision is not required. They matter significantly if you're building a payment processing job that must begin exactly at 00:00:00 UTC, or a rate-limited API poller where a delayed start eats into a narrow execution window.

AWS: EventBridge + Lambda

AWS EventBridge Scheduler (formerly CloudWatch Events) is the canonical way to invoke Lambda functions on a schedule. It supports both rate-based expressions (rate(5 minutes)) and cron expressions in a six-field format that prepends seconds and appends year to the standard five-field form.

Setting Up a Scheduled Lambda Invocation

The infrastructure-as-code approach using AWS CloudFormation or CDK is preferred over console clicks. A minimal configuration requires: an EventBridge rule with a schedule expression, a Lambda function target, and an IAM role that grants EventBridge permission to invoke the function.

bash
# Terraform example — EventBridge + Lambda scheduled invocation
resource "aws_cloudwatch_event_rule" "daily_job" {
  name                = "daily-etl-trigger"
  schedule_expression = "cron(0 2 * * ? *)"
  # AWS cron: minute hour dom month dow year
  # The ? in dow means "no specific value" (dom is set)
  # This fires at 02:00 UTC every day
}

resource "aws_cloudwatch_event_target" "lambda_target" {
  rule      = aws_cloudwatch_event_rule.daily_job.name
  target_id = "DailyEtlLambda"
  arn       = aws_lambda_function.etl.arn
}

resource "aws_lambda_permission" "allow_eventbridge" {
  statement_id  = "AllowEventBridgeInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.etl.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.daily_job.arn
}

IAM Considerations

EventBridge needs lambda:InvokeFunction on the specific function ARN — not a wildcard. The Lambda execution role needs only the permissions the function itself requires: S3 read/write, RDS connect, DynamoDB access, etc. Principle of least privilege applies doubly here because scheduled jobs often run unattended for months; an over-permissioned role is a lateral movement vector.

If using EventBridge Scheduler (the newer API, distinct from EventBridge Rules), the scheduler service also needs an IAM role with lambda:InvokeFunction. Schedulers created via the console create this role automatically; IaC deployments must create it explicitly.

Timeout Limits and Async Invocation

Lambda has a hard ceiling of 15 minutes per invocation. EventBridge invokes Lambda asynchronously, meaning EventBridge does not wait for the function to complete — it fires the invocation and moves on. If your job exceeds 15 minutes, the Lambda runtime terminates it without waiting for completion, and the partial work is not rolled back.

For jobs approaching the timeout limit, the correct pattern is to split the work: use the scheduled Lambda to enqueue work items into SQS, and use a separate SQS-triggered Lambda (or Step Functions workflow) to process them in parallelisable chunks. This keeps each individual function invocation well within the 15-minute ceiling while allowing the total job to span arbitrarily long pipelines.

GCP: Cloud Scheduler + Cloud Functions

Google Cloud Scheduler is a fully managed cron service that can target HTTP endpoints, Pub/Sub topics, and App Engine applications. Pairing it with Cloud Functions (2nd gen) gives you the serverless execution layer.

HTTP-Triggered Functions

The idiomatic GCP pattern triggers Cloud Functions via HTTP. Cloud Scheduler sends an authenticated HTTP POST to the function's URL on a schedule defined by a standard five-field cron expression. The function endpoint validates the OIDC token in the request header to confirm the caller is the scheduler service, not an arbitrary external request.

bash
# gcloud CLI — create a Cloud Scheduler job targeting a Cloud Function
gcloud scheduler jobs create http daily-report-job \
  --location=us-central1 \
  --schedule="0 6 * * 1-5" \
  --uri="https://us-central1-my-project.cloudfunctions.net/generateReport" \
  --http-method=POST \
  --oidc-service-account-email=scheduler-sa@my-project.iam.gserviceaccount.com \
  --oidc-token-audience="https://us-central1-my-project.cloudfunctions.net/generateReport"

Inside the Cloud Function, validate the incoming token before doing any work:

javascript
// Cloud Function (Node.js) — verify scheduler identity
import { Request, Response } from "@google-cloud/functions-framework";

export async function generateReport(req: Request, res: Response) {
  const authHeader = req.headers.authorization ?? "";
  if (!authHeader.startsWith("Bearer ")) {
    res.status(401).send("Unauthorised");
    return;
  }
  // The Functions framework verifies the OIDC token automatically
  // when deployed with --allow-unauthenticated=false
  // Proceed with job logic
  await runReport();
  res.status(200).send("OK");
}

Pub/Sub Alternative

For fire-and-forget semantics, Cloud Scheduler can publish a message to a Pub/Sub topic instead of calling an HTTP endpoint. The Cloud Function subscribes to that topic. This decouples scheduling from execution: the scheduler does not care whether the function is available, and Pub/Sub handles message delivery with its own retry semantics (up to 7 days of retention). This pattern suits jobs where exact invocation time is less critical than guaranteed eventual execution.

App Engine cron.yaml (Legacy)

Before Cloud Scheduler existed, Google recommended cron.yaml for App Engine applications. This file lives in the application root and defines schedules that App Engine's infrastructure honours. It uses a non-standard schedule syntax (every 24 hours, every monday 09:00) rather than cron expressions. New projects should use Cloud Scheduler; cron.yaml is relevant only if you're maintaining an existing App Engine application that predates Cloud Scheduler's 2019 GA release.

Azure: Timer Trigger Functions

Azure Functions supports a Timer trigger binding that fires the function on a schedule. Unlike EventBridge (which invokes a function externally via an event) or Cloud Scheduler (which sends an HTTP request), Azure's timer trigger is built into the function host itself — the schedule is evaluated by the Azure Functions runtime running alongside your code.

NCRONTAB Format

Azure Timer triggers use NCRONTAB, a six-field format where the first field is seconds, followed by the standard five fields. This is the same field ordering as Quartz Scheduler, not standard Unix cron. A common mistake is writing a five-field expression and having Azure interpret the first field as seconds, shifting every other field.

javascript
// function.json — Azure Timer trigger definition
{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 30 9 * * 1-5"
      // NCRONTAB: second=0, minute=30, hour=9, dom=*, month=*, dow=Mon-Fri
      // Fires at 09:30:00 on weekdays
    }
  ]
}
javascript
// TypeScript handler
import { app, InvocationContext, Timer } from "@azure/functions";

app.timer("dailyReport", {
  schedule: "0 30 9 * * 1-5",
  handler: async (timer: Timer, context: InvocationContext) => {
    if (timer.isPastDue) {
      context.log("Timer is running late — previous execution may have failed");
    }
    await runDailyReport();
  },
});

The timer.isPastDue property is true when the function was not invoked on schedule — useful for distinguishing first-run-after-deployment from a genuine scheduling delay.

Durable Functions for Long-Running Jobs

Azure Functions has a default timeout of 5 minutes on the Consumption plan and up to 60 minutes on Premium. For jobs exceeding these limits, Durable Functions provides an orchestration model where a timer-triggered orchestrator function fans out work to activity functions, each of which runs within the timeout ceiling. The orchestrator itself can run for days, waiting for activity completions using durable timers that checkpoint state to Azure Storage rather than blocking a thread.

Cold Start Impact on Scheduling

Cold starts are the latency penalty paid when a cloud provider must initialise a new execution environment for your function. The environment includes the VM or container image, the language runtime, and your function's initialisation code (database connection pooling, SDK client creation, config loading). Cold start duration ranges from under 100ms for lightweight runtimes (Node.js, Python) to over 3 seconds for JVM-based runtimes.

For scheduled jobs, cold starts have a specific implication: if a job fires at 02:00:00 UTC but takes 2 seconds to cold-start, the actual business logic begins at 02:00:02. Whether that matters depends entirely on the job's requirements. For a nightly report generation that runs for 5 minutes, a 2-second cold start is negligible. For a payment settlement job that must post transactions before a 02:00:05 banking cutoff, it is fatal.

Provisioned Concurrency (AWS)

AWS Lambda's Provisioned Concurrency pre-warms a specified number of execution environments, eliminating cold starts for those instances. You configure it on a function version or alias:

bash
# AWS CLI — configure provisioned concurrency on a Lambda alias
aws lambda put-provisioned-concurrency-config \
  --function-name my-scheduled-job \
  --qualifier production \
  --provisioned-concurrent-executions 1

One provisioned instance is sufficient for a scheduled job that runs serially. Provisioned concurrency costs roughly $0.015 per GB-hour (us-east-1, 2025 pricing) regardless of whether the instance handles invocations. For a job that runs once per day, provisioned concurrency costs more than the invocation itself — use it only when cold-start latency is genuinely a business constraint.

Minimum Instances (GCP)

Cloud Functions 2nd gen supports a --min-instances flag that keeps at least N instances warm at all times. Setting --min-instances=1 on a scheduled function ensures it never cold-starts. Like AWS provisioned concurrency, minimum instances incur charges even when idle. The cost is billed at the idle instance rate, which is approximately 10% of the active execution rate for Cloud Functions 2nd gen.

Timeout and Retry Strategies

Each provider imposes distinct timeout ceilings, and each has different default retry behaviour when a function fails or times out. Getting these wrong causes either silent job drops or explosive duplicate executions.

Provider Service Max Timeout Default Retries (on failure) Retry on Timeout?
AWS Lambda (async invoke) 15 minutes 2 automatic retries Yes, counts as failure
GCP Cloud Functions (HTTP) 60 minutes (2nd gen) 0 (scheduler retry config) Configurable (1–5 retries)
GCP Cloud Functions (Pub/Sub) 60 minutes (2nd gen) Pub/Sub retry until ack (up to 7 days) Yes, until acknowledged
Azure Functions (Consumption) 5 minutes 0 (timer triggers do not retry) No
Azure Functions (Premium) 60 minutes 0 (timer triggers do not retry) No

Idempotency Requirements

AWS Lambda's asynchronous invocation model retries a failed function up to twice by default. This means your scheduled job handler must be idempotent: running it twice with the same input must produce the same outcome as running it once. The standard techniques apply:

  • Upsert instead of insert. Use INSERT ... ON CONFLICT DO UPDATE or equivalent rather than plain INSERT. A retried job that re-inserts records causes duplicates; one that upserts is safe.
  • Idempotency keys. If calling an external API that isn't idempotent, generate a deterministic key from the schedule's invocation timestamp and pass it as an idempotency key header. Stripe, Adyen, and most payment processors support this pattern.
  • State machine checks. Before beginning destructive work, check a status flag in the database. If the job for today's date already has status completed, return early without repeating the work.

Dead-Letter Queues

AWS Lambda async invocations support a Dead Letter Queue (DLQ): after all retries are exhausted, the event payload is sent to an SQS queue or SNS topic. This is critical for scheduled jobs — without a DLQ, a job that fails all three attempts (original + 2 retries) disappears silently.

bash
# Terraform — configure Lambda DLQ for failed scheduled invocations
resource "aws_lambda_function" "etl" {
  # ... other config ...
  dead_letter_config {
    target_arn = aws_sqs_queue.job_dlq.arn
  }
}

The Lambda execution role needs sqs:SendMessage permission on the DLQ ARN. A CloudWatch alarm on the DLQ's ApproximateNumberOfMessagesVisible metric completes the observability loop: any failed job invocation generates an alert.

GCP's HTTP-target Cloud Scheduler jobs expose a retry configuration with min/max backoff and maximum retry count. Failed invocations (non-2xx HTTP response) are retried within those bounds. For Pub/Sub-triggered functions, Pub/Sub's own dead-letter topic configuration handles exhausted retries — configure it on the subscription, not the function.

Cost Comparison

Serverless scheduling cost has two components: the scheduling service itself (near-zero) and the function execution. The scheduling services are essentially free at typical volumes — AWS EventBridge rules cost $1 per million custom events, GCP Cloud Scheduler is $0.10 per job per month, Azure Timer triggers have no per-trigger cost. The real cost variable is the execution profile of the function.

Provider Free Tier (invocations/month) Price per 1M invocations Price per GB-second Max memory
AWS Lambda 1,000,000 $0.20 $0.0000166667 10 GB
GCP Cloud Functions 2,000,000 $0.40 $0.0000025 32 GB (2nd gen)
Azure Functions 1,000,000 $0.20 $0.000016 14 GB

Pricing figures are approximate as of mid-2025 for us-east/us-central regions. Always verify against provider pricing pages before making capacity decisions.

Break-Even Analysis vs Always-On Compute

The break-even point between serverless and always-on compute depends on invocation frequency, function duration, and memory allocation. The math is straightforward: at what point does continuous Lambda execution cost more than the cheapest always-on alternative?

Consider a job running 512 MB Lambda for 10 seconds, 288 times per day (every 5 minutes). Daily compute cost: 288 invocations × 10 seconds × 0.5 GB × $0.0000166667 = approximately $0.024/day, or $8.75/year. A t4g.nano EC2 instance runs about $37/year. Serverless is cheaper by a factor of 4 at this frequency.

Now consider a job running every 30 seconds, 24 hours a day, for 5 seconds each: 2,880 invocations/day × 5 seconds × 0.5 GB × $0.0000166667 = $0.12/day, or $43.80/year. The t4g.nano is now cheaper. The break-even for 512 MB Lambda vs a t4g.nano is roughly one invocation every 45 seconds for a 5-second duration job.

  • Infrequent, short jobs (hourly or less, under 60 seconds): Serverless wins decisively. Even with provisioned concurrency, the cost is a fraction of always-on compute.
  • Frequent, short jobs (every minute or more often): Always-on compute becomes competitive. A persistent process eliminates cold starts and process startup overhead.
  • Long-running jobs (minutes to hours): The 15-minute Lambda ceiling forces architectural changes (Step Functions, SQS fan-out). If your job naturally runs for 30 minutes, a container on ECS Fargate with a scheduled task is often simpler and cheaper.
  • Bursty patterns (idle most of the day, heavy bursts): Serverless wins on cost; the idle periods cost nothing. A scheduled batch job that runs for 10 minutes at 01:00 AM pays for 10 minutes of compute, not 24 hours.

Choosing the Right Pattern

The decision tree is relatively clean. If your job runs less frequently than once per minute, executes in under 10 minutes, and is stateless or can be made idempotent with reasonable effort, serverless scheduling is the right default. The operational overhead is minimal, cost is low, and you eliminate an entire class of infrastructure management.

If your job is time-critical at sub-second precision, runs more frequently than once a minute, requires more than 15 minutes of continuous execution, or maintains significant in-memory state between invocations — reach for a container-based scheduler or a persistent worker process. Serverless cron is a powerful tool, but its constraints are real. Matching the execution model to the job's actual requirements avoids expensive retrofits later.

Explore related resources

Related Guides