A Fargate task for hours of continuous work: it runs until the job's process exits, then stops billing by itself, about $15 a month for a 2.5-hour job twice a day. Lambda durable functions for long jobs made of short steps and waits: an execution lasts up to a year and waiting is free. Both when a durable function waits and a Fargate task does the heavy step. Managed Instances' 90 minutes rarely fits, and Trigger.dev does it all outside the client's AWS account at about six times Fargate's price.
When to pick which
Pick A Fargate scheduled task when
- Hours of continuous work in one process: an import, a report, a migration, a batch of AI calls.
- The end time isn't known: the task stops, and stops billing, when the job's process exits.
- The job needs a container: Chromium, ffmpeg, a vendor CLI, more than 10 GB of memory, up to 200 GB of disk.
Pick Lambda durable functions when
- The long job is a chain of short steps, each finishing well inside 15 minutes, with retries per step.
- It waits for a person or an outside system for hours or days: no compute is charged while it waits, and an execution lasts up to a year.
Pick Lambda Managed Instances when
- Only a large, steady, event-driven Lambda that already runs 15 to 90 minutes and must not be rewritten as a container: at least three instances run around the clock.
Pick Trigger.dev when
- Outside AWS: TypeScript jobs started from an app, mostly waiting or showing live progress, for a team that doesn't want to build retries, a dashboard and a progress channel.
Both, when
- A durable function runs the workflow and its waits, and starts a Fargate task for the heavy step; the task reports back through the function's callback when it's done.
- Not Fargate and Trigger.dev for the same job: they're two homes for the same work, and one of them keeps the data in the client's account.
Questions that change the answer
Does the job run continuously, or is it mostly waiting between short steps? Continuous points to Fargate, waiting to durable functions.
Must the code and the data stay in the client's AWS account, or in Australia? Then it's Fargate or durable functions, not a service outside AWS.
Can the job be restarted or resumed from a checkpoint? Only then is Fargate Spot worth it.
Does anyone wait on the result in an app? A 30 to 90 second container start is fine at 2 am and wrong behind a button.
Doesn't cover
Jobs that need a GPU: Fargate has none. A Runpod pod for experiments, SageMaker training jobs or EC2 on a client's data: Runpod or AWS for a GPU?
The short version
| Fargate scheduled task | Lambda durable functions | Lambda Managed Instances | Trigger.dev | |
|---|---|---|---|---|
| My verdict | Use when it fits: my answer for long jobs | Watching: what I'd pilot | Skip | Skip |
| Longest run | No fixed limit | 1 year in all (asynchronous); 15 minutes per invocation | 90 minutes, asynchronous and event-source calls only | No limit |
| Continuous compute in one go | Hours | 15 minutes | 90 minutes | Hours |
| Billed while waiting | Yes, every second the container is up | No | The instances run anyway | No, on its own waits |
| Billed between runs | Nothing | Nothing | At least three instances, around the clock | Nothing |
| Where it runs | The client's AWS account, Sydney | The client's AWS account, Sydney | The client's AWS account, Sydney | Trigger.dev's machines, no region published |
| Biggest machine | 16 vCPU, 120 GB, 200 GB of disk | 10 GB, about 6 vCPU | Your EC2 instance type | 8 vCPU, 16 GB |
| Pulumi | Official | Official | Official | No provider |
Running for a week isn't staying alive for a week
Two different things hide behind "long-running". A durable job is logically running for days but only uses compute in short bursts: it saves its progress after each step, stops, and resumes later, so a pause of a week costs nothing. A long process stays alive the whole time: a migration copying millions of rows, a video render, a batch of model calls holding state in memory.
Durable functions are built for the first. Fargate is built for the second. Most confusion comes from forcing one into the other: chopping a three-hour import into 15-minute Lambda pieces with their own checkpoints, or keeping a container up for a day to wait on someone's approval.
The lean Fargate setup, when the end time isn't known
Nothing has to stop Fargate when the job ends. EventBridge Scheduler starts one standalone ECS task on a schedule, the container runs the job, the process exits, and the task stops: Fargate bills per second from the image pull to the stop, with a one-minute minimum. No Lambda, no state machine, no "stopper".
Seven rules make that safe:
- A standalone task, never an ECS service. A service keeps a set number of copies running: when your job finishes, it starts another.
- One essential container, nothing else that runs forever. Send logs straight to CloudWatch with the
awslogsdriver, without a sidecar. An essential sidecar that never exits keeps the task, and the bill, alive. - The process must exit. Node.js stays alive while anything is open: a database pool, a timer, a socket. End the job with a clean shutdown and
process.exit(code), 0 on success and anything else on failure. - A kill switch, because ECS has no maximum run time. Start the job through
timeout: after 10 hours it sends SIGTERM, two minutes later a kill. Under the 12-hour gap between runs, two runs can never overlap. A run killed this way exits with code 124. - No NAT gateway. A public subnet, a public IP and a security group with no inbound rules. The address bills only while the task runs, about $0.005 an hour; a NAT gateway bills every hour of the month.
- An alert when it goes wrong. The schedule only knows that RunTask was accepted. An EventBridge rule on ECS task state changes (STOPPED with a non-zero exit code) catches a crash, the timeout kill and a task that never started.
- Regular Fargate, not Spot, for a job whose end you don't know. Spot can stop it with two minutes' notice, so it's only for jobs that save their progress and resume.
In Pulumi, everything is in @pulumi/aws:
const cluster = new aws.ecs.Cluster("jobs");
const logs = new aws.cloudwatch.LogGroup("job", { retentionInDays: 30 });
const task = new aws.ecs.TaskDefinition("job", {
family: "long-job",
requiresCompatibilities: ["FARGATE"],
networkMode: "awsvpc",
cpu: "2048",
memory: "4096",
runtimePlatform: { cpuArchitecture: "ARM64", operatingSystemFamily: "LINUX" },
executionRoleArn: executionRole.arn, // pulls the image, writes the logs
taskRoleArn: jobRole.arn, // what the job itself may touch
containerDefinitions: pulumi.jsonStringify([{
name: "job",
image: imageUri,
essential: true,
// Kill switch: SIGTERM after 10 h, SIGKILL 2 min later. Under the 12 h gap, so runs never overlap.
command: ["timeout", "-k", "120", "36000", "node", "dist/job.js"],
stopTimeout: 120,
logConfiguration: {
logDriver: "awslogs",
options: { "awslogs-group": logs.name, "awslogs-region": region, "awslogs-stream-prefix": "job" },
},
}]),
});
new aws.scheduler.Schedule("job-twice-daily", {
scheduleExpression: "cron(0 6,18 * * ? *)",
scheduleExpressionTimezone: "Australia/Sydney",
flexibleTimeWindow: { mode: "OFF" },
target: {
arn: cluster.arn,
roleArn: schedulerRole.arn, // ecs:RunTask on the task definition, iam:PassRole on both roles
retryPolicy: { maximumRetryAttempts: 3 },
ecsParameters: {
taskDefinitionArn: task.arn,
launchType: "FARGATE",
networkConfiguration: { subnets: publicSubnetIds, securityGroups: [noInboundSg.id], assignPublicIp: true },
},
},
});
// A crash, the timeout kill (exit 124) or a task that failed to start: send it to an SNS topic you're subscribed to.
new aws.cloudwatch.EventRule("job-failed", {
eventPattern: pulumi.jsonStringify({
source: ["aws.ecs"],
"detail-type": ["ECS Task State Change"],
detail: { clusterArn: [cluster.arn], lastStatus: ["STOPPED"], containers: { exitCode: [{ "anything-but": 0 }] } },
}),
});
The timeout form with seconds works with coreutils (Debian-based node:*-slim images) and recent BusyBox (Alpine). The schedule avoids 2 to 3 am, the hour Sydney skips the night daylight saving starts.
What it costs: 2 vCPU and 4 GB on Arm in Sydney is $0.09474 an hour. A job of about 2.5 hours twice a day is about 150 hours a month: about $15, plus about $0.75 for the public address and a few cents of logs and image storage. Nothing between runs.
If you'd rather have a timeout built in than the timeout wrapper, AWS Batch on Fargate has job timeouts and retries. It costs three more resources (a compute environment, a job queue, a job definition) for what one word in the command does, so I'd only reach for it once there are many such jobs.
Durable functions, when the job mostly waits
When the long part is waiting (an approval, a partner's callback, a rate limit to wait out), a container that stays up to wait is the wrong tool. A durable function saves each completed step, suspends during context.wait() or waitForCallback() without compute charges, and resumes on the next line days later. Each invocation still stops at 15 minutes, so any one step must fit inside that. The rules of replay, and the comparison with Step Functions, are in Durable functions or Step Functions?.
The two combine well. The durable function runs the workflow, starts a Fargate task for the step that needs an hour of CPU, and waits on a callback; the task's last act is to call that callback with its result.
Why not Managed Instances and their 90 minutes
Since 9 September 2026, Lambda Managed Instances allow a 90-minute timeout. Read the small print first: only asynchronous and event-source calls get it (synchronous calls and the init phase stay at 15 minutes), and the instances behind it don't scale to zero, three by default, around the clock. For a job twice a day, that's paying for machines all day to run them for three hours. Behind SQS it also means a visibility timeout of at least six times the function timeout: nine hours for a 90-minute function.
It only makes sense for a large, steady, event-driven Lambda that already runs that long and that a team won't rewrite as a container. For anything new, a Fargate task has no limit at all and bills nothing between runs.
Fargate or Trigger.dev?
Trigger.dev runs long TypeScript tasks on its own machines, with retries, free pauses and live progress built in. Here is the same work on both: Fargate at Sydney Arm rates plus the public address, Trigger.dev at its published prices.
| Machine, per hour | Fargate | Trigger.dev | Ratio |
|---|---|---|---|
| 0.5 vCPU (Fargate 1 GB, Trigger.dev 0.5 GB) | $0.029 | $0.122 | 4.2× |
| 1 vCPU, 2 GB | $0.052 | $0.306 | 5.9× |
| 2 vCPU, 4 GB | $0.100 | $0.612 | 6.1× |
| 4 vCPU, 8 GB | $0.195 | $1.224 | 6.3× |
| 8 vCPU, 16 GB | $0.384 | $2.448 | 6.4× |
| Job, per month | Fargate | Trigger.dev |
|---|---|---|
| 2.5 hours twice a day, 2 vCPU and 4 GB (150 hours) | about $15 | about $92 |
| A 2-hour nightly import, 2 vCPU and 4 GB (60 hours) | about $6 | about $37 |
| A daily fan-out to 500 two-minute pieces, 0.5 vCPU | about $15, but RunTask starts 10 tasks per call and you build the "wait for all 500" | about $61, with one batchTriggerAndWait call; the parent isn't billed while it waits |
| 3,000 runs of 10 minutes' compute plus a 4-hour wait for a person's approval, 1 vCPU and 2 GB | about $655 if the task stays up; about $26 split into two tasks with the state stored in between | about $153: the run is saved during the wait |
On raw compute Fargate always wins. Trigger.dev only wins when Fargate is left up through a wait, and on AWS a durable function does that waiting for free.
What Trigger.dev does that Fargate doesn't, without building it:
- Pauses mid-job with the process saved, resuming on the next line with its variables. Only its own waits are free; an ordinary
awaiton an API is still billed. - Live progress in the app: status, metadata and even an LLM's output streamed from the task to the browser.
- Starts in a fraction of a second (100 to 300 ms warm, its own figure) against 30 to 90 seconds for a Fargate task: what matters behind a button.
- Retries, history and a dashboard per run, with reruns from the dashboard, queues with limits per customer, and preview environments per branch.
What Fargate does that Trigger.dev doesn't: the code and the data stay in the client's AWS account, in Sydney, with an IAM role and direct access to the private database. It goes up to 16 vCPU, 120 GB and 200 GB of disk, runs any language or binary as it is, is in Pulumi officially, and has no plan cap on concurrent runs.
That's why it's a skip for me. For a startup that isn't on AWS and starts its jobs from an app, Trigger.dev's plumbing is worth its price.
And Inngest?
Inngest isn't compute: it orchestrates steps in code that runs on your own hosting, with the same replay model as durable functions. On Lambda, each step still stops at 15 minutes, so it doesn't solve a long process either. Its strengths are flow control per tenant and its dashboard, and it fits apps that aren't on AWS.
Sources, checked on 30 October 2026: Fargate pricing, scheduling ECS tasks with EventBridge Scheduler, the 90-minute timeout on Lambda Managed Instances, AWS Compute Blog on the 90-minute timeout, Lambda durable functions, Trigger.dev pricing, Trigger.dev v4, Inngest pricing.