diff --git i/src/include/commands/vacuum.h w/src/include/commands/vacuum.h
index 4450003b4a8..13f70a1f654 100644
--- i/src/include/commands/vacuum.h
+++ w/src/include/commands/vacuum.h
@@ -237,7 +237,8 @@ typedef struct VacuumParams
 											 * use default */
 	int			multixact_freeze_table_age; /* multixact age at which to scan
 											 * whole table */
-	bool		is_wraparound;	/* force a for-wraparound vacuum */
+	bool		is_wraparound;	/* antiwraparound autovacuum? */
+	AutoVacType trigger;		/* autovacuum trigger condition, if any */
 	int			log_min_duration;	/* minimum execution threshold in ms at
 									 * which autovacuum is logged, -1 to use
 									 * default */
@@ -328,6 +329,7 @@ extern void vacuum(List *relations, VacuumParams *params,
 extern void vac_open_indexes(Relation relation, LOCKMODE lockmode,
 							 int *nindexes, Relation **Irel);
 extern void vac_close_indexes(int nindexes, Relation *Irel, LOCKMODE lockmode);
+extern const char *vac_autovacuum_trigger_msg(AutoVacType trigger);
 extern double vac_estimate_reltuples(Relation relation,
 									 BlockNumber total_pages,
 									 BlockNumber scanned_pages,
diff --git i/src/backend/access/heap/vacuumlazy.c w/src/backend/access/heap/vacuumlazy.c
index 8f14cf85f38..8a64dce6180 100644
--- i/src/backend/access/heap/vacuumlazy.c
+++ w/src/backend/access/heap/vacuumlazy.c
@@ -639,6 +639,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 				 * implies aggressive.  Produce distinct output for the corner
 				 * case all the same, just in case.
 				 */
+				Assert(params->trigger == AUTOVACUUM_TABLE_XID_AGE ||
+					   params->trigger == AUTOVACUUM_TABLE_MXID_AGE);
 				if (vacrel->aggressive)
 					msgfmt = _("automatic aggressive vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
 				else
@@ -656,6 +658,9 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 							 vacrel->relnamespace,
 							 vacrel->relname,
 							 vacrel->num_index_scans);
+			if (!verbose)
+				appendStringInfo(&buf, _("triggered by: %s\n"),
+								 vac_autovacuum_trigger_msg(params->trigger));
 			appendStringInfo(&buf, _("pages: %u removed, %u remain, %u scanned (%.2f%% of total)\n"),
 							 vacrel->removed_pages,
 							 new_rel_pages,
diff --git i/src/backend/commands/vacuum.c w/src/backend/commands/vacuum.c
index 7b1a4b127eb..18278acb557 100644
--- i/src/backend/commands/vacuum.c
+++ w/src/backend/commands/vacuum.c
@@ -273,8 +273,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 		params.multixact_freeze_table_age = -1;
 	}
 
-	/* user-invoked vacuum is never "for wraparound" */
+	/* user-invoked vacuum never uses these autovacuum-only flags */
 	params.is_wraparound = false;
+	params.trigger = AUTOVACUUM_NONE;
 
 	/* user-invoked vacuum uses VACOPT_VERBOSE instead of log_min_duration */
 	params.log_min_duration = -1;
@@ -1874,7 +1875,11 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, bool skip_privs)
 		LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
 		MyProc->statusFlags |= PROC_IN_VACUUM;
 		if (params->is_wraparound)
+		{
+			Assert(params->trigger == AUTOVACUUM_TABLE_XID_AGE ||
+				   params->trigger == AUTOVACUUM_TABLE_MXID_AGE);
 			MyProc->statusFlags |= PROC_VACUUM_FOR_WRAPAROUND;
+		}
 		ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags;
 		LWLockRelease(ProcArrayLock);
 	}
@@ -2176,6 +2181,30 @@ vac_close_indexes(int nindexes, Relation *Irel, LOCKMODE lockmode)
 	pfree(Irel);
 }
 
