diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index c582021d29..c30d467644 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -250,6 +250,23 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>BACKGROUND</literal></term>
+    <listitem>
+     <para>
+      Specifies that <command>VACUUM</command> will make a request to
+      Autovacuum to process the task in the background. When this option is
+      specified the command returns immediately, without waiting for the
+      task to complete and without notification of success or failure.
+      Execution of the task by autovacuum will take place at a future time
+      and is not prioritized over its other planned actions. There is no
+      current limit on the number of background tasks that can be queued,
+      but the command will throw an ERROR if the task queue is full.
+      This option may only be specified with a single persistent table.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>TRUNCATE</literal></term>
     <listitem>
@@ -366,7 +383,8 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
    </para>
 
    <para>
-    <command>VACUUM</command> cannot be executed inside a transaction block.
+    <command>VACUUM</command> cannot be executed inside a transaction block,
+    except when the <option>BACKGROUND</option> option is specified.
    </para>
 
    <para>
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 7e386250ae..dcd309a5d0 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -208,7 +208,8 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 				recorded = AutoVacuumRequestWork(AVW_BRINSummarizeRange,
 												 RelationGetRelid(idxRel),
-												 lastPageRange);
+												 lastPageRange,
+												 0);
 				if (!recorded)
 					ereport(LOG,
 							(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 3c8ea21475..8fde1fcc3a 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -114,6 +114,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	bool		full = false;
 	bool		disable_page_skipping = false;
 	bool		process_toast = true;
+	bool		background = false;
 	ListCell   *lc;
 
 	/* index_cleanup and truncate values unspecified for now */
@@ -148,6 +149,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 			full = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "disable_page_skipping") == 0)
 			disable_page_skipping = defGetBoolean(opt);
+		else if (strcmp(opt->defname, "background") == 0)
+			background = defGetBoolean(opt);
 		else if (strcmp(opt->defname, "index_cleanup") == 0)
 		{
 			/* Interpret no string as the default, which is 'auto' */
@@ -244,6 +247,23 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 		}
 	}
 
+	if (background && (params.options & VACOPT_VERBOSE))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("VACUUM (BACKGROUND) cannot be verbose")));
+
+	if (background && (params.options & VACOPT_SKIP_LOCKED))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("VACUUM (BACKGROUND) cannot SKIP_LOCKED")));
+
+	if (background &&
+		(vacstmt->rels == NIL ||
+		list_length(vacstmt->rels) != 1))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("VACUUM (BACKGROUND) only allowed on a single table")));
+
 	/*
 	 * All freeze ages are zero if the FREEZE option is given; otherwise pass
 	 * them as -1 which means to use the default values.
@@ -269,6 +289,86 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	/* user-invoked vacuum uses VACOPT_VERBOSE instead of log_min_duration */
 	params.log_min_duration = -1;
 
+	/*
+	 * If we are requested to run vacuum in the background, request work
+	 * from Autovacuum. Make sure this is a table that AV can see
+	 * and check permissions now also.
+	 *
+	 * Note also that "background" is not a VACOPT item, since we don't need
+	 * vacuum() to know about that, and we wouldn't want autovacuum to itself
+	 * try to queue something into the background for execution.
+	 *
+	 * XXX note this does not yet handle partitioned tables correctly;
+	 * these just fail silently in vacuum_rel() at present.
+	 */
+	if (background)
+	{
+		foreach(lc, vacstmt->rels)
+		{
+			VacuumRelation *vrel = lfirst_node(VacuumRelation, lc);
+
+			/*
+			 * We need a lock to allow us to check permissions and to see
+			 * if the relation is persistent. We can't easily handle SKIP LOCKED here.
+			 * We release the lock almost immediately to avoid lock upgrade hazard
+			 * and to ensure we don't interfere with AV.
+			 */
+			Relation rel = relation_openrv(vrel->relation, AccessShareLock);
+
+			if (RelationUsesLocalBuffers(rel))
+			{
+				relation_close(rel, AccessShareLock);
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("BACKGROUND option not supported on temporary tables")));
+			}
+			else
+			{
+				bool requested;
+
+				/*
+				 * Check permissions, using identical check to vacuum_rel().
+				 *
+				 * Check if relation needs to be skipped based on ownership.  This check
+				 * happens also when building the relation list to vacuum for a manual
+				 * operation, and needs to be done additionally here as VACUUM could
+				 * happen across multiple transactions where relation ownership could have
+				 * changed in-between.  Make sure to only generate logs for VACUUM in this
+				 * case.
+				 */
+				if (!vacuum_is_relation_owner(RelationGetRelid(rel),
+											  rel->rd_rel,
+											  params.options & VACOPT_VACUUM))
+				{
+					relation_close(rel, AccessShareLock);
+					return;
+				}
+
+				/*
+				 * Request an autovacuum worker does the work for us,
+				 * and skip the actual execution in the current backend.
+				 * Note that we do not wait for the AV worker to complete
+				 * the task, nor do we check whether it succeeded or not.
+				 */
+				requested = AutoVacuumRequestWork(AVW_BackgroundVacuum,
+													RelationGetRelid(rel),
+													InvalidBlockNumber,
+													params.options);
+				if (requested)
+					ereport(NOTICE,
+							(errmsg("autovacuum of \"%s\" has been requested, using the options specified",
+									RelationGetRelationName(rel))));
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+							 errmsg("task queue full; background autovacuum of \"%s\" was not recorded",
+									RelationGetRelationName(rel))));
+				relation_close(rel, AccessShareLock);
+				return;
+			}
+		}
+	}
+
 	/* Now go through the common routine */
 	vacuum(vacstmt->rels, &params, NULL, isTopLevel);
 }
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 601834d4b4..446c87827c 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -258,6 +258,7 @@ typedef struct AutoVacuumWorkItem
 	Oid			avw_database;
 	Oid			avw_relation;
 	BlockNumber avw_blockNumber;
