knitr::opts_chunk$set(eval = FALSE)
Cloud is not magic. It has overhead, costs money, and AWS CPUs are slower per-core than a modern laptop like an M4 Pro. But with the right instance types and spot pricing, cloud can be both faster and cheaper than you expect.
Cloud wins when:
Local wins when:
These are real measured runs from bench/benchmark.R (see bench/README.md) on
live AWS — full cold starts, nothing excluded. They're chosen to show both sides, so
you can calibrate when bursting is worth it. Same setup for both: staRburst 0.3.9,
us-east-1, EC2 c7i.xlarge Spot, cold start, sequential baseline on an Apple M4 Pro
(Mac16,11; 12 cores = 8 performance + 4 efficiency, 48 GB, macOS 26.5.2, R 4.6.1),
2026-07-21.
30 tasks, each ~10 s of dense linear algebra (repeated SVD):
| Phase | Time | |-------|------| | Local (sequential, 1 pass over 30 tasks) | 319.3 s (5.3 min) | | Cloud: startup (provision + image pull) | 87.5 s | | Cloud: compute + collect (30 workers) | 38.4 s | | Cloud total (cold) | 125.8 s (2.1 min) | | Est. cost | $0.06 |
~2.5× faster even counting the full ~90 s cold startup — because the 30 tasks finish in parallel in ~38 s instead of ~5 min sequentially. Run this as a warm pool or a parameter sweep and the startup amortizes away, pushing the speedup higher.
20 tasks, each a fraction of a second (small terrain-stat grids):
| Phase | Time | |-------|------| | Local (sequential) | 0.1 s | | Cloud: startup | 74.7 s | | Cloud: compute + collect | 27.9 s | | Cloud total (cold) | 102.6 s | | Est. cost | $0.03 |
~1000× slower than local. The work is trivial, so the fixed startup is pure overhead with nothing to amortize it against. Running this locally is the right call.
Same tool, same 20–30 workers, opposite verdicts. The only thing that changed is how much work each task does. If your tasks are seconds-to-minutes each and you have many of them, staRburst wins; if they're sub-second, stay local. The rest of this guide makes that boundary precise.
Once workers are running, each task incurs a small fixed overhead from S3 data transfer:
| Task Duration | Overhead % | Efficiency | |--------------|------------|------------| | 1 second | 200–300% | Negative | | 10 seconds | 20–30% | Moderate | | 30 seconds | 7–10% | Good | | 60 seconds | 3–5% | Excellent | | 5 minutes | <1% | Optimal |
Sweet spot: tasks that run for 2–10 minutes each. For faster tasks, batch them.
The most important optimization. Instead of sending 10,000 tiny tasks, group them:
# Bad: 10,000 tasks of 0.1s each — overhead dominates starburst_map(1:10000, quick_fn, workers = 100)
# Good: 100 tasks of 100s each — overhead is negligible batches <- split(1:10000, ceiling(seq_along(1:10000) / 100)) starburst_map(batches, function(batch) lapply(batch, quick_fn), workers = 100)
Batch size formula:
# Profile your function first per_item_time <- 0.5 # seconds, from local profiling target_task_duration <- 60 # aim for 60s minimum per task batch_size <- ceiling(target_task_duration / per_item_time) # Result: 120 items per batch
The default instance type is c7g.xlarge (Graviton, ARM64) — provisioned
automatically by starburst_setup(). The types below are strong alternatives:
| Instance | Architecture | Price/Perf | Best For | |----------|-------------|------------|----------| | c8a | AMD 8th gen | ★★★★★ | Recommended — best overall price/perf | | c8g | Graviton4 ARM | ★★★★ | Best ARM64 option | | c7a | AMD 7th gen | ★★★★ | Proven, stable | | c8i | Intel 8th gen | ★★★ | High single-thread needs |
Custom instance types provision on first use.
starburst_setup()provisions only the default (c7g.xlarge). The first time you use a different type, staRburst automatically creates its capacity provider (a one-time ~1–2 min step, no instances launched by provisioning). To pre-provision explicitly, runstarburst_setup_ec2(instance_types = "c8a.xlarge")once.
# Recommended alternative: c8a with spot instances (auto-provisioned on first use) plan(starburst, workers = 50, instance_type = "c8a.xlarge", # AMD 8th gen — best price/performance use_spot = TRUE # 70% cheaper than on-demand )
Spot vs on-demand (50 workers, us-east-1):
c8a on-demand: $7.20/hr c8a spot: $2.16/hr ← 70% savings, low interruption risk
Workers need ~75–90 s to start cold (pool warmup + image pull + environment sync). This cost is fixed — it doesn't scale with job size. The key is to amortize it. The numbers below extrapolate from the measured "heavy" run above (5.3 min local vs ~90 s startup + ~38 s compute on 30 workers).
Local (sequential): 5.3 min, $0 Cloud 30 workers (cold): 2.1 min ($0.06) [87s startup + 38s compute] → ~2.5x faster, laptop freed — worth it, but startup is most of the cloud time
Local (sequential): 53 min (10 × 5.3), laptop tied up Cloud (pool stays warm): ~8 min (90s startup ONCE + 10 × ~38s compute) → Startup paid once, then each run is ~38s. Big win, laptop stays usable.
Warm pool started once in the morning; every run pays ~0 startup. → Per-run cost is just compute+collect (~38s here); startup overhead ≈ 0%. → Best case for staRburst — see the "keep warm pool" pattern below.
The real question isn't total job length — it's per-task work vs. the ~90 s cold startup, and how many times you'll run it. The measured examples above bracket it: the heavy workload won at ~5 min of sequential work; the trivial one lost badly. As a rule of thumb for a single cold run:
Per-task work is sub-second: Stay local (startup is pure overhead) Tasks are seconds–minutes, and there are many: Cloud wins — this is the sweet spot Total sequential work < ~2 min: Local usually wins on a cold start
Running it more than once changes everything — a warm pool pays the ~90 s startup once, so even modest jobs win on the 2nd+ run (see below).
How many times will you run it?
Once: Startup is ~20% of job time 2–5 times: Startup amortizes to ~5–10% 10+ times: Startup < 2% Daily: Keep warm pool, effectively zero
What's your local hardware?
4–6 cores: Cloud dominates 8–10 cores (M4): Cloud wins but it's closer 16+ cores: Cloud might not win on speed HPC cluster: Use your cluster
library(parallel) cl <- makeCluster(detectCores() - 1) results <- parLapply(cl, data, your_function) stopCluster(cl)
# Pay startup cost once, run many combinations for (alpha in seq(0.1, 1.0, 0.1)) { for (beta in seq(0.1, 1.0, 0.1)) { results <- starburst_map( data, function(x) model(x, alpha = alpha, beta = beta), workers = 50 ) } }
# Start warm pool once in the morning plan(starburst, workers = 50, warm_pool_timeout = 28800) # 8 hours # All runs during the day start in < 30s results_am <- starburst_map(morning_data, process) results_pm <- starburst_map(afternoon_data, process) # Pool shuts down automatically after 8 hours of inactivity
# Iterate quickly on a small sample locally results_test <- lapply(data[1:100], your_function) # When logic is right, scale to full dataset on cloud results_full <- starburst_map(data, your_function, workers = 100)
Quick formula (EC2 spot, us-east-1):
Cost ≈ workers × hours × $0.044/worker/hour
| Job size | Workers | Wall time | Spot cost | |----------|---------|-----------|-----------| | Small (1 hr sequential) | 10 | ~6 min | ~$0.04 | | Medium (5 hr sequential) | 25 | ~12 min | ~$0.22 | | Large (10 hr sequential) | 50 | ~25 min | ~$0.92 |
Use starburst_estimate() for a precise estimate before running.
Too many small tasks:
# Bad: each task is 0.1s — overhead is 20-30x the work starburst_map(1:10000, function(x) sqrt(x), workers = 100) # Good: batch into groups of 100 batches <- split(1:10000, ceiling(seq_along(1:10000) / 100)) starburst_map(batches, function(b) sapply(b, sqrt), workers = 100)
More workers than tasks:
# Bad: 40 workers sit idle starburst_map(1:10, fn, workers = 50) # Good: match workers to workload starburst_map(1:100, fn, workers = 25) # 4 tasks per worker
Sending large data to every worker:
# Bad: huge_matrix serialized and sent to each of 50 workers huge_matrix <- matrix(rnorm(1e8), ncol = 1000) starburst_map(1:50, function(i) process(huge_matrix, i), workers = 50) # Good: generate data inside the worker starburst_map(1:50, function(i) { data <- generate_chunk(i) # create data on the worker process(data) }, workers = 50)
| Use Case | Task Duration | Batch Size | Workers | Expected Speedup | |----------|--------------|------------|---------|------------------| | Fast calculations | 0.001s | 1000+ per task | 20–50 | 3–8x | | API calls | 0.5–2s | 20–100 per task | 20–50 | 8–15x | | Data processing | 10–60s | 5–20 per task | 20–50 | 12–20x | | Report generation | 60–300s | 1–5 per task | 20–50 | 15–25x | | Model training | 2–10 min | 1–2 per task | 20–50 | 18–30x |
staRburst uses the paws AWS SDK, which supports the full AWS credential chain:
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKENAWS_PROFILE=myprofile to select a profile from ~/.aws/credentialsaws login: supported via named profiles configured with SSO in ~/.aws/config (requires AWS CLI v2, aws login available since November 2025)# Standard profile export AWS_PROFILE=my-aws-account Rscript -e "library(starburst); starburst_setup_ec2()" # SSO profile (AWS CLI v2) aws login --profile my-sso-profile export AWS_PROFILE=my-sso-profile Rscript -e "library(starburst); starburst_setup_ec2()"
No explicit configuration is required in staRburst — it defers entirely to paws credential discovery.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.