DEV Community

Cover image for We Built a Blender Render Farm Out of Four Laptops
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

We Built a Blender Render Farm Out of Four Laptops

Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. Star us to help devs discover the project, give it a try, and share your feedback to help improve the product.

We built a render farm out of four laptops on a Saturday evening.

It had a job queue, a web dashboard, a private network spanning three cities' worth of ISPs, and a shared drive that every machine could see.

It worked.

And then it lost a race against my own laptop, rendering alone.

This is the story of that farm, the networking rabbit hole it dropped us into, and why a 130 MB file taking seven minutes to cross the internet ended the whole experiment.

Where we left Miles

If you have been following along, you know the setup.

In part one I rigged a Spider-Man model in Blender without ever having opened Blender before.

In part two Miles moonwalked off a billboard and a Python test caught it.

Since then, the film has grown into a roughly four minute LiveReview explainer in three parts.

That is about 7,300 frames at 30 fps.

And every one of them has to be rendered by something.

360p is a lie we tell ourselves

For review drafts, I render at 360p with EEVEE, Blender's real-time engine, at 8 samples with ray tracing and motion blur switched off.

On my laptop's GTX 1650 that comes out to about one second per frame.

A single part renders in 20 to 50 minutes.

Go make chai, come back, watch Miles swing.

Totally fine.

Then you want the real thing.

1080p, ray traced reflections on the wet street, proper sample counts.

So I asked Claude to stop guessing and time actual frames from our scene.

Bar chart of whole film render time at four quality levels, from about 2 hours at 360p to about 7 days with Cycles, plus a panel showing 2 workers are 1.3x faster and 3 workers are slower

The 1080p EEVEE versions we actually wanted sit between 10 and 22 hours for the whole film.

Cycles, the path tracer, came in at 76 to 90 seconds per frame.

That is a week.

Of my laptop doing nothing else.

With 6 GB of disk left.

"Is it not even using the GPU?"

That is the first question anyone asks, and it is a fair one.

So we sampled nvidia-smi during a render.

Blender was on the card, using about 1.5 GB of the 4 GB VRAM, at roughly 44% utilization.

At 360p the GPU finishes its part of the frame quickly and then sits there waiting for the CPU, which is busy posing the characters, evaluating the crowd and writing the JPEG.

So the next obvious move was to run more Blender processes in parallel.

Two workers were only 1.3x faster than one.

Three workers were slower than one, because three Blender processes fighting over eight cores and one GPU is not parallelism, it is a custody battle.

The only free speed-up we found locally was rendering each frame range as a single animation call instead of calling render() once per frame, which skips redoing scene setup every time.

That bought about 22% on one worker.

Nice, but nobody turns a 22 hour render into an overnight one with a 22% discount.

The conclusion was pretty clear.

We did not need a faster script. We needed more GPUs.

And there were three other laptops in the team, mostly idle on a Saturday evening.

Enter Flamenco

Flamenco is Blender's own open source render farm software.

It is a lot smaller than the phrase "render farm software" suggests.

There are exactly two programs:

Flamenco Manager is the brain.

It holds the job queue, serves a web dashboard, and splits each job into tasks.

Flamenco Worker is the muscle.

It runs on each rendering machine, polls the manager for work, launches Blender in the background, and reports progress.

Both are single Go binaries you download, extract and run.

No database server, no Kubernetes, no YAML archaeology.

When you submit a job, say "render frames 2370 to 2429 of this .blend", the manager cuts the range into chunks of N frames.

Each chunk becomes a task.

Every idle worker grabs the next task, renders it, and asks for another, so a fast machine naturally ends up doing more of the work than a slow one.

While a task runs, the worker keeps pinging the manager with a "may I keep running?" check, which is how cancelling a job actually reaches a laptop in another city.

If a task fails, it gets retried on a different worker.

After three failures on the same job, that worker gets blocklisted for the job so it stops eating chunks.

Here is the farm we built:

flowchart LR
    ME[My laptop<br/>Manager + Worker]
    SMB[(spidey share<br/>Samba)]
    TS{{Tailscale<br/>private network}}
    SH[Windows desktop<br/>later WSL]
    LI[pop-os laptop]
    RU[ASUS TUF laptop]

    ME --- SMB
    ME <-->|jobs + status| TS
    TS <--> SH
    TS <--> LI
    TS <--> RU
    SH -.->|reads .blend, writes frames| SMB
    LI -.->|reads .blend, writes frames| SMB
    RU -.->|reads .blend, writes frames| SMB

    classDef host fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef net fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef store fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a
    classDef peer fill:#ff9a5c,stroke:#c4642a,color:#1a1a1a
    class ME host
    class TS net
    class SMB store
    class SH,LI,RU peer

Notice the dotted lines.

That is the part that would end up mattering.

The one assumption Flamenco makes

Flamenco does not move your files around.

The shared storage docs are upfront about it: the manager and every worker must be able to see the same files, on a NAS or a file server or some other shared drive.

The job just says "open this path", and it is your problem to make sure that path exists everywhere.

On four laptops scattered across home connections, that sounds like a strange requirement.

To be fair to Flamenco, a studio render farm lives on one fast LAN with one NAS, and this is a completely reasonable design there.

Our "LAN" was four laptops on four home connections.

Which brings us to the networking.

Tailscale: a LAN made out of the internet

My first worry was the obvious one: does this mean my machine has to push everything over my internet connection?

It does not.

Tailscale just creates a private network so all our laptops can talk to each other.

Every machine runs the Tailscale client and logs into the same account (a "tailnet").

Each machine gets a stable private address in the 100.x.y.z range, and every pair of machines talks over an encrypted WireGuard tunnel.

Tailscale's servers only do the coordination: who is in the network, what their public keys are, where they might be reachable.

Your actual traffic is supposed to go machine to machine.

"Supposed to" is doing a lot of work in that sentence, and we will get there.

On my laptop, Claude set up a Samba share called spidey and kept it bound to loopback only, so it was never exposed to my Wi-Fi or the internet.

Then Tailscale itself forwarded the SMB port, so only machines on the tailnet could reach it:

# on my laptop: Samba listens on 127.0.0.1 only,
# Tailscale forwards tailnet traffic on 445 to it
tailscale serve --bg --tcp 445 tcp://127.0.0.1:445

# on each Linux worker: mount it at the same path everywhere
sudo mount -t cifs //100.76.212.8/spidey /mnt/spidey \
  -o credentials=/root/.spidey_smb,uid=$(id -u),gid=$(id -g),vers=3.0
Enter fullscreen mode Exit fullscreen mode

The first milestone was gloriously low-tech.

Someone created a file from Windows.

I created one from Linux.

Both files showed up on both machines.

We celebrated a text file like it was a product launch.

The path boss fight

Having a shared folder is step one.

Getting Blender on three operating systems to agree on what is in it is the actual game.

Paths baked into the .blend

Claude inspected street_part2_b.blend and found 31 external asset references.

25 of them were absolute paths starting with /home/lovestaco/pers/lr_demo/....

The Windows desktop has no /home/lovestaco.

Neither does the pop-os laptop.