+	bits32      avw_vac_options;
 } AutoVacuumWorkItem;
 
 #define NUM_WORKITEMS	256
@@ -619,6 +620,12 @@ AutoVacLauncherMain(int argc, char *argv[])
 
 	AutoVacuumShmem->av_launcherpid = MyProcPid;
 
+	/*
+	 * Advertise our latch that backends can use to wake us up while we're
+	 * sleeping.
+	 */
+	ProcGlobal->autovacuumLatch = &MyProc->procLatch;
+
 	/*
 	 * Create the initial database list.  The invariant we want this list to
 	 * keep is that it's ordered by decreasing next_time.  As soon as an entry
@@ -2663,6 +2670,66 @@ perform_work_item(AutoVacuumWorkItem *workitem)
 									ObjectIdGetDatum(workitem->avw_relation),
 									Int64GetDatum((int64) workitem->avw_blockNumber));
 				break;
+			case AVW_BackgroundVacuum:
+				{
+					BufferAccessStrategy	bstrategy;
+					autovac_table			tab;
+
+					tab.at_relid = workitem->avw_relation;
+					tab.at_sharedrel = false;
+
+					tab.at_params.index_cleanup = VACOPTVALUE_ENABLED;
+					tab.at_params.truncate = VACOPTVALUE_DISABLED;
+
+					/* Set options */
+					tab.at_params.options = workitem->avw_vac_options;
+
+					/*
+					 * All freeze ages are zero if the FREEZE option is given; otherwise pass
+					 * them as -1 which means to use the default values.
+					*/
+					if (tab.at_params.options & VACOPT_FREEZE)
+					{
+						tab.at_params.freeze_min_age = 0;
+						tab.at_params.freeze_table_age = 0;
+						tab.at_params.multixact_freeze_min_age = 0;
+						tab.at_params.multixact_freeze_table_age = 0;
+					}
+					else
+					{
+						tab.at_params.freeze_min_age = -1;
+						tab.at_params.freeze_table_age = -1;
+						tab.at_params.multixact_freeze_min_age = -1;
+						tab.at_params.multixact_freeze_table_age = -1;
+					}
+
+					/* user-invoked vacuum is never "for wraparound" */
+					tab.at_params.is_wraparound = false;
+
+					/* use default values */
+					tab.at_params.log_min_duration = 0;
+					tab.at_params.nworkers = 0;
+
+					/* Deliberately avoid using autovacuum parameters, since this is for user */
+					tab.at_vacuum_cost_delay = VacuumCostDelay;
+					tab.at_vacuum_cost_limit = VacuumCostLimit;
+					tab.at_dobalance = false;
+
+					/* Set names in case of error */
+					tab.at_relname = pstrdup(get_rel_name(tab.at_relid));
+					tab.at_nspname = pstrdup(get_namespace_name(get_rel_namespace(tab.at_relid)));
+					tab.at_datname = pstrdup(get_database_name(MyDatabaseId));
+
+					/*
+					 * Create a buffer access strategy object for VACUUM to use.  We want to
+					 * use the same one across all the vacuum operations we perform, since the
+					 * point is for VACUUM not to blow out the shared cache.
+					 */
+					bstrategy = GetAccessStrategy(BAS_VACUUM);
+
+					autovacuum_do_vac_analyze(&tab, bstrategy);
+				}
+				break;
 			default:
 				elog(WARNING, "unrecognized work item found: type %d",
 					 workitem->avw_type);