+/*
+ * Return translatable string describing autovacuum trigger threshold type
+ */
+const char *
+vac_autovacuum_trigger_msg(AutoVacType trigger)
+{
+	switch (trigger)
+	{
+		case AUTOVACUUM_NONE:
+			return _("none");
+		case AUTOVACUUM_TABLE_XID_AGE:
+			return _("table XID age");
+		case AUTOVACUUM_TABLE_MXID_AGE:
+			return _("table MXID age");
+		case AUTOVACUUM_DEAD_TUPLES:
+			return _("dead tuples");
+		case AUTOVACUUM_INSERTED_TUPLES:
+			return _("inserted tuples");
+	}
+
+	Assert(false);				/* cannot be reached */
+	return NULL;
+}
+
 /*
  * vacuum_delay_point --- check for interrupts and cost-based delay.
  *
diff --git i/src/backend/postmaster/autovacuum.c w/src/backend/postmaster/autovacuum.c
index f5ea381c53e..18d150c975b 100644
--- i/src/backend/postmaster/autovacuum.c
+++ w/src/backend/postmaster/autovacuum.c
@@ -330,12 +330,14 @@ static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 static void recheck_relation_needs_vacanalyze(Oid relid, AutoVacOpts *avopts,
 											  Form_pg_class classForm,
 											  int effective_multixact_freeze_max_age,
-											  bool *dovacuum, bool *doanalyze, bool *wraparound);
+											  AutoVacType *dovacuum, bool *doanalyze,
+											  bool *wraparound);
 static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
 									  Form_pg_class classForm,
 									  PgStat_StatTabEntry *tabentry,
 									  int effective_multixact_freeze_max_age,
-									  bool *dovacuum, bool *doanalyze, bool *wraparound);
+									  AutoVacType *dovacuum, bool *doanalyze,
+									  bool *wraparound);
 
 static void autovacuum_do_vac_analyze(autovac_table *tab,
 									  BufferAccessStrategy bstrategy);
@@ -2064,7 +2066,7 @@ do_autovacuum(void)
 		PgStat_StatTabEntry *tabentry;
 		AutoVacOpts *relopts;
 		Oid			relid;
-		bool		dovacuum;
+		AutoVacType dovacuum;
 		bool		doanalyze;
 		bool		wraparound;
 
@@ -2110,7 +2112,7 @@ do_autovacuum(void)
 								  &dovacuum, &doanalyze, &wraparound);
 
 		/* Relations that need work are added to table_oids */
-		if (dovacuum || doanalyze)
+		if (dovacuum != AUTOVACUUM_NONE || doanalyze)
 			table_oids = lappend_oid(table_oids, relid);
 
 		/*
@@ -2157,7 +2159,7 @@ do_autovacuum(void)
 		PgStat_StatTabEntry *tabentry;
 		Oid			relid;
 		AutoVacOpts *relopts = NULL;
-		bool		dovacuum;
+		AutoVacType	dovacuum;
 		bool		doanalyze;
 		bool		wraparound;
 
@@ -2193,7 +2195,7 @@ do_autovacuum(void)
 								  &dovacuum, &doanalyze, &wraparound);
 
 		/* ignore analyze for toast tables */
-		if (dovacuum)
+		if (dovacuum != AUTOVACUUM_NONE)
 			table_oids = lappend_oid(table_oids, relid);
 	}
 
@@ -2762,7 +2764,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 {
 	Form_pg_class classForm;
 	HeapTuple	classTup;
-	bool		dovacuum;
+	AutoVacType dovacuum;
 	bool		doanalyze;
 	autovac_table *tab = NULL;
 	bool		wraparound;
@@ -2792,10 +2794,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 
 	recheck_relation_needs_vacanalyze(relid, avopts, classForm,
 									  effective_multixact_freeze_max_age,
-									  &dovacuum, &doanalyze, &wraparound);
+									  &dovacuum, &doanalyze,
+									  &wraparound);
 
 	/* OK, it needs something done */
