Back to blog

Guides · Dec 23, 2025

AWS Lambda cron without EventBridge: the Function URL pattern

EventBridge is the AWS-native path for putting a Lambda on a clock, but it asks for an IAM role per target, an invoke permission per Rule, and a separate console per region. Lambda Function URLs (since 2022) plus a bearer header in the handler skip the IAM dance entirely. Here is the pattern.
crontap.com / blog
EventBridge is fine, but for one HTTPS call on a clock it brings IAM roles per target, lambda:InvokeFunction permissions, and a console wizard per region. Lambda Function URLs (since 2022) plus a bearer header in the handler give you the same fire-on-a-clock shape with one Crontap schedule, retries on 5xx, and one dashboard across every AWS account and region.

You wrote a Lambda that purges yesterday's expired DynamoDB items, and the next ticket is "fire this every day at 04:00 America/Los_Angeles". The default AWS answer is EventBridge (Rules or Scheduler), and EventBridge is a fine product when you actually need it, but for one HTTPS call on a clock it brings a lot of YAML. An IAM role per target, a lambda:InvokeFunction permission scoped to the Rule's ARN, a separate console wizard per region, and a cost surface split across EventBridge events plus CloudWatch logs plus the Lambda invocation itself. If your relationship with this Lambda is "call it on a schedule and alert me when it fails", there is a much shorter path.

If you want the short version: deploy the Lambda with a Function URL, check a bearer header inside the handler, and point Crontap at the URL. You get every 1 minute on Pro, per-IANA timezones, automatic retries on 5xx, and one dashboard across every AWS account, every region, and every other vendor (Cloud Run, Vercel, Workers) you also happen to call on a clock.

EventBridge as the official AWS path

The AWS-native way to put a Lambda on a clock is EventBridge. There are two flavors. The older one is EventBridge Rules, a "schedule expression" attached to a Rule that targets the Lambda. The newer one (since 2022) is EventBridge Scheduler, the same idea moved into a dedicated service with per-schedule timezone support and a higher quota.

Both want the same things from you:

  1. An IAM role with lambda:InvokeFunction on the target Lambda.
  2. A schedule expression (cron or rate) attached to the Rule or the Schedule.
  3. The target Lambda ARN, plus a permission on the Lambda's resource-based policy that says "this Rule (or Scheduler) is allowed to invoke me".

It works. It is the AWS-supported path. It is also the kind of paperwork that turns a one-line "fire this URL every day at 4am" ticket into a 40-line CloudFormation snippet.

The friction shows up per target, per region, per environment

The first time you wire this up, the IAM dance is twenty minutes. The fifth time, in a new region, for a new Lambda, it is twenty minutes again. The reason is that the IAM bindings, the EventBridge artefact, and the Lambda permission all live inside one AWS region, and they do not cross regions for free.

A few real shapes that bite:

  • IAM role per environment. Dev, staging, and prod are usually three accounts (or three regions in one account). Each one needs the role plus the resource-based permission, configured the same way each time. The first round goes into Terraform. Round two, when somebody adds a new Lambda, somebody has to remember the Terraform module exists.
  • Console UX per region. When an alert wakes you up, the first move is "which region is this schedule in". The EventBridge console is regional, so you switch the AWS region picker, find the right Rule or Schedule, and read its history. A Lambda that fires on three schedules in three regions is three context switches.
  • Cost surface across services. An EventBridge Rule fires the Lambda for free at the schedule cadence, but the invocation produces CloudWatch logs that are billed per GB ingested, and EventBridge Scheduler has its own pricing tier above the free million invocations. The dollar number is small at low volume; the bill arrives split across three line items, which is the part that surprises people.
  • Schedule lives in two places. The schedule is in EventBridge; the work is in the Lambda. If the team that owns the Lambda is not the same as the team that owns the EventBridge stack (true at most companies past 50 people), every cadence change is a cross-team ticket.

For some teams that paperwork is exactly the cost of doing business. For most teams calling Lambdas on a clock, it is more friction than the actual work justifies.

Lambda Function URLs as the simpler shape

Since April 2022, AWS lets a Lambda have a Function URL: a stable HTTPS endpoint that maps directly to the function. No API Gateway, no Application Load Balancer, no CloudFront in front. Two AuthType modes:

  • AWS_IAM. Requests must be SigV4-signed with credentials that hold lambda:InvokeFunctionUrl. Useful for AWS-to-AWS calls.
  • NONE. The endpoint is publicly reachable; auth is whatever you check inside your handler.

For an external scheduler, AuthType: NONE plus a bearer-in-handler check is the right shape. Crontap is the clock, the Lambda is the runtime, and the contract between them is one HTTPS POST with an Authorization header.