The fix was to copy the project into the share and convert every reference to a Blender relative path (//assets/...), resolved from wherever the .blend itself lives.

After that, all three street files had zero absolute paths left.

Paths in the job itself

The .blend location and output folder in a Flamenco job are a separate problem, and Flamenco does solve this one with two-way variables.

The manager stores job paths with a placeholder, and each worker swaps in its own platform's value:

# flamenco-manager.yaml
variables:
  spidey:
    is_twoway: true
    values:
      - platform: linux
        value: /mnt/spidey
      - platform: windows
        value: D:\spidey
Enter fullscreen mode Exit fullscreen mode

Two gotchas we learned the hard way.

On Windows, Flamenco wants a drive letter, not a \\server\share UNC path.

And on Linux, every machine has to use the same mount point, because there is only one Linux value.

That is why everyone mounted at /mnt/spidey, and why the wizard rejected ~/spidey outright (it does not expand ~).

Creating D:\spidey with mklink also failed, which turned out to be because mklink is a builtin of the old Command Prompt, not of PowerShell.

cmd /c mklink ... from an admin window fixed it.

Where is Blender?

Then the Windows worker printed this:

There was an error finding Blender on this system.

Blender was on PATH.

The shell had been restarted.

It still could not find it.

Monkey puppet side-eye meme about Flamenco on Windows looking for the program that opens .blend files instead of checking PATH

The Blender variable docs explain why: on Windows the worker looks up which program is associated with .blend files, rather than trusting PATH.

The fix was gloriously petty.

Create an empty junk .blend file, use it to set the file association, and the worker immediately reported Blender 5.2.2 LTS.

By 8:15 pm we had four workers awake on the dashboard:

image pending: Flamenco web dashboard showing four workers awake and the farm status idle

My laptop, a Windows desktop, a pop-os laptop and an ASUS TUF gaming laptop.

All on Blender 5.2.2.

An actual render farm.

On a Saturday.

The first job

We kept it small on purpose.

60 frames of part 2b at 360p draft quality, split into three chunks of 20.

Three workers picked up a chunk each within seconds.

Frames started landing in the shared folder.

And the farm genuinely worked.

When the job finished, Claude compared frames rendered by three different machines and they were identical: same lighting, same colours, same Miles.

That part is honestly very cool.

Then we looked at how long everything took.

Bar chart of how long each worker spent reading the 115 MB .blend: about 3 seconds locally, over 3 minutes on the relayed laptops, 82 seconds then a failure on Windows, and about 6 minutes on WSL

My worker read the 115 MB .blend off its own disk in a few seconds and rendered at about 0.4 s per frame.

The two relayed laptops spent over three minutes just opening the file before rendering a single frame.

A 20 frame chunk at 360p renders in seconds.

So the farm was spending almost all its time downloading and almost none of it rendering.

"Missing DNA block"

The Windows worker did something more dramatic.

It spent 82 seconds reading the file and then Blender died with:

ERROR Failed to read blend file 'D:\spidey...\street_part2_b_360p.blend': Missing DNA block

This error is a great little window into how .blend files work.

A .blend is basically a memory dump.

It is a header followed by a long run of data blocks, each one a chunk of Blender's in-memory structs written straight to disk.

To read those structs back, Blender needs to know their layout, which fields, which types, which offsets, and that description lives in a special block called DNA1 (the "SDNA").

That is how a file saved years ago, by a Blender with differently shaped structs, can still be opened today.

The catch is that DNA1 gets written near the end of the file.

So if a read dies partway through, Blender never reaches the block that explains everything before it, and you get "Missing DNA block".

We checked the obvious thing first.

The copy on Windows had the right size, 114,783,416 bytes, and once its SHA-256 finally finished (that alone took ages over the link), the hash matched mine exactly.

The file was fine.

The read was not.

The chunk size argument

This sparked the best debate of the night.

Claude suggested bigger chunks, so each worker would pay the slow load less often.

The obvious pushback: wouldn't smaller chunks download faster, in more pieces, spread across more machines?

The intuition makes sense if chunks map to pieces of the file, like a torrent.

They do not.

A chunk is a range of frames, but every chunk opens the entire scene.

Smaller chunks mean more full reads, not smaller ones.

Bernie Sanders meme: every 10 frame chunk, I am once again asking for the whole 115 MB .blend

The next question was a better one though: once the file has been read, isn't it done? Shouldn't only the first read be slow, since the OS caches the file?

So we ran a second job with 10 frame chunks and watched the ASUS laptop take its second chunk.

The first read took 3 min 02 s.

The second took 31 seconds.

Caching was real.

Not instant, but real.

On Windows, though, the cached retry failed with the same "Missing DNA block", so that SMB client and Blender were clearly not getting along.

Moving that worker into WSL got past the error, and it promptly rendered at about 50 seconds per frame, which smells a lot like EEVEE running without access to the GPU.

A side quest: whose GPU is it anyway

Even after loading, the remote workers were slow per frame.

The pop-os laptop took about 2.3 s per frame against my 0.4 s, and its GPU sampling step alone was over 3x slower than mine.

Its lspci explained it: an NVIDIA GTX 1650 Mobile, and an AMD Radeon Vega built into the Ryzen processor.

Two graphics chips, like most gaming laptops.

And glxinfo said OpenGL was running on the AMD integrated chip by default, because that is what laptops do to save battery.

EEVEE was happily rendering Miles on the weak chip while the GTX 1650 sat there.

The fix is PRIME render offload, starting the worker with __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia so Blender lands on the NVIDIA card.

Worth knowing if you ever farm out renders to other people's laptops.

Their GPU is not necessarily the GPU Blender gets.

Direct vs DERP (and what on earth is "blr")

Back to the real problem, the reads.

tailscale status on my machine had one telling word at the end of each peer's line.

The WSL worker was connected direct.

The pop-os and ASUS laptops were going through relay "blr".

Here is what those two words actually mean.

Diagram comparing a direct WireGuard UDP tunnel between the WSL worker and my laptop at about 97 ms ping with the pop-os laptop's traffic relayed through Tailscale's DERP server in Bengaluru at 58 to 415 ms, plus a note that it sits behind a hard NAT

Direct

Tailscale's first choice is a direct WireGuard tunnel over UDP between the two machines.

The hard part is that almost nobody's laptop has a public IP.

You are behind your home router's NAT, and quite often behind your ISP's NAT too.

So Tailscale does NAT traversal, also known as hole punching.

Each machine discovers what its public IP and port look like from the outside, the coordination server shares those with the peer, and both sides fire UDP packets at each other at the same time.

Each router sees an outgoing packet, creates a mapping for the reply, and if the timing and ports line up, the two outgoing "holes" meet in the middle.

Tailscale's How NAT traversal works is one of the best networking posts on the internet if you want the full version.

DERP

When hole punching fails, Tailscale falls back to DERP, short for Designated Encrypted Relay for Packets.

DERP servers are relays that Tailscale runs around the world.

Your packets go to the relay over HTTPS and it forwards them to the other machine.

They stay WireGuard-encrypted end to end, so the relay cannot read them, it just shuffles opaque bytes.

DERP regions are named after airport-style city codes.

"blr" is Bengaluru.

So "relay blr" just means "that laptop's packets to me were taking a detour through Tailscale's Bengaluru relay".

Direct is faster for the reasons you would guess.

A relay adds an extra hop, it is a shared server rather than your own path, and it runs over TCP, so a congested relay hurts you in ways a direct UDP path does not.

Pings from the pop-os laptop through blr ranged from 58 ms to 415 ms, and one test run stalled after receiving about 159 MB, with even a plain ls on the share hanging for two minutes.

Why it could not go direct

tailscale netcheck on the pop-os laptop said:

* UDP: true
* IPv6: no, but OS has support
* MappingVariesByDestIP: true
* PortMapping: UPnP
* Nearest DERP: Bengaluru (blr: 48.6ms)
Enter fullscreen mode Exit fullscreen mode

The important line is MappingVariesByDestIP: true.

That is a "hard" NAT.

Its router assigns a different public port for every destination, so the port my laptop learns about is not the port that router will actually accept traffic on from me.

Hole punching needs at least one side to be predictable.

Mine was the easy kind, but my router offered no UPnP or NAT-PMP to open a port automatically, and that side had no IPv6 to sidestep the whole mess.

Here is roughly how Tailscale decides:

flowchart TD
    A[Two peers want to talk] --> B{Either side has<br/>a public, open port?}
    B -->|yes| D[Direct WireGuard over UDP]
    B -->|no| C{Both NATs map ports<br/>predictably?}
    C -->|yes| E{Hole punch succeeds?}
    C -->|no, hard NAT| F{IPv6 on both sides?}
    E -->|yes| D
    E -->|no| G[Relay through nearest DERP]
    F -->|yes| D
    F -->|no| G

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef good fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef bad fill:#ff9a5c,stroke:#c4642a,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    class B,C,E,F decision
    class D good
    class G bad
    class A start

The suggested fix was forwarding UDP 41641 on my router so the hard NAT side would have an open door to knock on.

The twist: direct did not save us either

Then we noticed something awkward.

The WSL worker was on a direct connection.

And it still took about six minutes to load the file.

So we measured my connection.

0.65 MB/s upload.

About 5 Mbit/s.

Every worker in this farm was reading the .blend from my laptop.

Which means every byte, direct or relayed, had to squeeze out through my upload.

At 0.65 MB/s, one 115 MB read takes about three minutes.

Two workers reading at once, about six.

That worker took in 136 MB in roughly six minutes, about 0.38 MB/s, which is exactly what half of my upload looks like.

Waiting skeleton meme: remote worker loading the 115 MB .blend over my 5 Mbit/s upload

Fixing the hard NAT would have shortened the detour.

It would not have widened the pipe.

I switched to a faster Wi-Fi network for one more run, which bumped my upload to about 2.15 MB/s, and the remote first loads dropped to about a minute and a half.

Better.

But in that same run my laptop alone finished four of the six chunks while the remote workers were still loading.

Why we pulled the plug

For a remote worker to help, two things have to be true.

It has to get the scene fast, and it has to render fast once it has it.

Ours had neither.

The scene came through my home upload, partly via a relay, and the laptops that did load it were either on the wrong GPU, in WSL with no GPU, or tripping over SMB on Windows.

Every fix we found was real, and every one of them was its own project.

Copy the project to every laptop ahead of time and lose the "one shared folder" simplicity that made the setup appealing.

Fight every home router.

Fix every laptop's GPU setup.

Make sure every laptop stays awake, plugged in and on Wi-Fi for ten hours.

So at 9:52 pm we shut it all down.

Flamenco manager and worker stopped, Samba stopped and disabled, the Tailscale forward turned off, Tailscale itself stopped.

Nothing deleted, just switched off.

A few things I would tell past me before this Saturday:

A render farm is a file distribution problem wearing a GPU costume.

The render was the easy part.

Getting the same 130 MB to every machine, reliably, was the whole job.

Measure your upload before you build anything.

Five minutes with a speed test would have predicted most of this evening.

"Direct" is about the route, not the speed.

A perfect peer-to-peer tunnel is still capped by the slowest pipe on either end.

Check which GPU Blender is actually using.

Dual-GPU laptops will quietly hand you the integrated chip.

Flamenco is fine.

It assumes a studio LAN with a NAS, and we gave it four home connections and a laptop pretending to be a NAS.

So did Miles ever get his 1080p?

We really wanted this one to work.

Four laptops, one Saturday, our own little render farm.

It is a great story, it just is not a fast one.

But the film still needs rendering, and we did eventually crack it, by spreading the work across multiple providers instead of multiple laptops.

That is the next post.

Follow along so you do not miss how we finally got Miles rendered.

And I would love to hear from you in the comments.

Have you ever tried to farm out a render, or any big compute job, across machines you did not own?



Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.

I'm building LiveReview, a blast-radius aware AI code review built for your business-critical systems.

Instead of presenting every diff with equal emphasis, LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.

Spend code review effort where business risk is highest — not spread evenly across every diff.

⭐ Star it on GitHub:

GitHub logo HexmosTech / LiveReview

Blast-Radius Aware AI Code Review for Business-Critical Systems

LiveReview

gitleaks.yml osv-scanner.yml govulncheck.yml semgrep.yml dependabot-enabled mcp-testcases.yml

LiveReview: Risk-Aware AI Code Review for Business-Critical Systems

LiveReview is an AI code reviewer that scores every hunk of a diff by blast radius: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.

blast-radius-demo.mp4

LiveReview's Blast Radius & Review Priority scoring, live in the diff viewer.
















The exact math, not a black box Visualize blast radius at a glance Every factor that feeds the score

How does Blast Radius scoring work? (a more technical explanation)

Here's the goal:

  • A 3-line fix in a function used by 40 other files, that also writes to a database, should score high.
  • A 300-line UI change in one file, fully covered by tests…




Click below to try LiveReview with your codebase:

LiveReview Banner

Top comments (0)