-	if (doanalyze || dovacuum)
+	if (doanalyze || dovacuum != AUTOVACUUM_NONE)
 	{
 		int			freeze_min_age;
 		int			freeze_table_age;
@@ -2860,7 +2863,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 		 * skip vac_update_datfrozenxid(); we'll do that separately.
 		 */
 		tab->at_params.options =
-			(dovacuum ? (VACOPT_VACUUM | VACOPT_SKIP_DATABASE_STATS) : 0) |
+			(dovacuum != AUTOVACUUM_NONE ? (VACOPT_VACUUM | VACOPT_SKIP_DATABASE_STATS) : 0) |
 			(doanalyze ? VACOPT_ANALYZE : 0) |
 			(!wraparound ? VACOPT_SKIP_LOCKED : 0);
 
@@ -2878,6 +2881,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 		tab->at_params.multixact_freeze_min_age = multixact_freeze_min_age;
 		tab->at_params.multixact_freeze_table_age = multixact_freeze_table_age;
 		tab->at_params.is_wraparound = wraparound;
+		tab->at_params.trigger = dovacuum;
 		tab->at_params.log_min_duration = log_min_duration;
 		tab->at_vacuum_cost_limit = vac_cost_limit;
 		tab->at_vacuum_cost_delay = vac_cost_delay;
@@ -2911,7 +2915,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
 								  AutoVacOpts *avopts,
 								  Form_pg_class classForm,
 								  int effective_multixact_freeze_max_age,
-								  bool *dovacuum,
+								  AutoVacType *dovacuum,
 								  bool *doanalyze,
 								  bool *wraparound)
 {
@@ -2933,9 +2937,9 @@ recheck_relation_needs_vacanalyze(Oid relid,
 /*
  * relation_needs_vacanalyze
  *
- * Check whether a relation needs to be vacuumed or analyzed; return each into
- * "dovacuum" and "doanalyze", respectively.  Also return whether the vacuum is
- * being forced because of Xid or multixact wraparound.
+ * Check whether a relation needs to be vacuumed or analyzed; return whether
+ * and why vacuum needs to be performed in *dovacuum, whether the table needs
+ * to be analyzed in *doanalyze.
  *
  * relopts is a pointer to the AutoVacOpts options (either for itself in the
  * case of a plain table, or for either itself or its parent table in the case
@@ -2966,6 +2970,15 @@ recheck_relation_needs_vacanalyze(Oid relid,
  * autovacuum_vacuum_threshold GUC variable.  Similarly, a vac_scale_factor
  * value < 0 is substituted with the value of
  * autovacuum_vacuum_scale_factor GUC variable.  Ditto for analyze.
+ *
+ *
+ * There can only be exactly one triggering condition for vacuum, even when
+ * multiple thresholds happened to be crossed at the same time.  We prefer to
+ * return "table XID age" in the event of such a conflict, after which we
+ * prefer "table MXID age" as the criteria, then "dead tuples", with "inserted
+ * tuples" placed last.  These predecence rules are largely arbitrary.  We
+ * must at least ensure that all antiwraparound autovacuums are advertised as
+ * triggered by table XID/MXID age criteria.
  */
 static void
 relation_needs_vacanalyze(Oid relid,
@@ -2974,11 +2987,10 @@ relation_needs_vacanalyze(Oid relid,
 						  PgStat_StatTabEntry *tabentry,
 						  int effective_multixact_freeze_max_age,
  /* output params below */
-						  bool *dovacuum,
+						  AutoVacType *dovacuum,
 						  bool *doanalyze,
 						  bool *wraparound)
 {
-	bool		force_vacuum;
 	bool		av_enabled;
 	float4		reltuples;		/* pg_class.reltuples */
 
@@ -3055,24 +3067,27 @@ relation_needs_vacanalyze(Oid relid,
 	xidForceLimit = recentXid - freeze_max_age;
 	if (xidForceLimit < FirstNormalTransactionId)
 		xidForceLimit -= FirstNormalTransactionId;
-	force_vacuum = (TransactionIdIsNormal(classForm->relfrozenxid) &&
-					TransactionIdPrecedes(classForm->relfrozenxid,
-										  xidForceLimit));
-	if (!force_vacuum)
-	{
-		multiForceLimit = recentMulti - multixact_freeze_max_age;
-		if (multiForceLimit < FirstMultiXactId)
-			multiForceLimit -= FirstMultiXactId;
-		force_vacuum = MultiXactIdIsValid(classForm->relminmxid) &&
-			MultiXactIdPrecedes(classForm->relminmxid, multiForceLimit);
-	}
-	*wraparound = force_vacuum;
+	multiForceLimit = recentMulti - multixact_freeze_max_age;
+	if (multiForceLimit < FirstMultiXactId)
+		multiForceLimit -= FirstMultiXactId;
+
+	/* See header comments about trigger precedence */
+	if (TransactionIdIsNormal(classForm->relfrozenxid) &&
+		TransactionIdPrecedes(classForm->relfrozenxid, xidForceLimit))
+		*dovacuum = AUTOVACUUM_TABLE_XID_AGE;
+	else if (MultiXactIdIsValid(classForm->relminmxid) &&
+			 MultiXactIdPrecedes(classForm->relminmxid, multiForceLimit))
+		*dovacuum = AUTOVACUUM_TABLE_MXID_AGE;
+	else
+		*dovacuum = AUTOVACUUM_NONE;
+
+	/* A table age autovacuum always gets antiwraparound protections */
+	*wraparound = *dovacuum != AUTOVACUUM_NONE;
 
 	/* User disabled it in pg_class.reloptions?  (But ignore if at risk) */
-	if (!av_enabled && !force_vacuum)
+	if (!av_enabled && !*wraparound)
 	{
 		*doanalyze = false;
-		*dovacuum = false;
 		return;
 	}
 
@@ -3112,9 +3127,18 @@ relation_needs_vacanalyze(Oid relid,
 				 NameStr(classForm->relname),
 				 vactuples, vacthresh, anltuples, anlthresh);
 
-		/* Determine if this table needs vacuum or analyze. */
-		*dovacuum = force_vacuum || (vactuples > vacthresh) ||
-			(vac_ins_base_thresh >= 0 && instuples > vacinsthresh);
+		/*
+		 * Determine if this table needs vacuum or analyze.
+		 *
+		 * See header comments about trigger precedence.
+		 */
+		if (*dovacuum == AUTOVACUUM_NONE)
+		{
+			if (vactuples > vacthresh)
+				*dovacuum = AUTOVACUUM_DEAD_TUPLES;
+			else if (vac_ins_base_thresh >= 0 && instuples > vacinsthresh)
+				*dovacuum = AUTOVACUUM_INSERTED_TUPLES;
+		}
 		*doanalyze = (anltuples > anlthresh);
 	}
 	else
@@ -3124,7 +3148,6 @@ relation_needs_vacanalyze(Oid relid,
 		 * for anti-wrap purposes.  If it's not acted upon, there's no need to
 		 * vacuum it.
 		 */
-		*dovacuum = force_vacuum;
 		*doanalyze = false;
 	}
 
@@ -3169,14 +3192,15 @@ autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
 static void
 autovac_report_activity(autovac_table *tab)
 {
-#define MAX_AUTOVAC_ACTIV_LEN (NAMEDATALEN * 2 + 56)
+#define MAX_AUTOVAC_ACTIV_LEN (NAMEDATALEN * 2 + 100)
 	char		activity[MAX_AUTOVAC_ACTIV_LEN];
 	int			len;
 
 	/* Report the command and possible options */
 	if (tab->at_params.options & VACOPT_VACUUM)
 		snprintf(activity, MAX_AUTOVAC_ACTIV_LEN,
-				 "autovacuum: VACUUM%s",
+				 "autovacuum due to %s: VACUUM%s",
+				 vac_autovacuum_trigger_msg(tab->at_params.trigger),
 				 tab->at_params.options & VACOPT_ANALYZE ? " ANALYZE" : "");
 	else
 		snprintf(activity, MAX_AUTOVAC_ACTIV_LEN,