@@ -3210,6 +3277,11 @@ autovac_report_workitem(AutoVacuumWorkItem *workitem,
 			snprintf(activity, MAX_AUTOVAC_ACTIV_LEN,
 					 "autovacuum: BRIN summarize");
 			break;
+
+		case AVW_BackgroundVacuum:
+			snprintf(activity, MAX_AUTOVAC_ACTIV_LEN,
+					 "autovacuum: user vacuum");
+			break;
 	}
 
 	/*
@@ -3250,7 +3322,7 @@ AutoVacuumingActive(void)
  */
 bool
 AutoVacuumRequestWork(AutoVacuumWorkItemType type, Oid relationId,
-					  BlockNumber blkno)
+					  BlockNumber blkno, bits32 options)
 {
 	int			i;
 	bool		result = false;
@@ -3273,6 +3345,7 @@ AutoVacuumRequestWork(AutoVacuumWorkItemType type, Oid relationId,
 		workitem->avw_database = MyDatabaseId;
 		workitem->avw_relation = relationId;
 		workitem->avw_blockNumber = blkno;
+		workitem->avw_vac_options = options;
 		result = true;
 
 		/* done */
@@ -3281,6 +3354,14 @@ AutoVacuumRequestWork(AutoVacuumWorkItemType type, Oid relationId,
 
 	LWLockRelease(AutovacuumLock);
 
+	/*
+	 * You might think we would issue a SetLatch here to wake up the
+	 * Autovacuum, but the current mechanism uses specifically timed
+	 * requests, so that would require significant rethinking.
+	 * We just need to be patient that AV will get to our task
+	 * eventually.
+	 */
+
 	return result;
 }
 
diff --git a/src/include/postmaster/autovacuum.h b/src/include/postmaster/autovacuum.h
index 9d40fd6d54..44cd979e9b 100644
--- a/src/include/postmaster/autovacuum.h
+++ b/src/include/postmaster/autovacuum.h
@@ -22,7 +22,8 @@
  */
 typedef enum
 {
-	AVW_BRINSummarizeRange
+	AVW_BRINSummarizeRange,
+	AVW_BackgroundVacuum
 } AutoVacuumWorkItemType;
 
 
@@ -74,7 +75,7 @@ extern void AutovacuumLauncherIAm(void);
 #endif
 
 extern bool AutoVacuumRequestWork(AutoVacuumWorkItemType type,
-								  Oid relationId, BlockNumber blkno);
+								  Oid relationId, BlockNumber blkno, bits32 options);
 
 /* shared memory stuff */
 extern Size AutoVacuumShmemSize(void);
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 8d096fdeeb..92fa275178 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -400,6 +400,8 @@ typedef struct PROC_HDR
 	Latch	   *walwriterLatch;
 	/* Checkpointer process's latch */
 	Latch	   *checkpointerLatch;
+	/* Autovacuum process's latch */
+	Latch	   *autovacuumLatch;
 	/* Current shared estimate of appropriate spins_per_delay value */
 	int			spins_per_delay;
 	/* Buffer id of the buffer that Startup process waits for pin on, or -1 */
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index c63a157e5f..1e3b182414 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -282,6 +282,13 @@ ALTER TABLE vactst ALTER COLUMN t SET STORAGE EXTERNAL;
 VACUUM (PROCESS_TOAST FALSE) vactst;
 VACUUM (PROCESS_TOAST FALSE, FULL) vactst;
 ERROR:  PROCESS_TOAST required with VACUUM FULL
+-- Single table inside transaction block
+VACUUM (BACKGROUND) vactst;
+NOTICE:  autovacuum of "vactst" has been requested, using the options specified
+BEGIN;
+VACUUM (BACKGROUND) vactst;
+NOTICE:  autovacuum of "vactst" has been requested, using the options specified
+COMMIT;
 DROP TABLE vaccluster;
 DROP TABLE vactst;
 DROP TABLE vacparted;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 9faa8a34a6..3782e13a2b 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -237,6 +237,12 @@ ALTER TABLE vactst ALTER COLUMN t SET STORAGE EXTERNAL;
 VACUUM (PROCESS_TOAST FALSE) vactst;
 VACUUM (PROCESS_TOAST FALSE, FULL) vactst;
 
+-- Single table inside transaction block
+VACUUM (BACKGROUND) vactst;
+BEGIN;
+VACUUM (BACKGROUND) vactst;
+COMMIT;
+
 DROP TABLE vaccluster;
 DROP TABLE vactst;
 DROP TABLE vacparted;
