Database Architecture

Stop Storing Images in Your Database: An 8.6 GB Base64 Cleanup

Why base64 images bloat relational databases and how to migrate them safely to object storage without disrupting production.

Octacer August 28, 2026
A dense, overloaded database cylinder straining under blocks of image data, with a lighter separate stack of image files beside it.

The symptom: three bugs, no common thread

The reports came in from different teams, on different days, and each looked unrelated.

The mobile team said the property listing screen took over 20 seconds to load on a mid-range device. The back-office team said the admin panel kept throwing memory errors when exporting a client's full property portfolio. And the infrastructure team noticed the MySQL database on the primary replica had grown to a size that made backups take most of the night.

Three bugs. Three teams. Three separate investigations.

Why base64-in-database happens in the first place

Storing images as base64 text in a relational database is not a decision anyone makes deliberately. It is usually an accident of convenience.

A developer building an internal tool needs to persist an image. The easiest path is a single insert statement with everything — including the image — in one row. No file storage setup, no CDN configuration, no separate serving path. The database is already there, already backed up, and already accessible. It works, and it works for a while.

The problem is that the convenience does not scale. Every subsequent feature — list views, exports, mobile sync, duplicate detection — inherits the cost of that initial shortcut.

What the database actually pays

A relational database is built for structured, indexable, queryable data. It is not built to serve large binary payloads, and the architecture punishes you for treating it that way.

The application server serializes everything

When your API returns a list of properties, each row carries its image column. Even if the client only needs a thumbnail URL, the server pulls the full base64 string from disk, deserializes it into memory, and sends it over the wire. The client then decodes it to display a small picture.

Consider a portfolio view that shows 50 properties. That list query needs the name, address, price, and status for each property — a few hundred bytes of real data. But because the image column lives in the same table, the query engine reads 50 full image strings. The response payload is no longer measured in kilobytes. It is measured in tens of megabytes, and 99% of it gets discarded by the client.

The query planner cannot help

With separate storage, you can exclude the image column entirely for list queries. The index covers the fields you need, and the database reads only the relevant pages.

With blobs in the same table, every row is large. The database must read a full-page spread just to fetch one row's metadata. As the table grows, pages fragment, and the working set balloons far beyond what the indexes actually need. This is why the same query that took 200 ms on a small dataset takes 8 seconds once the images accumulate.

Backups and replication carry the dead weight

Every image is backed up multiple times — nightly snapshots, replication streams, and standby replicas. An 8.6 GB database that is mostly images means an 8.6 GB backup that is mostly images, even though only a few hundred megabytes are operational data. Restore times, failover times, and storage costs all scale with the bloat.

The structural fix: move images out, keep references in

The solution is not to compress the images better or to cache more aggressively. It is to change where images live.

The principle is separation of concerns:

  • The database stores metadata — property records, image IDs, filenames, ordering, and URLs — because that is what it is good at.
  • A file store holds the binary payloads — because that is what object storage is built for.
  • A serving path (CDN or static hosting) delivers them — because that is the fastest way to get pixels to a client.

This is a deliberate division of responsibility, not a stylistic preference. Each component does what it is actually designed to do, and the failure modes become isolated. If the image store is slow, the metadata queries are unaffected. If the metadata database needs maintenance, the images already served by the CDN are unaffected.

The canonical data flow

A working design has a few well-defined paths:

  1. Write path: When an image is uploaded, the application streams the bytes directly to object storage, gets back a key, and stores that key plus metadata in the database. The database never sees the binary payload.
  1. Read path: When a client requests a property list, the API queries the database for metadata only. The response contains image URLs. The client fetches the images from the CDN in parallel, and the database is never involved in serving pixels.
  1. Derived assets: Thumbnails and resized variants are generated once at upload time and stored alongside the original. The client requests the exact size it needs instead of downloading the full image and resizing locally.

