WordPress attachment status and term counts can produce one of the more confusing taxonomy behaviors in the WordPress data model: an attachment can be assigned to a taxonomy term correctly while that term still reports a count of zero.
The relationship exists.
The attachment exists.
The taxonomy exists.
Yet WordPress may appear to insist:
Term count: 0
This behavior becomes particularly noticeable when developers add categories or other taxonomies to the WordPress Media Library.
The reason lies in the interaction between the attachment post type, its normal inherit post status and the Core function WordPress uses to calculate taxonomy term counts.
The default WordPress taxonomy counter is primarily designed around ordinary published posts. Attachments behave differently, and that difference can produce counts that do not match the number of Media Library items actually assigned to a term.
This guide explains how WordPress attachment statuses work, what inherit really means, how taxonomy relationships are stored, how _update_post_term_count() calculates counts, why unattached Media Library files can be excluded, how hide_empty can make the problem more visible and when a custom term-count strategy is appropriate.
Start with the WordPress attachment model
A WordPress media upload is not represented only by a physical file inside:
wp-content/uploads/
WordPress also creates an attachment record.
An attachment is a built-in WordPress post type:
post_type = attachment
The official wp_insert_attachment() documentation shows the normal attachment creation model.
A typical attachment record looks conceptually like this
ID:
1842
post_type:
attachment
post_status:
inherit
post_parent:
500
post_mime_type:
image/jpeg
The attachment can then have additional information stored through:
- attachment metadata;
- post meta;
- taxonomy relationships;
- image sub-size metadata;
- alt text;
- captions;
- descriptions.
Attachments normally use post_status = inherit
This is one of the key differences between ordinary WordPress posts and attachments.
A normal published post commonly has:
post_type:
post
post_status:
publish
An uploaded attachment commonly has:
post_type:
attachment
post_status:
inherit
Why does WordPress use inherit?
Historically, attachments can belong to a parent post.
For example:
Post #500
↓
Attachment #1842
The attachment’s effective publication state can therefore follow the status of its parent.
An attachment can have a parent
The relationship is stored through:
post_parent
For example:
Attachment ID:
1842
post_parent:
500
means attachment 1842 is associated with post 500 as its parent.
But an attachment does not need a parent
Many files are uploaded directly through:
Media
→ Add New
or:
Media
→ Library
without being attached to a particular post.
Those attachments can have:
post_parent:
0
This is commonly called an unattached attachment
For example:
ID:
2401
post_type:
attachment
post_status:
inherit
post_parent:
0
The file is completely valid.
It may be:
- a company logo;
- a downloadable PDF;
- a reusable hero image;
- a product asset;
- a brand document;
- a file referenced by a theme option;
- a page-builder asset.
Unattached does not mean unused
This distinction is important.
An attachment with:
post_parent = 0
can still be used through:
- featured-image metadata;
- block content;
- custom fields;
- theme settings;
- page builders;
- CSS;
- plugin configuration;
- direct URLs.
The attachment parent is not a universal WordPress usage tracker.
What does get_post_status() report for an attachment?
WordPress provides get_post_status() to determine the effective status of a post object.
Attachments receive special handling.
An unattached inherit attachment is effectively treated as published
Current WordPress Core logic says that if an attachment:
post_type = attachment
post_status = inherit
post_parent = 0
then get_post_status() treats it as:
publish
This makes sense from an application perspective
A normal Media Library upload with no parent should not suddenly become invisible merely because it has no parent post.
Conceptually:
Raw database status:
inherit
Effective status:
publish
An attached attachment can inherit its parent’s status
Suppose:
Attachment:
post_status = inherit
post_parent = 500
Parent post:
post_status = publish
get_post_status() can resolve the attachment as effectively:
publish
If the parent is private, the inherited state can change
Conceptually:
Attachment:
inherit
Parent:
private
Effective attachment status:
private
This is why the status is called:
inherit
So where does the taxonomy count problem come from?
The surprising part is that taxonomy term counting does not simply run:
get_post_status( $attachment )
for every relationship.
WordPress uses dedicated SQL-based counting functions.
The default post taxonomy counter is _update_post_term_count()
For taxonomies associated with post types, WordPress normally uses the internal:
_update_post_term_count()
function.
The official _update_post_term_count() reference documents the current Core implementation.
The function is internal Core functionality
The leading underscore is not decorative enthusiasm.
WordPress marks:
_update_post_term_count()
as private Core functionality rather than a public API developers should call directly as ordinary application code.
The counter normally begins with publish
By default, the list of statuses included in the post-term count starts with:
publish
This is appropriate for ordinary posts
Suppose a Category contains:
4 published posts
2 drafts
1 private post
A public-facing category count commonly needs to represent the published content rather than every database relationship.
Attachments complicate that model
Most attachments do not store:
post_status = publish
They store:
post_status = inherit
Core therefore contains attachment-specific term-count logic
The current implementation recognizes when the taxonomy applies to:
attachment
and performs a separate attachment count.
Published-status attachments are counted
If an attachment directly has one of the statuses configured to be counted, it can enter the result.
Normally the relevant default is:
publish
Inherited attachments require a parent in the default SQL
The important part of the Core logic can be summarized conceptually as:
Count attachment if:
post_status is counted
OR
post_status = inherit
AND post_parent > 0
AND parent status is counted
Notice the post_parent > 0 condition
This is the source of the famous attachment-count gap.
Consider:
Attachment #1842
post_status:
inherit
post_parent:
500
Parent #500:
publish
This attachment can be counted.
Now consider an ordinary unattached Media Library file
Attachment #2401
post_status:
inherit
post_parent:
0
It fails this part:
post_parent > 0
and its raw stored status is:
inherit
rather than:
publish
The result can therefore be zero
Even if attachment 2401 has a perfectly valid taxonomy relationship:
Attachment #2401
↓
Media Category:
Brand
the default post-term counter may not include it.
This is the key contradiction
Ask WordPress:
get_post_status( 2401 )
and an unattached inherit attachment can effectively resolve as:
publish
But the taxonomy-count SQL sees:
post_status = inherit
post_parent = 0
and does not count it under the standard attachment branch.
The two systems are solving different problems
get_post_status() answers:
What is this object's effective status?
_update_post_term_count() answers something closer to:
How many relationships satisfy
the taxonomy's counting rules?
The fact that those answers can diverge is confusing, but it does not mean the taxonomy relationship was lost.
The taxonomy relationship can still exist
Suppose:
Media Category:
Brand
Assigned attachment:
2401
WordPress can store that relationship in:
wp_term_relationships
while:
wp_term_taxonomy.count
still contains:
0
Relationship and count are separate data
Conceptually:
RELATIONSHIP
Attachment #2401
→ Brand
CACHED TERM COUNT
Brand
→ 0
The count is not the relationship itself.
Where is the term count stored?
WordPress stores taxonomy information across several tables, including:
wp_terms
wp_term_taxonomy
wp_term_relationships
wp_terms stores the term identity
For example:
term_id:
18
name:
Brand
slug:
brand
wp_term_taxonomy stores taxonomy-specific information
This includes fields such as:
term_taxonomy_id
term_id
taxonomy
description
parent
count
The count column is a stored aggregate
For example:
taxonomy:
media_category
term:
Brand
count:
27
That value is maintained when WordPress updates taxonomy relationships and relevant object states.
wp_term_relationships stores the actual assignments
Conceptually:
object_id:
2401
term_taxonomy_id:
52
This means:
Object #2401
belongs to taxonomy term relationship #52.
A wrong count does not automatically mean broken relationships
This is the first thing to verify when debugging.
If:
Term count = 0
do not immediately conclude:
No attachments are assigned.
Inspect the relationships themselves.
How are term counts updated?
WordPress provides wp_update_term_count().
The function ultimately updates counts for the specified terms using the taxonomy’s configured counting callback.
Counts may also be deferred
WordPress can defer term counting during operations that change many relationships.
This avoids unnecessarily recalculating a term after every individual modification.
Conceptually:
Assign 1,000 objects
↓
defer repeated counts
↓
recalculate affected terms
at the appropriate point
This is a performance optimization
Term counts are aggregate information.
Recounting thousands of objects after every single relationship change would be wasteful.
What happens when an object’s status changes?
WordPress also updates taxonomy counts when relevant post statuses transition.
The internal _update_term_count_on_transition_post_status() function handles this Core behavior.
By default, publish is the counted post status
Current Core exposes:
update_post_term_count_statuses
so the list of statuses considered by post-term counting can be filtered.
The default remains publish
Conceptually:
$counted_statuses = array(
'publish'
);
This filter was added because not every content model uses publish the same way
A custom application might need counts to include:
- a custom public status;
- a workflow-specific state;
- another state considered visible by that application.
But changing counted statuses globally deserves caution
Suppose you add:
inherit
to the counted statuses for a taxonomy.
You may solve one attachment problem while changing how other objects associated with the same taxonomy are counted.
Taxonomy scope matters
A taxonomy may be registered only for:
attachment
or shared across:
attachment
post
page
custom post type
Your counting strategy should match the actual object model.
register_taxonomy() provides update_count_callback
The official register_taxonomy() documentation exposes:
update_count_callback
for controlling how the taxonomy’s object count is calculated.
Without a custom callback, post-type taxonomies use the post counter
For taxonomies associated only with WordPress post types, Core normally uses:
_update_post_term_count()
WordPress documentation specifically warns about attachments
The official documentation notes that attachment taxonomies are a special case because the standard post counter can exclude Media Library attachments that are not attached to a parent post.
WordPress also has a generic count callback
The Core function:
_update_generic_term_count()
calculates the number of raw object relationships for each term.
The official _update_generic_term_count() documentation shows that it counts records directly from wp_term_relationships.
The conceptual difference is important
_update_post_term_count()
Counts relationships
subject to post-type
and status rules
_update_generic_term_count()
Counts raw relationships
without post-status filtering
The generic counter can solve the unattached-media gap
If:
Attachment #2401
→ Brand
exists in wp_term_relationships, the generic counter can count that relationship regardless of:
post_status = inherit
post_parent = 0
But the generic callback is not automatically perfect either
This needs emphasis.
Because the generic callback counts relationships rather than evaluating WordPress post statuses, it does not inherently ask:
Is this attachment trashed?
Is this object private?
Should this workflow exclude this status?
A generic count may therefore be broader than you actually want
Consider:
Brand category
20 active attachments
3 trashed attachments
Raw relationships:
23
If taxonomy relationships remain for the trashed objects, a relationship-only counter can return:
23
while your Media Library interface might intentionally display only:
20
The best counter depends on the meaning of the number
Ask:
What is this count supposed to represent?
Possible answers include:
Every stored relationship
Every non-trashed attachment
Every visible attachment
Every attachment visible to this user
Every published parent-attached file
Those are five different definitions.
A custom callback can express the real requirement
For a Media Library taxonomy, a custom counter might deliberately count:
post_type = attachment
AND post_status != trash
AND taxonomy relationship exists
without requiring:
post_parent > 0
This often matches editorial expectations better
If administrators organize their Media Library into:
Brand
Products
Blog
Campaigns
they probably expect:
Brand (42)
to mean:
42 files currently filed in Brand
not:
42 files whose raw attachment state
happens to satisfy a post-centric
publication rule.
TheOneWP Media Categories handles this explicitly
TheOneWP Media Categories uses a genuine WordPress taxonomy for attachments while providing its own corrected Media Library count behavior.
Its category interface is designed so the number shown beside a media category reflects the files actually filed within that Media Library workflow instead of relying blindly on WordPress’s default post-oriented attachment count.
The implementation uses a direct count query
The module’s verified feature implementation calculates Media Category counts with a direct database query rather than iterating over every attachment individually.
This matters when a Media Library contains:
100 files
1,000 files
50,000 files
because count accuracy should not require loading every attachment into PHP first.
Why does WordPress keep the default behavior?
Because the taxonomy system was originally designed primarily around posts and their publication state.
The default behavior is reasonable for:
Category:
WordPress
5 published posts
3 drafts
Count:
5
The trouble appears when the same mechanism is reused for:
attachment
objects whose lifecycle is different.
This is a general WordPress lesson
WordPress APIs are deliberately reusable.
But reusable does not mean every default was designed around every later use case.
Attachment taxonomies are a legitimate WordPress architecture
As explained in WordPress taxonomies beyond posts and pages, attachments are WordPress post objects and can participate in the taxonomy system.
The relationship architecture itself is perfectly valid.
The problem is specifically the count definition
Do not conflate:
Taxonomy does not support attachments
with:
Default term counting
does not match my attachment workflow.
The second statement is the accurate one.
What does WP_Term->count represent?
A WP_Term object includes a:
count
property.
That count comes from the stored taxonomy count maintained in:
wp_term_taxonomy.count
It is not recalculated from every relationship every time you access the term
This is why count-maintenance logic matters.
If the stored count is wrong for your use case, displaying:
$term->count
repeats the same mismatch.
get_terms() can use the stored count
The official WP_Term_Query documentation exposes parameters such as:
hide_empty
hide_empty is true by default in WP_Term_Query
Conceptually:
hide_empty = true
means terms considered empty should not normally be returned.
For non-hierarchical term queries, Core can literally require count > 0
The current WP_Term_Query::get_terms() implementation adds a SQL condition equivalent to:
tt.count > 0
when hide_empty applies in the relevant query mode.
This turns a count problem into a discovery problem
Suppose:
Brand
Relationships:
14 attachments
Stored count:
0
Now run a term query with:
hide_empty => true
WordPress can decide:
Brand is empty.
The term can disappear from the query result even though fourteen attachment relationships exist.
This is why attachment count bugs can look worse than they are
You may observe:
Term missing from dropdown
and assume:
Term does not exist.
But the real chain may be:
Attachment relationships exist
↓
Core count excludes attachments
↓
term count becomes zero
↓
hide_empty removes term
↓
interface appears empty
Setting hide_empty to false can help diagnose the problem
For example:
$terms = get_terms(
array(
'taxonomy' => 'media_category',
'hide_empty' => false,
)
);
If the missing terms suddenly appear, inspect their stored counts and object relationships.
But hide_empty = false is not necessarily the final fix
It only says:
Show terms even when
WordPress considers them empty.
It does not correct:
$term->count
and it does not fix interfaces that legitimately need accurate counts.
The proper fix is often to define the correct counting semantics
If Media Category counts should represent:
all active attachments
assigned to the term
then build the count around exactly that definition.
Do not repair the symptom only in the UI
A fragile workaround might calculate:
Brand (14)
manually in one sidebar while leaving:
WP_Term count = 0
everywhere else.
That may be acceptable for an intentionally isolated interface, but it should be a deliberate decision rather than an accidental inconsistency.
Term counts can affect more than labels
Applications may use the count for:
- navigation badges;
- term dropdowns;
- empty-state decisions;
- REST responses;
- frontend filters;
- archive logic;
- sitemaps;
- administrative dashboards.
WordPress taxonomy sitemaps also use hide_empty
WordPress’s taxonomy sitemap provider retrieves taxonomy terms with:
hide_empty = true
for public taxonomy sitemap generation.
This is another reason a public taxonomy should have counts that match the intended public content model.
Media-only taxonomies normally should not need public sitemaps anyway
A taxonomy used purely to organize:
Brand assets
Client files
Internal graphics
usually does not need public term archives or sitemap entries.
Keep administrative organization and public SEO architecture separate.
wp_count_terms() counts terms, not attachments
The function:
wp_count_terms()
can sound like it counts objects assigned to a term.
It does not.
The official wp_count_terms() documentation describes it as counting the number of terms returned for a taxonomy query.
Compare the concepts
$term->count
How many counted objects
belong to this term?
wp_count_terms()
How many terms match
this term query?
Similar names. Different jobs. Naturally.
wp_count_attachments() is different again
WordPress also provides wp_count_attachments().
This counts attachments grouped by MIME type.
It does not count attachments by taxonomy term
Conceptually:
image/jpeg:
320
image/png:
180
application/pdf:
45
rather than:
Brand:
42
Products:
163
Blog:
295
wp_count_attachments() also uses different status logic
Its Core SQL counts attachment posts where:
post_status != trash
and separately records Trash.
This creates another useful comparison
Imagine an unattached image:
post_type:
attachment
post_status:
inherit
post_parent:
0
wp_count_attachments() can count it as an attachment because:
inherit != trash
while the standard taxonomy post counter can exclude it from a Media Category term count.
Same object, different counters, different semantics
This is not necessarily a bug in either function.
They were designed to answer different questions.
Attachment count
Question:
How many media attachments
of this MIME type exist?
Taxonomy post count
Question:
How many objects matching this
taxonomy's counted-status rules
belong to this term?
Media Library category count
Often what an administrator actually wants is:
How many non-trashed Media Library
files are assigned to this category?
That third definition may need custom logic
This is the gap a dedicated media-taxonomy implementation should solve.
What about private attachments?
Attachments can also use special states such as:
private
trash
auto-draft
depending on the workflow.
Decide explicitly whether they belong in the count
If a category sidebar exists only for normal Media Library browsing, you might decide:
inherit:
count
private:
count only for authorized users
trash:
do not count
Another application may need different behavior.
There is no universally correct count without context
A count shown to:
Administrator
may legitimately differ from a count shown to:
Author
if role-based visibility filters the actual attachments each user can access.
Global taxonomy counts are not user-specific
The normal:
wp_term_taxonomy.count
value is a global stored aggregate.
It is not automatically recalculated according to:
- current user role;
- per-user restrictions;
- temporary query filters;
- custom Media Library visibility rules.
This matters for role-based Media Library visibility
Suppose:
Brand category:
50 attachments total
but an Author is allowed to see only:
12
Displaying:
Brand (50)
while showing twelve files is confusing and may disclose information about hidden assets.
Visible counts may need to be calculated separately
TheOneWP Media Visibility can apply visibility rules to individual attachments and Media Categories.
Its supported Media Library workflow can therefore use counts that reflect the files visible to the current role rather than blindly exposing the taxonomy’s global stored total.
Global term count vs. visible count
GLOBAL TERM COUNT
All qualifying objects
assigned to the term
VISIBLE COUNT
Objects from that term
that the current user/query
is allowed to see
Those numbers can legitimately differ
The important thing is that the interface clearly uses the count appropriate to its purpose.
Do not modify global term counts for each user
A terrible solution would be:
User A opens Media Library
→ rewrite Brand count to 12
User B opens Media Library
→ rewrite Brand count to 31
Administrator opens Media Library
→ rewrite Brand count to 50
Global database aggregates should not mutate according to whoever happens to open the screen.
Calculate contextual counts separately
If a UI needs role-specific numbers, calculate those numbers for the query or interface without corrupting the canonical taxonomy count.
What happens when an attachment is trashed?
When media moves to Trash, its:
post_status
can become:
trash
The taxonomy relationship may still exist while the item is in Trash
This is why a raw relationship counter can potentially include more objects than an active Media Library interface.
Deleting and trashing are different
Trashing is reversible.
Permanent deletion removes the attachment and associated WordPress data more aggressively.
Any custom counting implementation should decide whether:
trash
belongs in its definition of an active category.
For most Media Library organization interfaces, it probably should not
If the user sees:
Brand (40)
they normally expect to be able to browse approximately forty active files in that category, not thirty-five active files plus five items hidden in Trash.
What happens when the attachment parent changes?
Under the default post counter, parent status matters for an attachment stored as:
inherit
with:
post_parent > 0
A published parent can make the attachment countable
Conceptually:
Attachment:
inherit
Parent:
publish
Result:
countable by default attachment logic
A non-counted parent can remove it from the default count
If the parent moves to a status not included by:
update_post_term_count_statuses
the corresponding attachment may stop contributing to the term count.
This can make category counts change without taxonomy assignments changing
That is another crucial distinction.
The relationship:
Attachment
→ Brand
may remain untouched.
Only the object’s eligibility for the aggregate count changed.
Do not treat the count as a relationship audit
If you need to know:
Which objects are actually assigned
to this term?
query the relationships or objects.
Do not reverse-engineer membership from:
$term->count
WP_Query also has attachment-specific status behavior
The official WP_Query documentation notes that attachments normally use:
post_status = inherit
while the default WP_Query post status is:
publish
This means a naive attachment query can return no results
For example:
new WP_Query(
array(
'post_type' => 'attachment'
)
);
may not behave the way somebody expects if the appropriate attachment statuses are not included.
A typical attachment query needs explicit status handling
For example:
new WP_Query(
array(
'post_type' => 'attachment',
'post_status' => 'inherit',
)
);
or an appropriately broader status configuration for the application’s purpose.
The WordPress Media Library itself knows this
The Core Media Library query configures attachment statuses intentionally.
The current wp_edit_attachments_query_vars() implementation uses:
inherit
and can include:
private
when the current user has the relevant capability.
This is why reproducing the Media Library with a generic WP_Query is not trivial
If you build:
my custom media browser
with the assumptions used for ordinary blog posts, you can accidentally omit a large portion of the Media Library.
Attachment status is part of the content model
A correct attachment query should consider:
post_type = attachment;- attachment statuses;
- user permissions;
- MIME type;
- taxonomy filters;
- Trash state;
- visibility rules.
Taxonomy filtering and term counts are independent
A category filter can query:
attachments assigned to Brand
and successfully return fourteen objects even when:
Brand count = 0
if the query itself does not rely on the stored count.
This is a useful debugging test
If:
taxonomy query returns attachments
but
term count is zero
then the problem is probably:
count semantics
rather than:
missing term relationships
How to debug an attachment taxonomy count mismatch
Work through the layers in order.
1. Confirm taxonomy exists
↓
2. Confirm taxonomy is registered
for attachment
↓
3. Confirm attachment exists
↓
4. Inspect raw post_status
↓
5. Inspect post_parent
↓
6. Confirm term relationship
↓
7. Inspect stored term count
↓
8. Identify update_count_callback
↓
9. Compare count rules
with intended Media Library rules
Step 1: inspect the attachment itself
Check:
post_type
post_status
post_parent
For the problematic object.
Step 2: compare raw and effective status
Raw database:
inherit
Effective:
get_post_status()
These may not return conceptually identical states for attachments.
Step 3: inspect the taxonomy relationship
Use WordPress APIs such as:
wp_get_object_terms()
or:
get_the_terms()
to determine whether the attachment is actually assigned.
Step 4: inspect the term’s stored count
Compare:
relationship exists:
yes
term count:
0
This strongly suggests the counting callback is excluding the object.
Step 5: inspect taxonomy registration
Check whether:
update_count_callback
was explicitly configured.
If not, Core chooses its normal counting strategy based on the taxonomy’s object types.
Step 6: test hide_empty = false
If terms appear only when:
hide_empty => false
then stored counts are participating in the apparent disappearance.
Step 7: recalculate only after fixing the counting rule
Running a recount using the same unsuitable callback simply produces the same unsuitable result more efficiently.
Fix the definition first.
Then recalculate affected terms
Once the correct callback or counting mechanism exists, WordPress can recalculate affected taxonomy counts.
Do not edit wp_term_taxonomy.count manually as the normal fix
You could technically change:
count = 0
to:
count = 42
directly in SQL.
WordPress will then happily overwrite your handcrafted numerical masterpiece the next time it recalculates the term.
Fix the source of the count
A durable solution changes:
how WordPress determines 42
rather than manually storing:
42
Custom SQL counts should still respect WordPress architecture
If building a custom attachment counter, account for:
- correct table prefixes;
- term taxonomy IDs;
- attachment post type;
- desired statuses;
- Trash behavior;
- object visibility;
- taxonomy scope.
Do not count by term_id when the relationship uses term_taxonomy_id
wp_term_relationships references:
term_taxonomy_id
not simply:
term_id
Mixing those identifiers can produce incorrect joins.
Term taxonomy ID and term ID are not interchangeable concepts
Although modern WordPress term handling hides much of this complexity behind APIs, the database schema still distinguishes:
term identity
from
term within a taxonomy
Prefer WordPress APIs unless direct SQL has a clear reason
For ordinary application work, functions such as:
get_terms()
wp_get_object_terms()
wp_set_object_terms()
wp_update_term_count()
should normally be preferred.
Direct SQL can make sense for carefully controlled aggregate calculations where performance requires it, but then the implementation becomes responsible for matching the intended WordPress semantics.
Large Media Libraries make this more important
Imagine:
80,000 attachments
300 media categories
A category sidebar cannot reasonably perform:
300 categories
×
80,000 attachment loops
on every request.
Accurate counting must also scale
A useful Media Library count strategy needs:
- correct semantics;
- efficient SQL;
- appropriate indexes;
- minimal repeated work;
- correct cache invalidation.
Caching counts can improve performance
But cached counts introduce another requirement:
When should the cached number
be invalidated?
Count-changing events include
- assigning a term;
- removing a term;
- moving an attachment to Trash;
- restoring an attachment;
- deleting an attachment;
- changing relevant status;
- changing category visibility where counts are contextual.
Stale counts and wrong counts are different problems
A stale count means:
The calculation definition is correct,
but it has not been refreshed.
A wrong count definition means:
Recalculating forever will still
produce the wrong number.
Identify which one you have
This saves enormous amounts of time spent pressing:
recount
recount
recount
while WordPress obediently reproduces the same zero.
Hierarchical taxonomies introduce parent-count behavior
If a taxonomy is hierarchical:
Brand
├── Logos
├── Guidelines
└── Photography
the count shown for a parent can depend on whether the application uses direct counts or padded counts that include descendants.
WP_Term_Query supports pad_counts
This can make a parent term conceptually represent:
direct objects
+
objects in child terms
Do not confuse padded counts with attachment-status problems
If:
Brand = 0
because its only objects are in:
Logos = 12
that is a hierarchy/count presentation issue.
If:
Logos = 0
despite twelve direct attachment relationships, investigate attachment count semantics.
These are separate layers
Relationship count
↓
Attachment eligibility
↓
Direct term count
↓
Optional child padding
↓
Displayed count
Media Categories can make these distinctions visible
Media Categories adds a classification layer to attachments while preserving their attachment IDs and file URLs.
The taxonomy provides:
organization
while its corrected counter provides:
Media Library-appropriate totals
The taxonomy does not move physical files
For example:
/uploads/2026/08/logo.svg
can belong to:
Brand
without becoming:
/uploads/brand/logo.svg
The taxonomy relationship exists in WordPress’s data model rather than the filesystem.
This is why term counts matter operationally
If a sidebar says:
Brand (0)
while opening Brand reveals:
42 files
administrators reasonably assume something is broken.
Accurate counts are part of interface trust
A count is not decorative metadata.
Users rely on it to understand:
- whether categories contain content;
- how assets are distributed;
- whether cleanup succeeded;
- whether bulk moves worked;
- whether filters are behaving correctly.
The same principle applies outside media
The attachment case is unusually visible, but taxonomy count semantics also matter for:
- custom post statuses;
- private content;
- workflow post types;
- non-public content models;
- taxonomies shared across several post types.
Custom statuses deserve special attention
The default post counter begins with:
publish
If your custom post type uses:
approved
as the meaningful public workflow state, default counts may not match the application’s logic.
WordPress provides the update_post_term_count_statuses filter
The filter can change the statuses used by Core’s post-term counter.
For example, conceptually:
publish
approved
could both contribute to counts for a particular taxonomy.
Scope the filter carefully
The callback receives the:
WP_Taxonomy
object.
This means your code can decide whether the additional status applies to:
this specific taxonomy
instead of rewriting count behavior everywhere.
Global filters are easy to overapply
A line that changes term-count statuses for every taxonomy on the site can quietly alter:
- blog Category counts;
- Tag counts;
- custom taxonomy counts;
- attachment counts.
Use the narrowest rule possible.
Term counts are not authorization
Suppose a restricted role sees:
Brand (10)
and an Administrator sees:
Brand (40)
These numbers may support the interface, but they do not themselves protect any file.
For the security distinction, see Hiding vs. restricting access to WordPress media.
Taxonomy membership is not file security either
Putting:
confidential-report.pdf
inside:
Private Documents
does not prevent somebody from accessing:
/wp-content/uploads/confidential-report.pdf
when the web server still serves that URL publicly.
Classification, visibility and access control are different layers
TAXONOMY
What group does this file belong to?
VISIBILITY
Should this user see it
in the Media Library?
ACCESS CONTROL
Can this request retrieve
the actual file?
Do not make one layer pretend to be all three
This keeps WordPress architecture considerably easier to reason about.
What about Grid vs. List Media Library counts?
The same taxonomy count should represent the same underlying classification regardless of how attachments are displayed.
See WordPress Media Library: grid view vs. list view for the broader interface differences.
Presentation should not redefine taxonomy membership
Switching from:
Grid
to:
List
does not alter:
- attachment IDs;
- post statuses;
- term relationships;
- stored taxonomy counts.
Infinite scrolling does not change counts either
TheOneWP Media Infinite Scroll changes how Media Library result batches are loaded.
It does not change whether attachment 2401 belongs to:
Brand
or how taxonomy relationships are stored.
Do not calculate category totals from currently loaded DOM items
With progressive loading, the screen may currently contain:
80 attachments
from a category containing:
850 attachments
Counting visible HTML elements would produce a pagination count, not a taxonomy count.
The count should come from the data layer
The interface should know:
Total matching attachments:
850
Currently loaded:
80
Those are separate values.
This matters for large libraries
For broader Media Library organization strategies, see Organizing a large WordPress media library and Keeping a WordPress media library organized at scale.
A practical attachment-count model
For an ordinary administrative Media Library taxonomy, a useful count definition might be:
Count attachments where:
1. attachment has relationship to term
2. post_type = attachment
3. post_status is visible in active Media Library
4. attachment is not trashed
5. any application-specific
visibility rule is respected
when calculating contextual counts
Do not copy this blindly into every project
The right rule depends on what your taxonomy represents.
A document archive may want:
all historical relationships
while an editor sidebar may want:
only currently manageable files
Define semantics before SQL
This is the most important engineering rule in this entire problem.
Do not start with:
SELECT COUNT(*) ...
Start with:
What exactly should this
number mean?
Then implement the query
Once the semantics are clear, the SQL becomes much less mysterious.
Attachment status and term-count checklist
- Remember that attachments are a WordPress post type.
- Remember that normal attachments commonly use
post_status = inherit. - Check
post_parentwhen investigating counts. - Do not equate unattached with unused.
- Understand that
get_post_status()can resolve unattachedinheritattachments as effectively published. - Understand that default taxonomy counting uses separate SQL logic.
- Remember that the default attachment term counter requires a parent for the inherited-parent branch.
- Do not assume a zero count means no taxonomy relationships exist.
- Inspect
wp_term_relationshipsor WordPress term APIs when debugging membership. - Remember that
wp_term_taxonomy.countis an aggregate value. - Check the taxonomy’s
update_count_callback. - Use
hide_empty => falseas a diagnostic tool when terms disappear. - Do not mistake
hide_empty => falsefor a complete count fix. - Understand that the generic term counter counts relationships without post-status filtering.
- Exclude Trash deliberately if active-media counts should not include it.
- Use a custom callback when neither Core default exactly matches the required semantics.
- Scope
update_post_term_count_statusesfilters narrowly. - Do not edit stored counts manually as the primary fix.
- Recalculate counts after correcting the counting strategy.
- Separate global taxonomy counts from user-specific visible counts.
- Do not use taxonomy counts as an authorization mechanism.
- Do not calculate total counts from only the currently loaded Media Library items.
Common WordPress attachment count mistakes
Assuming inherit means unpublished
An attachment with raw inherit status can effectively behave as published.
Assuming get_post_status() and taxonomy counting use the same logic
They do not.
Assuming unattached media should not count
That depends entirely on the application. In a Media Library organization system, unattached files are often precisely the files you need to classify.
Assuming term count equals relationship count
The stored count can apply additional eligibility rules.
Assuming zero means no files are assigned
The relationships may still exist.
Calling wp_count_terms() to count attachments in a category
That counts terms returned by a term query, not objects assigned to one term.
Calling wp_count_attachments() to count a Media Category
That counts attachments by MIME type, not taxonomy membership.
Blindly switching to _update_generic_term_count()
It fixes the parent/status filtering problem by counting raw relationships, but that may include object states your interface does not want.
Adding inherit to every taxonomy’s counted statuses
A global change can affect unrelated taxonomies.
Manually updating wp_term_taxonomy.count
The next legitimate count update can overwrite it.
Using hide_empty = false and declaring victory
The terms reappear, but the underlying counts can remain wrong.
Using the global term count for role-restricted interfaces
The total may not match what the current user is allowed to see.
Counting only DOM elements in infinite-scroll views
You are counting loaded results, not total taxonomy membership.
A debugging decision tree
Term shows count = 0
│
├── Is attachment actually assigned?
│ │
│ ├── No
│ │ └── Relationship problem
│ │
│ └── Yes
│ │
│ └── What is raw post_status?
│ │
│ ├── inherit
│ │ │
│ │ └── post_parent = 0?
│ │ │
│ │ ├── Yes
│ │ │ └── Default attachment
│ │ │ counter may exclude it
│ │ │
│ │ └── No
│ │ └── Check parent status
│ │
│ └── Other
│ └── Check counted statuses
│
└── Is custom count callback registered?
│
├── No
│ └── Review Core default semantics
│
└── Yes
└── Review callback rules
A count-strategy decision tree
What should count mean?
│
├── Published post-like objects only
│ └── Core post count may fit
│
├── Every raw taxonomy relationship
│ └── Generic count may fit
│
├── Every active Media Library attachment
│ └── Custom attachment-aware count
│
└── Only objects visible to current user
└── Contextual UI/query count,
not global stored count
Example: a normal post taxonomy
Suppose:
Category:
WordPress
Published:
10
Draft:
4
Private:
2
The default post-oriented count:
10
is sensible.
Example: Media Library taxonomy with attached files
Suppose:
Brand
10 attachments
All:
inherit
All parents:
published
The default attachment-aware Core count can include those relationships.
Example: Media Library taxonomy with unattached files
Brand
10 attachments
All:
inherit
All:
post_parent = 0
The relationships can exist while the standard post-term count fails to represent the ten files.
Example: generic count
Suppose:
Brand
10 active attachments
2 trashed attachments
Relationships remaining:
12
A pure relationship count may report:
12
even when the active Media Library should display:
10
Example: role-specific visibility
Brand total:
40
Administrator sees:
40
Editor sees:
30
Author sees:
8
The global taxonomy count might legitimately remain:
40
while each interface computes its own visible count.
The important lesson
There is no magical:
correct count
without first defining:
correct for what?
Related WordPress taxonomy and Media Library guides
For the wider taxonomy, attachment and Media Library cluster, continue with:
- WordPress taxonomies beyond posts and pages
- What happens to taxonomies when you change post type
- WordPress post types vs. custom post types
- Organizing a large WordPress media library
- Keeping a WordPress media library organized at scale
- WordPress Media Library: grid view vs. list view
- Hiding vs. restricting access to WordPress media
- Replacing vs. re-uploading WordPress media
- Media Categories
- Media Visibility
- Media Infinite Scroll
- Post Type Converter
Final thoughts
WordPress attachment term counts are confusing because several individually reasonable Core behaviors intersect in an unintuitive way.
Attachments are WordPress post objects, but unlike ordinary published posts they commonly store:
post_status = inherit
An unattached Media Library file with post_parent = 0 can be treated by get_post_status() as effectively published, yet the default taxonomy post counter uses its own SQL and does not include that attachment through the inherited-parent branch because there is no parent.
The taxonomy relationship can therefore remain completely valid while the stored term count says zero.
That matters because counts are used by interfaces and term queries, and hide_empty can turn an inaccurate zero into a term that appears to vanish entirely.
The solution is not to assume WordPress taxonomies cannot work with attachments. They can.
The solution is to decide what the count is supposed to represent.
If you need the number of published post-like objects, the default Core counter may be exactly right. If you need every raw relationship, the generic counter may fit. If you need every active Media Library attachment regardless of parent relationship, a dedicated attachment-aware counter is usually more appropriate.
TheOneWP Media Categories uses that third model: it keeps a genuine WordPress taxonomy for attachments while correcting the Media Library count separately so the category numbers correspond to the files administrators are actually organizing.
Once you separate:
attachment status
taxonomy relationship
stored term count
visible query result
the behavior stops looking random.
It is still unusually elaborate for the apparently innocent question “how many files are in this category?”, but this is WordPress. Somewhere beneath every number is a SQL query from 2007 wearing three compatibility layers and doing its best.

