What base64 is, and why playable ads use it at all

Base64 is a way of representing binary data — the raw bytes that make up an image, an audio clip, a font — as plain text made up of letters, digits, +, /, and a padding character. Browsers store and transmit HTML and JavaScript as text, so if a binary asset needs to live directly inside an HTML file or a JS string rather than as a separate file the browser fetches over the network, it first has to be converted into that text form. Base64 is the standard way to do that conversion.

Playable ads lean on this constantly because most ad networks flatly prohibit external file references in the delivered creative. AppLovin, Meta, Unity Ads, Vungle, Mintegral, and TikTok all expect (or strongly prefer) a single self-contained HTML file with zero outside requests once it's served inside their ad player. That rules out linking to images/hero.png the way a normal website would — every image, audio clip, and sometimes font has to be embedded as text directly in the markup, usually via a base64 data URI like data:image/png;base64,iVBORw0KG.... It's not a stylistic choice; it's what the spec requires.

The real math behind the ~33% increase

Base64 works by taking 3 bytes of raw binary data (24 bits) and re-encoding them as 4 text characters, each representing 6 bits. That's the whole mechanism. The size ratio falls straight out of that: 4 characters out for every 3 bytes in, or 4/3 ≈ 1.333. So a file that's N bytes on disk becomes roughly N × 4/3 bytes once base64-encoded — an increase of about 33.3%, not an approximation someone rounded for convenience.

Two things push the real number slightly past that clean 1.333 figure:

  • Padding. Base64 processes input in 3-byte chunks. If the final chunk has only 1 or 2 bytes left over, it gets padded with = characters to fill out the last 4-character group — a small, mostly negligible add-on for anything beyond a few bytes.
  • The data URI wrapper. A prefix like data:image/png;base64, adds a fixed handful of characters per asset, on top of the encoding itself.

Neither of those changes the headline number much. For anything larger than a trivial file, expect right around 33%, not a rough "it gets bigger."

Why this catches people off guard

The disconnect is almost always about which number someone is looking at. A developer checks the assets folder — 4 MB of PNGs, an audio file, maybe a video clip — sees it comfortably under a network's 5 MB cap, and assumes they're fine. Then the build step runs, inlines every one of those assets as base64 text into the final HTML, and the delivered file comes out at 5.3 MB or more: 4 MB × 4/3 ≈ 5.33 MB. What was margin to spare becomes a rejection, and the size the network measures — the finished creative, not the source art — was never the number anyone looked at.

This is exactly the gap that catches teams by surprise on Luna/Playworks exports too, where brotli compression, base64/base122 encoding, and the Unity engine data blob combine in ways that are easy to reason about wrong from source-folder size alone. If you want the deeper technical breakdown of how that specific pipeline works, see how Luna compresses assets: brotli, base64, and base122 explained. This article is the more general version: the same base64 math applies no matter what tool produced your build.

How to check your real, post-encoding size

The only way to know for certain is to measure the file after encoding, not before. Opening the assets folder in a file browser tells you nothing about what the final HTML will weigh once every image and audio clip is inlined as text. A quick, reliable way to check is to upload your build to PlayableKit's free size checker, which reports the actual delivered size per network — after compression and encoding are accounted for — so a cap violation shows up before submission instead of after a rejected review. If you're already over a specific network's limit, what to do when your playable ad exceeds the 5MB limit walks through the common causes and fixes.

Practical ways to reduce the impact

Most networks won't let you skip the encoding step itself — it's a hard requirement of the delivery format, not an optional optimization. What you can control is how much raw data goes into the encoder in the first place:

  • Compress images before encoding, not after. Re-encoding a PNG as a well-tuned JPEG or WebP, or running it through a lossless optimizer, shrinks the source bytes that base64 then multiplies by 4/3. A 33% tax on a smaller number is a smaller tax.
  • Resize to the actual display size. A 2000px-wide background rendered at 400px in the ad is pure waste before encoding even starts — downscale to what's actually shown.
  • Compress audio aggressively. Lower bitrate or mono audio for short SFX clips rarely hurts perceived quality in a 5-15 second playable, and audio files are frequently the largest single assets in the bundle.
  • Cut unused assets. Every image, sound, or font that ships in the build but isn't actually used gets encoded anyway. Auditing for dead weight before the build step is free size savings.
  • Let a general-purpose compression pass run first where the pipeline supports it. Compressing raw bytes before base64-encoding them (rather than after) captures real savings, since compressing already-encoded text gains far less than compressing the original binary.

None of this makes the ~33% overhead disappear — it's baked into how base64 works. But shrinking what goes in before encoding is the only lever that actually moves the delivered file size, and it's the difference between a build that clears a network's cap with room to spare and one that's rejected over a few hundred kilobytes that were avoidable from the start.

Worth knowing: if your build is Luna/Playworks-based, the pipeline already runs a compression pass before encoding — but the source assets going into it still drive the final number. Check the real embedded size with PlayableKit's free size checker before you submit anywhere.