In WordPress, media files like images play a critical role in enhancing user experience, improving SEO, and engaging visitors. However, these files aren’t directly stored in the WordPress database. Instead, they are uploaded to the wp-content/uploads directory, while the associated data is stored in the WordPress database. Understanding how WordPress handles image storage matters if you’re managing a media-heavy site, migrating between servers, or trying to figure out why a gallery plugin is behaving strangely.
WordPress Database Structure Overview
The WordPress database consists of several tables, each handling different aspects of your website’s content, from posts and pages to comments and user accounts. For images, WordPress splits the job between the filesystem and the database. The actual image bytes sit in the uploads folder on your server. The database holds everything about those images: where they live, what sizes were generated, what alt text was set, which post they belong to.
Two tables do almost all of the work here:
- wp_posts, the record that an attachment exists at all
- wp_postmeta, the details about that attachment
That’s a smaller footprint than people expect. A lot of developers assume there’s a dedicated “media” table somewhere in WordPress core. There isn’t. Attachments are posts, just like your blog posts and pages, with a different post_type value.
The Role of wp_posts in Storing Image Information
When you upload an image, WordPress creates a new row in wp_posts with post_type set to attachment. That single row carries several fields worth knowing:
- Post Type: Always “attachment” for media files, regardless of whether the file is an image, PDF, or video.
- Post Title: Usually derived from the filename, though you can edit it from the Media Library.
- Post Content: Typically empty for images, unless you added a caption or description in the attachment editor.
- Post Mime Type: The file’s MIME type, like image/jpeg or image/png, stored in post_mime_type.
- Post Parent: If the image was uploaded through a specific post’s editor, post_parent links back to that post’s ID. Images uploaded straight to the Media Library without attaching to a post have post_parent set to 0.
- Post Date: When the file was uploaded.
A simplified version of the insert WordPress runs when you upload an image looks like this:
INSERT INTO wp_posts (post_type, post_title, post_mime_type, post_parent, post_date)
VALUES ('attachment', 'image_name.jpg', 'image/jpeg', 45, '2024-09-19 14:00:00');
Note the post_parent value of 45 in that example. That’s what ties an image to the post it was uploaded from. Useful when you’re trying to find every image attached to a particular article.
The Role of wp_postmeta in Storing Image Metadata
The wp_posts row only gets you so far. The real detail lives in wp_postmeta, where WordPress stores key-value pairs tied to each attachment’s post ID. Two meta keys matter most:
- _wp_attached_file: The relative path to the uploaded file. Upload something in September 2024, and the value might read 2024/09/image_name.jpg. WordPress uses this to locate the original file on disk.
- _wp_attachment_metadata: A serialized PHP array holding the image’s dimensions plus the full list of resized versions WordPress generated. That includes the standard thumbnail and medium sizes, along with any custom sizes registered by your theme or plugins.
A row for _wp_attached_file might look like this:
INSERT INTO wp_postmeta (post_id, meta_key, meta_value)
VALUES (123, '_wp_attached_file', '2024/09/image_name.jpg');
The _wp_attachment_metadata value is denser. Unserialized, it typically contains something along these lines:
array(
'width' => 1920,
'height' => 1080,
'file' => '2024/09/image_name.jpg',
'sizes' => array(
'thumbnail' => array('file' => 'image_name-150x150.jpg', 'width' => 150, 'height' => 150),
'medium' => array('file' => 'image_name-300x169.jpg', 'width' => 300, 'height' => 169),
'large' => array('file' => 'image_name-1024x576.jpg', 'width' => 1024, 'height' => 576),
),
)
This is the array WordPress reads whenever a theme calls wp_get_attachment_image_src() or a plugin needs to know which sizes exist for a given upload.
Themes and plugins can register their own custom sizes on top of the defaults using add_image_size(), and each one gets added to that same sizes array the next time WordPress processes the image. This is why switching themes sometimes leaves you with a media library full of image sizes the new theme never asked for. The old theme’s registered sizes were generated and recorded at upload time, and nothing automatically prunes them when you deactivate the plugin or theme that defined them. Running the Regenerate Thumbnails plugin, or wp media regenerate from WP-CLI, is the usual fix, since it rebuilds the sizes array against whatever sizes are currently registered.
WooCommerce and BuddyPress both register at least one custom image size this way, and so do most page builders. On a site running several of these at once, it’s not unusual to see a dozen resized files generated from a single upload, each one tracked in that same metadata array rather than as a separate database record.
None of this changes where the metadata lives, just how much of it there is.
How Featured Images Connect to Posts
Featured images work through yet another meta key, this time attached to the post that’s using the image rather than the image itself. When you set a featured image, WordPress writes a _thumbnail_id entry into wp_postmeta for the post, with the value pointing to the attachment’s post ID. That’s the entire mechanism. There’s no separate featured-image table. If you’ve ever wondered why deleting an attachment sometimes leaves a broken featured image behind, this is why: the _thumbnail_id reference doesn’t get cleaned up automatically in every code path, particularly with custom import scripts.
Where BuddyPress and BuddyBoss Avatars Break the Pattern
Everything above describes the standard WordPress media pipeline. If you’re running BuddyPress or a BuddyBoss-based community, member avatars and cover photos don’t follow it. BuddyPress writes profile photos directly into wp-content/uploads/avatars/{user_id}/ without creating a wp_posts attachment row at all. There’s no post_type of “attachment,” no _wp_attachment_metadata array, nothing you’d find by querying the Media Library.
This trips up developers constantly. Someone builds a script to find “all images on the site” by querying wp_posts for attachment post types, and it comes back clean while member avatars are sitting untouched in a completely separate folder structure. It also means that a plugin cleaning up “unused attachments” based on wp_posts will never see BuddyPress avatars, which is generally what you want, but worth knowing before you run any bulk media cleanup on a community site. Group avatars follow the same pattern, stored under wp-content/uploads/group-avatars/{group_id}/ rather than going through the standard attachment flow.
Other Tables That Touch Media
wp_posts and wp_postmeta carry most of the weight, but two more tables occasionally get involved:
- wp_terms and wp_term_relationships: If you tag or categorize media (some plugins add taxonomy support to attachments), those relationships live here, the same way they do for regular post categories and tags.
- wp_comments: Attachments can technically receive comments, though almost no themes expose a comment form on attachment pages anymore. If you find rogue comment rows tied to attachment post IDs, this is why.
Where Thumbnails and Resized Images Actually Live
Every resized version WordPress generates on upload gets written to disk in the same folder as the original, following your Settings > Media configuration for the thumbnail and medium dimensions (large is handled separately, capped by the theme’s max content width in most cases). A single upload might produce:
- wp-content/uploads/2024/09/image_name.jpg (original)
- wp-content/uploads/2024/09/image_name-150×150.jpg (thumbnail)
- wp-content/uploads/2024/09/image_name-300×169.jpg (medium)
- wp-content/uploads/2024/09/image_name-1024×576.jpg (large)
None of those resized files exist as separate wp_posts rows. They’re just paths recorded inside the single _wp_attachment_metadata array for the original attachment. That’s a common point of confusion when people manually delete files from the uploads folder expecting the database to reflect the change automatically. It won’t. The database entry stays until you delete the attachment through WordPress itself, or clean it up directly.
Working With Image Data Directly
A few situations push people into the database or the file system instead of the Media Library screen.
Migrating to a new domain or server. Image URLs get hardcoded into post content and into the serialized _wp_attachment_metadata arrays. A plain find-and-replace on the database can corrupt those serialized arrays if it changes string lengths without updating the length prefix PHP uses for serialization. Tools built for this, like WP-CLI’s wp search-replace command, handle serialized data correctly. Running raw SQL UPDATE statements against serialized meta fields is one of the more common ways people quietly break their media library during a migration.
Finding and removing unused images. You can query wp_posts for rows where post_type = ‘attachment’ and then cross-reference post IDs against everywhere they might be referenced (post content, featured image meta, ACF fields, widget content). This is genuinely tricky to do safely by hand, which is why cleanup plugins exist rather than people running DELETE statements directly.
Troubleshooting broken images. If an image shows a broken icon on the front end but the file clearly exists in the Media Library, check whether _wp_attached_file still points to the correct relative path. A partial migration, a renamed uploads folder, or a restored backup from a different server path are the usual culprits.
WooCommerce Product Images Add Another Layer
If the site is running WooCommerce, product images add one more meta key worth knowing about. The main product image works exactly like a featured image, through _thumbnail_id on the product post. But the product gallery, the row of extra thumbnails buyers click through on a product page, gets stored separately as a comma-separated list of attachment IDs in a meta key called _product_image_gallery. Each ID in that list still points to a normal wp_posts attachment row with its own _wp_attachment_metadata, so the underlying storage mechanism doesn’t change. WooCommerce just adds a layer that groups multiple attachment references together under one product.
This matters for the same reason the BuddyPress avatar quirk matters: any custom script that walks through “every image used on the site” needs to know where to look. wp_posts and post_content catches inline images. _thumbnail_id catches featured images and main product photos. _product_image_gallery catches the rest of a product’s gallery. Miss one of these and a bulk media audit will report images as unused when they’re very much in active use.
A Quick WP-CLI Detour
If your host gives you shell access, WP-CLI’s media commands sit on top of exactly the database structure described above. wp media regenerate rebuilds the resized versions listed in _wp_attachment_metadata without touching the original file. wp post list --post_type=attachment --format=count gives you a fast attachment count without loading the Media Library screen, which matters once a library has grown past a few thousand items and the admin grid starts to lag.
A Practical Walkthrough: Tracing an Attachment ID Back to Its File
Say you’ve got an attachment ID from an error log or a query result, and you need to know exactly which file it points to and where it lives. Here’s the path, step by step.
First, confirm the row exists and check its basic info:
SELECT ID, post_title, post_mime_type, post_parent
FROM wp_posts
WHERE ID = 4821 AND post_type = 'attachment';
That confirms the attachment exists and tells you the mime type and which post, if any, it’s attached to. Next, pull the file path:
SELECT meta_value
FROM wp_postmeta
WHERE post_id = 4821 AND meta_key = '_wp_attached_file';
That gives you the relative path, something like 2024/09/image_name.jpg. Combine it with your site’s uploads base directory and you have the exact location on disk. If you need the full breakdown of generated sizes, pull _wp_attachment_metadata instead and unserialize the result. WP-CLI does this same lookup in one command: wp post meta get 4821 _wp_attached_file. Same data, no manual SQL required, which is generally the safer route unless you’re specifically trying to understand the underlying structure.
Common Mistakes to Avoid
A few patterns show up repeatedly on real sites:
Deleting files via FTP instead of through WordPress. The database still thinks the attachment exists, so you end up with orphaned wp_posts and wp_postmeta rows pointing at nothing.
Restoring a database backup without restoring the matching uploads folder. Every attachment reference in the restored database points to files that no longer exist on the new server.
Running a manual search-and-replace on post_content without touching serialized postmeta. URLs inside plain text update fine. URLs buried inside a serialized array get skipped, and image sizes silently stop resolving.
Best Practices for Managing Image Data
Compress images before uploading. Every upload triggers multiple resize operations, so a 12MB camera photo doesn’t just sit on disk taking up space, it makes WordPress do more work generating each registered size from a larger source file. Running images through a compressor first cuts both storage use and the CPU time spent on resizing.
Audit your media library on a schedule, not just when disk space runs low. A plugin that flags unattached or duplicate images can save real disk space on a site that’s been running for several years, but remember the earlier point about BuddyPress avatars and WooCommerce galleries. Any audit tool needs to check post_content, _thumbnail_id, and any plugin-specific meta keys before it decides something is genuinely unused.
Back up the database and the uploads folder together, on the same schedule. A database backup without the matching uploads folder leaves you with attachment records pointing at files that don’t exist. An uploads folder without the database leaves you with files nothing in WordPress can find. They’re only useful as a pair.
Consider offloading media to a CDN or object storage once the library grows large enough that server disk space or bandwidth becomes a real constraint. This doesn’t change how the database tracks the file conceptually. It changes where the actual bytes get served from, with the offload plugin rewriting the URLs WordPress generates so they point at the CDN instead of your own server.
Frequently Asked Questions
Does WordPress store images as binary data (BLOBs) in the database? No. WordPress never stores image binary data in MySQL by default. Everything in the database is metadata: paths, dimensions, MIME types, and relationships. The actual pixels live on disk.
Why did my image sizes disappear after a migration? Usually because the serialized _wp_attachment_metadata array got corrupted by a naive string replace, or the uploads folder wasn’t migrated alongside the database.
Can I query which posts use a specific image? You can search wp_postmeta for _thumbnail_id matches to find featured image usage, and search wp_posts.post_content for the file URL to catch inline images. Neither catches every possible reference (custom fields, block attributes with just an ID), which is part of why “unused image” detection is harder than it sounds.
What happens to the database rows when I delete an image from the Media Library? WordPress deletes the wp_posts row, all associated wp_postmeta rows, and every resized file on disk. If the attachment is currently set as a post’s featured image, that post’s _thumbnail_id meta becomes orphaned unless the deletion process catches it, which is why a “missing” featured image sometimes shows up after a media cleanup.
Is there a limit to how many images WordPress can track in the database? Not a hard limit imposed by WordPress itself. In practice, sites with tens of thousands of attachments can see the Media Library admin screen slow down, since it’s still running a paginated query against wp_posts. That’s an admin UI performance issue rather than a database capacity issue.
Once you understand that images are really just attachment posts with metadata orbiting them, a lot of otherwise confusing WordPress behavior starts to make sense: why deleting a post can cascade into deleting its attached images, why migrations need special handling for media URLs, and why a “missing” image almost always traces back to a mismatch between what the database expects and what actually exists in the uploads folder.