This is the pattern that cut the property listing load time from over 20 seconds to under one in the scenario described here. The mobile app stopped downloading hundreds of kilobytes of base64 text per property and instead fetched small, pre-generated thumbnails from a CDN.

A safer migration path

Moving 8.6 GB of images out of MySQL is not a one-command operation. The migration needs to be incremental, reversible, and verifiable — especially in production.

The approach that works well is a dual-read, backfill, cutover sequence:

1. Add the file store, keep the old column

Deploy the write path first. New image uploads go to object storage, and the database stores both the new reference and the legacy base64 column. Nothing breaks, and the system still runs on the old data.

2. Backfill in batches

Write a staggered backfill job that processes records in small batches — for example, 500 at a time — reading the base64 string, writing the file to object storage, and updating the record with the new reference.

The cleanup job should be idempotent so an interrupted run can resume without duplicating files. A simple uniqueness check on the storage key provides that safety. This is also the moment to generate derived thumbnails, since you are reading each image exactly once.

3. Dual-read with fallback

Update the read path to prefer the new reference. If the reference is missing or the file store errors, fall back to the legacy base64 decode. This protects against any silently failed backfill.

4. Cut over and reclaim the space

Once the backfill completes and the dual-read window shows no fallback activity, switch the read path to use the file store exclusively. Then drop the base64 column, rebuild the table to reclaim the space, and watch the backup times normalize.

A practical detail: rewrite the table rather than just dropping the column. In InnoDB, dropping a large column can leave the underlying tablespace fragmented. Rewriting the table compacts the data and returns the space to the operating system, which is the point of this entire exercise.

What good looks like after migration

The operational signals change quickly and visibly:

  • List queries drop from seconds to milliseconds. The database no longer reads large blobs for metadata requests, and the working set fits comfortably in the buffer pool.
  • The database shrinks dramatically. A database that was mostly images becomes a database that is mostly real data. Backups complete in a fraction of the previous time.
  • The API response payload collapses. A property list response is now a few hundred kilobytes of metadata, not tens of megabytes of encoded images.
  • Image load is parallelized. The client fetches multiple thumbnails from the CDN concurrently instead of waiting on one serialized base64 blob.

In the scenario described, the load time dropped from over 20 seconds to under one second. The mobile app no longer froze on the listing screen, the admin export stopped throwing memory errors, and the nightly backup finished well before the next business day began.

Where this approach is right — and where it is not

This pattern is correct for virtually any case where images are user-generated or operational assets: property photos, product images, profile pictures, document scans, uploaded attachments. The file count may be in the millions, but each file is independent, and object storage handles that scale naturally.

It is also correct for compliance-sensitive data that benefits from object lifecycle policies — automated retention, deletion, and region replication — which a database blob column cannot offer.

The approach is not the right fit when the images are:

  • Small and bounded. A few dozen icons or logos totaling a few hundred kilobytes might not justify the added infrastructure.
  • Highly transactional and tightly coupled to the row. If the image must be updated atomically with the record itself, you need distributed transaction coordination that a file store does not provide.
  • Prototyped or exploratory. For a short-lived internal tool, the extra moving parts may not earn their complexity. But the moment the data becomes production-facing, the migration becomes worthwhile.

The lesson: separate the data from the payload

The three bugs were never separate. They were three different symptoms of the same architectural mistake — storing binary payloads inside a database built for metadata.

The fix was not faster queries, better caching, or a bigger instance. It was moving the bytes to a storage layer designed for them and letting the database do what it does well. The result was a system that was not only faster but also simpler to operate — smaller backups, cleaner queries, and a clearer boundary between what the database owns and what object storage owns.

Worth mapping your own data flow? If you have a table that carries image columns, video blobs, or base64 document strings, the pattern is worth a closer look. The symptom may not be an 8.6 GB database — it might be a slow list endpoint, a brittle export, or an overnight backup that keeps creeping later. They often trace back to the same decision.

Ready to Implement These Strategies?

Let's discuss how to apply these insights to your specific business challenges.

Schedule Consultation