How To Upload Files In Elixir Phoenix JSON Api — Tentamen Software Testing Blog

Gigalixir Node Memory Consumption Of 25MB File Upload As Base64 encoded string.


In my Elixir Phoenix application, I have a JSON API for mobile applications. One endpoint accepts file uploads. In this post, I explain my architectural decision why to implement file upload as multi-part form data.

JSON API Upload As Base64 Data Stream

Google returned this excellent DockYard blog post: Building An Image Upload API With Phoenix. I implemented image and video upload as a Base64 encoded string in JSON. The above Screenshot depicts memory consumption for file upload of 25MB. So for 40 concurrent file uploads of that size, I will need 1GB Gigalixir instance. What about 400 concurrent uploads?

MultiPart Form Data

I switched to multipart form data. Your controller is straightforward:

@spec add_note(Plug.Conn.t(), any) :: Plug.Conn.t()
def add_note(conn, params) do

When clients send multi-part Form Data with a file, the Phoenix framework does all the heavy work before your controller is in control. So if you sent a file under the form data attribute named screenshot, then Phoenix will put in params["screenshot"] Plug.Upload structure.

Note that you still can send back a JSON response. My initial guess was that I must use JSON on input for JSON API. Wrong, we can also use multipart form data. For sending additional attributes, just send them as additional form data parameters.

The Gain

The same file upload of 25MB left the following memory footprint on my Gigalixir instance:

Memory Consumption For Multipart upload Of 25MB file

Check two mini spikes at the end. 3MB memory consumption.

Under The Hood

Plug.Upload is A server (a GenServer specifically) that manages uploaded files. Uploaded files are stored in a temporary directory and removed from that directory after the requested file dies. This temporary location is stored in :path an attribute. But the most important is this:

All options supported by Plug.Conn.read_body/2 are also supported here. They are repeated here for convenience:

:length — sets the maximum number of bytes to read from the request, defaults to 8_000_000 bytes

:read_length — sets the amount of bytes to read at one time from the underlying socket to fill the chunk, defaults to 1_000_000 bytes

Socket stream is read in 1Mb and is written to tmp file location using Elixir File.Stream. When 1Mbytes is ready, they are written to file. Making memory impact minimal.


Use multipart file import in Phoenix applications because Plug.Upload does all the magic in the background.

Originally published at on November 16, 2020.




Founder of Tentamen, software testing agency.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Bonjour tech-nerd !!


Code Your Own React Like Hooks using JavaScript

Angular, how to optimize scss compilation

#100DaysOfCode Day 32: Front-End Work Using React.Js

Smart ways to write JSX in your React app

10 ES6 feature that every JavaScript developer should master.

Tips: Iterating Data That Interpreted As an Object in Javascript

How to Manage Your React Application State With Recoil.js, Part 2/2

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Karlo Smid

Karlo Smid

Founder of Tentamen, software testing agency.

More from Medium

Comparing dates in Elixir

How to sneak in a XSS exploit in 4 steps or how to detect said attempt

Avoid using `loadByProperties` to load entities

Test toggle states with RSpec