Crontap (cron)  →  HTTPS POST  →  https://abc123xyz.lambda-url.us-west-2.on.aws/  →  the actual work

No Rule. No Scheduler. No lambda:InvokeFunction permission to scope. The auth check moves out of IAM and into your handler, where it lives next to the work it gates.

Step 1: Add a Function URL to the Lambda

If you are wiring this from scratch with the AWS CLI:

aws lambda create-function-url-config \
  --function-name your-lambda \
  --auth-type NONE \
  --region us-west-2

The response contains a FunctionUrl like https://abc123xyz.lambda-url.us-west-2.on.aws/. That is the URL Crontap will hit. (If you prefer Terraform, the equivalent is the aws_lambda_function_url resource with authorization_type = "NONE".)

AuthType: NONE does not mean "no auth". It means "AWS does not enforce auth at the platform layer; your code does". The Function URL is reachable from the public internet, which is exactly what an external scheduler needs.

Step 2: Enforce a bearer token in the handler

Pick a strong random secret:

openssl rand -base64 32

Store it somewhere safe (Secrets Manager or Parameter Store) and reference it as the CRON_SECRET environment variable on the Lambda. In Node.js (the same shape works in Python, Go, Rust):

export const handler = async (event: any) => {
  const auth =
    event.headers?.authorization || event.headers?.Authorization;

  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return { statusCode: 401, body: "Unauthorized" };
  }

  await runDailyCleanup();
  return { statusCode: 200, body: JSON.stringify({ ok: true }) };
};

Three things worth calling out:

  1. The token rides in the Authorization header, not a query string. The secret never lands in CloudWatch access logs.
  2. The handler returns 200 quickly. If the cleanup is long, kick the work off async (or pair the Function URL Lambda with a Step Function or SQS queue) and return immediately; do not block the scheduler waiting for a 5-minute job.
  3. The Function URL maps to one Lambda. If you want one cron to fan out to several Lambdas, use one Crontap schedule per target, or have the Function URL handler dispatch internally.

Step 3: Store the bearer in Crontap

Head to Crontap and create a new schedule.

  1. URL. Paste the Function URL, e.g. https://abc123xyz.lambda-url.us-west-2.on.aws/.
  2. Method. POST.
  3. Headers. Add Authorization: Bearer <your CRON_SECRET>. Crontap stores the value encrypted; you do not see the plaintext again after saving.
  4. Cadence. Type plain English ("every day at 4am") or paste a cron expression. Crontap previews the next 5 fires inline so you can sanity-check the timezone before saving.
  5. Timezone. Pick the IANA zone that matches the schedule's intent. America/Los_Angeles for the daily cleanup, UTC for genuine UTC, Europe/Berlin for a local-business window.
  6. Failure alerts. Add an integration: email / webhook (Slack / Discord / Telegram). Crontap fires on 4xx and 5xx with the response body and timing in the payload, so the alert is immediately useful.

Press Perform test to fire a real request before you trust the cadence. If the Lambda returns 200, you are done. If you see 401, the bearer is mismatched. If you see 5xx, the Lambda itself failed and the alerting just proved itself.

Fix this in 60 seconds with Crontap. Free tier available. No credit card. Schedule your first job →

Worked example: daily DynamoDB cleanup at 04:00 America/Los_Angeles

A small analytics team runs a pipeline backed by DynamoDB. Every day they need to purge expired session rows older than 30 days. The job is small (a few thousand items), runs in 2 to 3 seconds, and has to fire at 04:00 America/Los_Angeles because that is the quietest window on the table.

EventBridge Scheduler is genuinely capable of this. The setup looks like this: create an IAM role with lambda:InvokeFunction on the cleanup Lambda, attach a resource-based policy on the Lambda allowing scheduler.amazonaws.com to invoke it, create the Schedule with cron(0 4 * * ? *) and scheduleExpressionTimezone: America/Los_Angeles. It works. It is also four artefacts that have to be reasoned about together when something breaks.

The Crontap shape is shorter. The Lambda gets a Function URL (one CLI call), the handler checks process.env.CRON_SECRET, Crontap fires 0 4 * * * in America/Los_Angeles. If the cleanup throws, the Lambda returns 500, Crontap retries once on 5xx, and on the second failure the on-call channel gets a Slack message with the response body. Total artefacts: one schedule and one bearer token.

The cleanup itself runs the same way under both. Only the clock changed.

When EventBridge is still the right call

