None of the three (Batch, Future, Queueable) can be passed directly to System.schedule — only Schedulable classes can. But each behaves differently when invoked from a Schedulable’s execute. The one that causes the most issues under heavy schedules is Batch Apex.
Quick comparison
| Async type | Schedulable directly? | Invoke from Schedulable.execute? | Common issues when scheduled |
|---|---|---|---|
| Future | No | Allowed | Cannot be uniquely “scheduled” — each invocation is one-shot. Fewer issues, but limited capability. |
| Queueable | No | Allowed | Generally well-behaved. Watch the 50-per-transaction enqueue limit. |
| Batch Apex | Yes (if dual-implements Schedulable) | Allowed via Database.executeBatch | Apex Flex Queue can fill up; 100-Holding cap is the most common failure. |
Why Batch causes the most scheduling issues
1. Flex Queue cap
Batch jobs use the Apex Flex Queue: 5 Processing + 100 Holding = 105 max. If your scheduled batch runs faster than it completes — say, scheduled every 10 minutes but takes 15 minutes to run — jobs pile up. After a few hours, the queue fills:
System.LimitException: Attempt to add a job to the apex flex queue failed because the queue is full.
2. Overlapping runs
public class OverlappingBatch implements Schedulable {
public void execute(SchedulableContext ctx) {
Database.executeBatch(new HeavyBatch(), 200);
}
}
System.schedule('Every 5 minutes', '0 */5 * * * ?', new OverlappingBatch());
If HeavyBatch takes 7 minutes, run #2 starts before run #1 finishes — both compete for the same queue slot. After a few hours, dozens of batches are Holding, none completing.
3. Long-running batch blocks deploys
You can’t deploy changes to a class that’s actively scheduled or running. A long-running scheduled batch can keep a developer blocked.
Why Future scheduling is “limited”
@future cannot be scheduled directly because it’s not a class — it’s a method annotation. You can only invoke a future method from inside a Schedulable’s execute:
public class FutureCaller implements Schedulable {
public void execute(SchedulableContext ctx) {
MyService.doStuffAsync(getIds());
}
}
public class MyService {
@future
public static void doStuffAsync(List<Id> ids) { /* ... */ }
}
Issues with this pattern:
- Only 50 future calls per transaction — fine for one call from a Schedulable, but tighter than other options.
- Future cannot make sObject parameters — only primitives.
- No JobId, no chaining — once enqueued, you can’t track its position or compose multiple steps.
For new code, prefer Queueable over future every time.
Why Queueable is the safest
Queueable from a Schedulable is the modern recommended pattern:
public class QueueableCaller implements Schedulable {
public void execute(SchedulableContext ctx) {
System.enqueueJob(new MyQueueable());
}
}
- No Flex Queue contention (Queueable uses a separate, much larger queue).
- Returns a JobId for tracking.
- Can chain.
- Accepts sObjects and complex types.
- Callouts via
Database.AllowsCallouts.
The only Queueable failure mode under schedules is daily async limit exhaustion (250,000 jobs/24 hours), and that affects all three types equally.
What an interviewer is testing
The question is really “do you know that:
- Only Schedulable can be passed to
System.schedule? - Batch Apex needs a wrapper and contends with the Flex Queue?
- Future is awkward to schedule and outdated?
- Queueable is the cleanest async to invoke from a schedule?”
Common interview follow-ups
- Why can’t you schedule a future method? — Future is a method annotation, not a class.
System.schedulerequires a class instance. - What’s the workaround for scheduling a future? — Wrap in a Schedulable; call the future from
execute. - Which is best practice in 2026? — Schedulable invoking Queueable. Use Batch only when you need 50K+ records processed asynchronously.
Verified against: Apex Developer Guide — Asynchronous Apex Limits. Last reviewed 2026-05-17.