Scheduled Jobs
Work that happens on a timer costs differently from work that happens on request. A nightly report runs for seconds and then nothing happens for a day, and a container sized for that job spends almost all of its life idle. This page is about the architecture that fits demand like that, and about the point where it stops being the right answer.
The Architecture
| Component | What it does |
|---|---|
| nightly-report | The function, invoked on a cron schedule |
| db | The data it reads |
| reports | Object storage for the files it writes |
{
"version": "2.0",
"platform": "aws",
"components": [
{ "type": "Network", "name": "network" },
{ "type": "Database", "name": "db" },
{ "type": "Bucket", "name": "reports" },
{
"type": "Function",
"name": "nightly-report",
"runtime": "python3.12",
"handler": "main.handler",
"schedule": "cron(0 3 * * ? *)",
"memory": 512,
"timeout": 120,
"source": {
"git": {
"url": "github.com/you/app",
"context": "jobs/report",
"commands": [
"pip install -r requirements.txt -t .",
"zip -r report.zip ."
],
"artifact": "report.zip"
}
}
}
],
"connections": [
{ "from": "network", "to": "db" },
{ "from": "network", "to": "nightly-report" },
{ "from": "nightly-report", "to": "db" },
{ "from": "nightly-report", "to": "reports" }
]
}A function runs outside the network by default and can still reach the public internet, so network → nightly-report is here for one reason: reaching the database is only allowed once the function is attached, and the same is true of a volume. A job that only calls an API and writes to a bucket needs no network at all.
The code is a file jobs/report/main.py exporting handler(event, context): it reads the database, writes a file to the bucket, and returns.
The Wiring
The schedule is a field, not a service. cron(0 3 * * ? *) fires at 03:00 UTC daily, and rate expressions such as rate(1 hour) work the same way. The platform arranges the invocation for you, so there is no scheduler to run, no crontab on a machine someone has to remember, and the cadence is versioned and reviewed alongside everything else. Remove the field and the same function becomes on-demand compute.
The artifact is built from your repository. For a zip-packaged function, commands run inside context and must produce the file named in artifact, which here means installing dependencies into the folder and zipping it. runtime and handler then tell the platform which interpreter to start and which function to call, so main.handler is the handler in main.py. If you would rather ship a container image, setting the package type to image swaps the whole mechanism for a Dockerfile build and the handler comes from the image’s command instead.
The connections behave as they do for a container, with two exceptions worth knowing. nightly-report → reports injects REPORTS_BUCKET and grants access to that bucket. nightly-report → db injects the five DB_* variables and opens the path to the database. A function connected to a Secret is the second exception: it receives the secret’s ARN in <SECRET>_SECRET_ARN plus permission to read that one secret, and fetches the value itself when it runs. A serverless runtime has nowhere safe to hold it in advance, and baking it into the configuration would expose it.
Notice how little the file says about the function beyond its own definition. Its schedule, its size, and what it may reach are all here. The one placement decision left is whether it sits inside the network, and that follows from what it has to reach rather than from where you would like it to run.
The Decisions Behind It
Demand picks the compute. A container is right for anything answering requests through the day, because it is already warm and the cost is the same whether it is busy or not. A function is right for work that is triggered and finishes, because it costs nothing between invocations. What you pay for that is a cold start on the first call and a hard ceiling on how long one invocation may run.
Several small functions rather than one large one. Each function has its own schedule, its own logs, and its own failure. A single job that syncs, reports, and cleans up gives you one log stream in which three unrelated failures look identical, and one timeout budget shared between them.
The schedule belongs to the function, not to a scheduler. The alternative is a long-running process whose job is to wake up and call things, which is another service to deploy, monitor, and pay for while it waits. Attaching the cadence to the work itself removes that middleman, and it means deleting the job deletes its schedule with it.
No compute in this file runs between 03:00 and 03:00 the next day, which is the whole argument for a function. The database it reads is the component still on the clock.
The Limits
The timeout is the wall. timeout is seconds, 120 here and 30 if you leave it out, so a job that grows past the number you set does not degrade, it fails. When that happens the work is no longer a function: it belongs in a long-running worker container, which is an ordinary Container that declares no ports so nothing can route a request to it, or it needs splitting into invocations small enough to finish.
Two smaller edges follow from the same model. memory is MiB, 512 here against a default of 128, and it sets the proportional share of CPU with it, so a slow job is sometimes a memory setting rather than a slow algorithm. Note it is a plain integer here and one of four literal strings on a container. And a scheduled invocation carries an empty payload with no notion of what happened last time, so anything resembling progress or a cursor has to live in the database or the bucket. That is why this architecture pairs so naturally with The Containerized Web App: the application produces the data and the state, and the function reads both.