External cron is a shape, not a religion. EventBridge is genuinely the right answer when:

  • The trigger source is not a clock. Scheduled cron(...) is one Rule type; "S3 ObjectCreated", "DynamoDB Stream record", and the rest of the AWS event sources are the others. Crontap is HTTPS-only; it does not subscribe to AWS event streams.
  • The target is not a Function URL Lambda. EventBridge fans out to SQS queues, Step Functions, ECS tasks, Kinesis streams, and other AWS services without the HTTP hop. If the cleanup is "drop a message in SQS, let a worker pick it up", EventBridge is the cleaner trigger.
  • Your security policy mandates IAM-bound auth on the Lambda. If AuthType: NONE is non-negotiable and you cannot front the Lambda with a thin proxy, the SigV4 path or the EventBridge IAM dance is what you need.
  • The schedule has to live in CloudFormation alongside the Lambda. For some compliance regimes, "the schedule lives outside our IaC repo" is itself a finding. EventBridge stays in CloudFormation; Crontap does not.

For the long tail of "this Lambda fires on a clock and I want to know if it failed", the bearer-in-handler shape reads cleaner.

FAQ

What's the shortest interval Crontap supports?

Every 1 minute on Pro. Free tier available for slower cadences. EventBridge Scheduler is also 1 minute minimum, so if you need sub-minute you are on the wrong runtime regardless. For minute cadence and slower, both work; the difference is the IAM and dashboard story.

Can I use Crontap with AuthType: AWS_IAM?

Not directly. Crontap does not sign requests with SigV4. The two paths if you must keep AWS_IAM on the target: (1) front the protected Lambda with a thin Function URL Lambda configured AuthType: NONE that signs and forwards, or (2) use API Gateway with a Lambda authorizer that accepts a bearer header from Crontap. Both paths add a hop, which is the trade-off.

Will this be cheaper than EventBridge Scheduler?

It depends on volume. You save the EventBridge events cost and the per-target IAM glue. You pay the same Lambda invocation cost you would anyway, and you pay Crontap's flat per-schedule price. For a handful of schedules the savings are small; for an org with dozens of cron-driven Lambdas across regions, the math tilts toward Crontap once you factor in the engineering time of the IAM setup.

How do I monitor invocations?

CloudWatch keeps working the way it always has; logs, metrics, and alarms all fire whether the Lambda was triggered by EventBridge or by a Function URL HTTPS call. Crontap stores its own request and response history alongside, so you have two views: the AWS-native view, and the cron-side view of "did the schedule fire on time and did the response come back 200".

Can one Crontap schedule call Lambdas in multiple AWS regions?

One schedule fires one URL. If you have a daily cleanup in us-west-2, eu-west-1, and ap-southeast-1, that is three schedules in Crontap, one per region. The dashboard view across all three is one tab; the alert routing across all three is one Slack channel. EventBridge requires you to set the same schedule up three times across three consoles.

Does this work with Lambda@Edge?

No. Lambda@Edge is fronted by CloudFront and triggered by viewer or origin requests, not by direct invocations. For Lambda@Edge, the trigger is HTTP traffic; for "fire on a schedule and run code at the edge", use Cloudflare Workers Cron Triggers or a Crontap schedule against a regular Lambda Function URL.

References

Related on Crontap

From the blog

Read the blog

Guides, patterns and product updates.

Tutorials on scheduling API calls, webhooks and automations, plus deep dives into cron syntax, timezones and reliability.

Alternatives

Vercel Cron every minute: beating the Hobby hourly limit

Vercel Cron caps Hobby at hourly cadence and 5 jobs, and ties every change to a redeploy. Here is the external cron pattern teams use to ship per-minute schedules, per-IANA timezones, and one dashboard across projects without paying $20/mo per user for Pro.

Alternatives

Cloud Run cron without Cloud Scheduler

Cloud Scheduler costs $0.10 per job per month after the first 3 and asks for OIDC plus IAM bindings on every target. Here is the IAM-free pattern Cloud Run teams use to fire their .run.app URLs on a clock with one bearer token and one dashboard across every GCP project.

Alternatives

Heroku Scheduler alternative: any cron expression without the add-on

Heroku Scheduler caps you at three cadences and account-wide UTC, and spins a one-off dyno per run. Here is the external cron pattern that gives you any cron expression, per-schedule timezones, and zero per-execution dyno spin-up cost.

Guides

Running an OpenAI sentiment pipeline on a real scheduler

OpenAI batch work needs a clock, not a user session. Here is the scheduled HTTP-route pattern teams use to drain LLM batches at a sustainable rate inside OpenAI's rate limits, with per-task failure alerts.

Reference

Cron syntax cheat sheet with real-world examples

Cron syntax without the math. Every pattern you're likely to reach for (every 5 minutes, weekdays, business hours, first of the month), with a practical example and a link to a free debugger.