f <- function(x) 2*x
y <- f(1)
z <- f(3) @HenrikBengtsson

Parallel & distributed processing can be used to:
We may choose to parallelize on:
We can measure the processing time of our code using system.time().
We can measure the processing time of our code using tic()/toc() from the futureverse package:

We can get a progress bar by piping our calls to progressify().
Default output (using the built-in utils::txtProgressBar()):
> ys <- lapply(1:10, slow_sqrt) |> progressify()
|============================= | 80%
Progress bar is erased on completion.
We can customize the style of the progress bar using the cli package.
Output:
> ys <- lapply(1:10, slow_sqrt) |> progressify()
■■■■■■■■■■■■■■■■■■■■■■■■■ 80% | ETA: 1s
Progress bar is erased on completion.
We can combine multiple progress handlers:
Output:
> ys <- lapply(1:10, slow_sqrt) |> progressify()
■■■■■■■■■■■■■■■■■■■■■■■■■ 80% | ETA: 1s
with ♫♪♬♫ … ♬♫
Progress bar is erased on completion.
By default, progress bars are erased on completion. This feature can be disabled, but to keep things simple, we will use a special “tutorial” handler today:
Output:
> ys <- lapply(1:10, slow_sqrt) |> progressify()
[ 5s] | [============================] 100% [10/10] (ETA: 0s)
>
Progress bar remains beyond completion.
By default, the progress bars are cleared at each update, and also at the very end. Because of this, we will use a special “tutorial” handler for this workshop:
Output:
> ys <- lapply(1:10, slow_sqrt) |> progressify()
[ 0s] - [-----------------------------] 0% [0/10] (ETA: ?s)
[ 1s] \ [==>--------------------------] 10% [1/10] (ETA: 5s)
[ 1s] | [=====>-----------------------] 20% [2/10] (ETA: 4s)
[ 2s] / [========>--------------------] 30% [3/10] (ETA: 4s)
[ 2s] - [===========>-----------------] 40% [4/10] (ETA: 3s)
[ 3s] \ [==============>--------------] 50% [5/10] (ETA: 3s)
[ 3s] | [=================>-----------] 60% [6/10] (ETA: 2s)
[ 4s] / [====================>--------] 70% [7/10] (ETA: 2s)
[ 4s] - [=======================>-----] 80% [8/10] (ETA: 1s)
[ 5s] \ [==========================>--] 90% [9/10] (ETA: 1s)
[ 5s] | [============================] 100% [10/10] (ETA: 0s)
>
Progress output remains beyond completion.
Mathematics:
\[ \begin{aligned} f(x) &= 2 \cdot x \\ y &= f(1) \\ z &= f(3) \end{aligned} \]
A function has:
2*xxA function has:
b*xx and bWhen we call the function:
R evaluates the code with the values of arguments x (= 4) and b (= 2), returns the value, and assigns the value to variable y.
When we call the function:
R evaluates the code with the value of argument x (= 4), the default value of b (= 2), returns the value, and assigns the value to variable y.
\[ h(\mathbf{x}) = \sum_{i=1}^{n} x_i ; \mathbf{x} = (x_1, x_2, \ldots, x_n) \\ \]
\[ h(\mathbf{x}) = \sum_{i=1}^{n} x_i ; \mathbf{x} = (x_1, x_2, \ldots, x_n) \\ \]
\[ h(\mathbf{x}) = \sum_{i=1}^{n} x_i \]
\[ \phantom{h(\mathbf{x})} = \sum_{i=1}^{k} x_i + \sum_{i=k+1}^{n} x_i \\ \]
\[ \phantom{h(\mathbf{x})} = h(\mathbf{x}_{1:k}) + h(\mathbf{x}_{(k+1):n}) \]
\[ \begin{aligned} h(\mathbf{x}) &= \sum_{i=1}^{n} x_i \\ \mathbf{x} &= (1, 2, \ldots, 10) \end{aligned} \]
\[ y = h(\mathbf{x}) \]
\[ \begin{aligned} a &= h(\mathbf{x}_{1:5}) \\ b &= h(\mathbf{x}_{6:10}) \\ y &= a + b \end{aligned} \]
\[ y = 55 \]
\[ \begin{aligned} a &= h(\mathbf{x}_{1:5}) \\ b &= h(\mathbf{x}_{6:10}) \\ y &= a + b \end{aligned} \]
\[ y = 55 \]
\[ \text{concurrently} \left\{\begin{matrix} a = h(\mathbf{x}_{1:5}) \\ b = h(\mathbf{x}_{6:10}) \\ \end{matrix}\right. \\ \hspace{10ex} y = a + b \]
How can we calculate \(a\) and \(b\) concurrently in R?
a and b take 5 seconds each => everything takes ~5 seconds
%<-% is called the future-assignment operatorslow_sum()slow_sum()Create \(x = (1, 2, ..., 10)\) in R
Call y <- slow_sum(x) and confirm it takes 10 seconds
Call a <- slow_sum(x[1:5]) and confirm it takes 5 seconds
Same for b <- slow_sum(x[6:10])
Confirm correct result
Confirm that it finishes in 5 seconds - use tic()/toc()
How long does each line take? - use tic()/toc()
Something else?
Note that:
is the same as:
which is not what we wanted, i.e.

Friedman & Wise (1976, 1977), Hibbard (1976), Baker & Hewitt (1977)
Verify result
Verify total run time
Verify run time per line

Low friction:
Static-code inspection by walking the abstract syntax tree (AST):
=> globals identified and exported to the worker:
slow_sum() - a function (also searched recursively)x - a numeric vector of length 100For-loop:
Base R:
Plyr:
Foreach:
Base R:
is identical to
R can’t tell the difference! (e.g. try with lobstr::ast(...))

Task: Create a function ys <- parallel_map(xs, slow_sqrt)
Combine the idea of:
with:
to make a “concurrent” for-loop.
Hint: You need two for-loops - one for future() and one for value()
Building blocks:
Compare how fast:
is compared to:
system.time() or tic()/toc()lapply(fs, value) resolves futures sequentially:
lapply() still blocks waiting for the first and second futures to finish.Vectorized value(fs) is aware of all futures:
value() immediately throws the error.ys <- parallel_map(xs, slow_sqrt)
future() and value() are very powerfulparallel_map()
Parallel alternatives to traditional, sequential functions:
futurize() is new since January 2026Our ys <- map(xs, slow_sqrt) |> futurize() can be resolved on lots of different backends - it doesn’t matter where!
On the current machine
plan(sequential)
sequentially (default)
plan(multisession)
plan(multisession, workers=4)
in parallel via background R workers
plan(future.mirai::mirai_multisession)
plan(future.mirai::mirai_multisession, workers=4)
in parallel via future.mirai; low latency
plan(future.callr::callr)
plan(future.callr::callr, workers=4)
in parallel via future.callr; all memory is returned as each future is resolved
On other machines
plan(cluster, workers=c("n1", "m2.uni.edu", "vm.cloud.org"))
distributed on other local or remote computers
plan(future.mirai::mirai)
distributed via future.mirai daemon queue
On high-performance compute (HPC) cluster
In parallel on HPC job schedulers for long-running tasks with higher latency.
You can combine progressify() and futurize() to get progress reporting in parallel:
Output:
ys <- lapply(xs, slow_sqrt) |> progressify() |> futurize()
■■■■■■■■■■■■■■■■■■■■■■■■■ 80% | ETA: 1s
Progress is reported in a near-live fashion.
Sequential:
Built-in multisession:
Add-on mirai-based multisession:
Run:
Then benchmark with:
plan() without argumentsRun:
Then benchmark with:
What type of results may this function produce?
Random numbers need extra care when tasks run in parallel.
Using rnorm() without declaring it:
Warning message:
UNRELIABLE VALUE: Future (...) unexpectedly generated random numbers
without specifying argument 'seed'. There is a risk that those random
numbers are not statistically sound and the overall results might be
invalid. To fix this, specify 'seed = TRUE'. ...
Declaring parallel-safe RNG with seed = TRUE:
Why it matters:
seed = TRUE uses the parallel-safe L’Ecuyer-CMRG generator to give each task its own independent, reproducible streamParallelization adds overhead - it is not always a win.
Parallelization can be slower when:
sqrt(xs) beats any parallel maptic()/toc(), don’t assumeSome objects are tied to your current R session and cannot be exported to a worker:
Error: Detected a non-exportable reference in the globals:
'con' of class 'SQLiteConnection' uses an external pointer
Enable early detection with options(future.globals.onReference = "error").
library(futurize); plan(multisession, workers = 8)
library(progressify); handlers("tutorial")
# Simulate dataset
n <- 5000
data <- data.frame(y = rbinom(n, 1, 0.5), x1 = rnorm(n), x2 = rnorm(n))
# Fit logistic regression
fit_glm <- function(data, indices) {
coef(glm(y ~ x1 + x2, data = data[indices, ], family = binomial))
}
# Bootstrap
b <- boot::boot(data, statistic = fit_glm, R = 1000) |> progressify() |> futurize()Popular statistics & ML packages support futurize() - no new API to learn:
# Bootstrap ({boot})
boot::boot(data, statistic, R = 1000) |> futurize()
# Cross-validation ({glmnet})
glmnet::cv.glmnet(x, y) |> futurize()
# Model tuning ({caret})
caret::train(y ~ ., data = d, method = "rf") |> futurize()
# Additive models ({mgcv})
mgcv::bam(y ~ s(x1) + s(x2), data = d) |> futurize()
# Mixed-model bootstrap ({lme4})
lme4::bootMer(fit, statistic, nsim = 1000) |> futurize()Your existing call, plus one pipe - it runs on whatever backend plan() you chose.
futurize() doesn’t parallelize your call directly - it rewrites (“transpiles”) it into the equivalent Futureverse call, then evaluates that.
Pass eval = FALSE to see the generated code instead of running it:
The same holds for domain-specific packages - inspect what boot generates:
local({
cl <- do.call(future::makeClusterFuture, args = list(seed = FALSE,
globals = TRUE, stdout = TRUE, conditions = "condition",
label = "fz:boot::boot"))
oopts <- options(future.ClusterFuture.clusterEvalQ = "ignore")
on.exit(options(oopts))
boot::boot(data, statistic, R = 1000, parallel = "snow",
ncpus = 2L, cl = cl)
})Each supported package has its own transpiler mapping the sequential call to a parallel backend.

Sequential
processing
Parallelization
with 4 workers
Nested parallelization
with 5 workers each
running 3 parallel tasks
library(futurize)
## 1. Choose WHERE to run (once)
plan(sequential) # default - no parallelism
plan(multisession, workers = 4) # background R workers
plan(cluster, workers = c("n1", "n2")) # other machines
## 2. Parallelize - just add a pipe
ys <- lapply(xs, fcn) |> futurize()
ys <- purrr::map(xs, fcn) |> futurize()
ys <- foreach(x = xs) %do% fcn(x) |> futurize()Avoid Resource Waste:
Monitor Resources:
futurize()The name foreach() tricks us into believing it’s a for-loop
!=

This does not work, because the <- assignment is done inside a function
This does not work, because the <- assignment is done inside a function