diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php
index ca4b3d03fd..3770e3e18b 100644
--- a/src/wp-includes/user.php
+++ b/src/wp-includes/user.php
@@ -536,6 +536,110 @@ function wp_validate_logged_in_cookie( $user_id ) {
 	return wp_validate_auth_cookie( $_COOKIE[ LOGGED_IN_COOKIE ], 'logged_in' );
 }
 
+/**
+ * Counts the number of posts for a particular post type.
+ *
+ * @param  int	  $userid      User ID.
+ * @param  string $post_type   Optional. Single post type or array of post types to count the number of posts for. Default 'post'.
+ * @param  bool   $public_only Optional. Whether to only return counts for public posts. Default false.
+ * @return int Number of posts the user has written in this post type.
+ */
+function count_user_posts_for_single_type( $userid, $post_type = 'post', $public_only = false ) {
+	global $wpdb;
+
+	$where 		 = get_posts_by_author_sql( $post_type, true, $userid, $public_only );
+	$where_hash  = wp_hash( $where );
+	$cache_key   = "count_user_{$post_type}_{$userid}_{$where_hash}";
+	$cache_group = $public_only ? 'user_posts_count_public' : 'user_posts_count';
+
+	// Try to get count from cache.
+	$count = wp_cache_get( $cache_key, $cache_group );
+
+	// If cache is empty, query the database.
+	if( false === $count ) {
+		$count = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" );
+
+		wp_cache_add( $cache_key, $count, $cache_group );
+	}
+
+	return absint( $count );
+}
+
+/**
+ * Clears the cached count for user for given post type.
+ *
+ * @param int 	 $userid    User ID.
+ * @param string $post_type Post type.
+ */
+function clear_count_user_posts_cache( $userid, $post_type ) {
+	$cache_key   = "count_user_{$post_type}_{$userid}";
+	$cache_groups = [
+		'user_posts_count_public',
+		'user_posts_count',
+	];
+
+	foreach( $cache_groups as $cache_group ) {
+		wp_cache_delete( $cache_key, $cache_group );
+	}
+}
+
+/**
+ * Check if post author has changed. If changed, clear count posts cache for user.
+ *
+ * @param int     $post_ID      Post ID.
+ * @param WP_Post $post_after   Post object following the update.
+ * @param WP_Post $post_before  Post object before the update.
+ */
+function check_author_change( $post_id, $post_after, $post_before ) {
+	if ( $post_after->post_author !== $post_before->post_author ) {
+		$post_type = get_post_type( $post_id );
+
+		clear_count_user_posts_cache( $post_after->post_author, $post_type );
+		clear_count_user_posts_cache( $post_before->post_author, $post_type );
+	}
+}
+add_action( 'post_updated', 'check_author_change', 10, 3 );
+
+/**
+ * When the post is created, clear the cache.
+ *
+ * @param int 	  $post_id Post ID.
+ * @param WP_Post $post Post object.
+ * @param bool 	  $update Whether this is an existing post being updated.
+ *
+ * @return void
+ */
+function clear_cache_on_post_update( $post_id, $post ) {
+	// Don't do anything if revision is being saved.
+	if ( wp_is_post_revision( $post_id ) ) {
+		return;
+	}
+
+	$post_type = $post->post_type;
+	$author_id = $post->post_author;
+
+	clear_count_user_posts_cache( $author_id, $post_type );
+}
+add_action( 'save_post', 'clear_cache_on_post_update', 10, 2 );
+
+/**
+ * Counts the number of posts for given post types.
+ *
+ * @param  int	  $userid      User ID.
+ * @param  array  $post_types  Optional. Single post type or array of post types to count the number of posts for. Default 'post'.
+ * @param  bool   $public_only Optional. Whether to only return counts for public posts. Default false.
+ * @return int Number of posts the user has written in this post type.
+ */
+function count_user_posts_for_multiple_types( $userid, $post_types = [ 'post' ], $public_only = false ) {
+	$numposts = 0;
+
+    foreach( $post_types as $post_type ) {
+        $numposts += count_user_posts_for_single_type( $userid, $post_type, $public_only );
+    }
+
+	return $numposts;
+}
+
 /**
  * Gets the number of posts a user has written.
  *
@@ -549,14 +653,16 @@ function wp_validate_logged_in_cookie( $user_id ) {
  * @param int          $userid      User ID.
  * @param array|string $post_type   Optional. Single post type or array of post types to count the number of posts for. Default 'post'.
  * @param bool         $public_only Optional. Whether to only return counts for public posts. Default false.
- * @return string Number of posts the user has written in this post type.
+ * @return int Number of posts the user has written in this post type.
  */
 function count_user_posts( $userid, $post_type = 'post', $public_only = false ) {
-	global $wpdb;
+	$count = 0;
 
-	$where = get_posts_by_author_sql( $post_type, true, $userid, $public_only );
-
-	$count = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts $where" );
+	if ( is_array( $post_type ) ) {
+        $count = count_user_posts_for_multiple_types( $userid, $post_type, $public_only );
+    } else {
+		$count = count_user_posts_for_single_type( $userid, $post_type, $public_only );
+	}
 
 	/**
 	 * Filters the number of posts a user has written.
@@ -586,19 +692,13 @@ function count_user_posts( $userid, $post_type = 'post', $public_only = false )
  * @return string[] Amount of posts each user has written, as strings, keyed by user ID.
  */
 function count_many_users_posts( $users, $post_type = 'post', $public_only = false ) {
-	global $wpdb;
-
 	$count = array();
 	if ( empty( $users ) || ! is_array( $users ) ) {
 		return $count;
 	}
 
-	$userlist = implode( ',', array_map( 'absint', $users ) );
-	$where    = get_posts_by_author_sql( $post_type, true, null, $public_only );
-
-	$result = $wpdb->get_results( "SELECT post_author, COUNT(*) FROM $wpdb->posts $where AND post_author IN ($userlist) GROUP BY post_author", ARRAY_N );
-	foreach ( $result as $row ) {
-		$count[ $row[0] ] = $row[1];
+	foreach ( $users as $user_id ) {
+		$count[ $user_id ] = count_user_posts( $user_id, $post_type, $public_only );
 	}
 
 	foreach ( $users as $id ) {
diff --git a/tests/phpunit/tests/user.php b/tests/phpunit/tests/user.php
index 7157a00cfa..0df2c2dbd8 100644
--- a/tests/phpunit/tests/user.php
+++ b/tests/phpunit/tests/user.php
@@ -560,21 +560,21 @@ class Tests_User extends WP_UnitTestCase {
 
 		wp_set_current_user( self::$author_id );
 		$counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', false );
-		$this->assertSame( '1', $counts[ self::$author_id ] );
-		$this->assertSame( '1', $counts[ $user_id_b ] );
+		$this->assertSame( 1, $counts[ self::$author_id ] );
+		$this->assertSame( 1, $counts[ $user_id_b ] );
 
 		$counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', true );
-		$this->assertSame( '1', $counts[ self::$author_id ] );
-		$this->assertSame( '1', $counts[ $user_id_b ] );
+		$this->assertSame( 1, $counts[ self::$author_id ] );
+		$this->assertSame( 1, $counts[ $user_id_b ] );
 
 		wp_set_current_user( $user_id_b );
 		$counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', false );
-		$this->assertSame( '1', $counts[ self::$author_id ] );
-		$this->assertSame( '2', $counts[ $user_id_b ] );
+		$this->assertSame( 1, $counts[ self::$author_id ] );
+		$this->assertSame( 2, $counts[ $user_id_b ] );
 
 		$counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', true );
-		$this->assertSame( '1', $counts[ self::$author_id ] );
-		$this->assertSame( '1', $counts[ $user_id_b ] );
+		$this->assertSame( 1, $counts[ self::$author_id ] );
+		$this->assertSame( 1, $counts[ $user_id_b ] );
 	}
 
 	/**
diff --git a/tests/phpunit/tests/user/countUserPosts.php b/tests/phpunit/tests/user/countUserPosts.php
index dbc94f418e..ebf9722ab2 100644
--- a/tests/phpunit/tests/user/countUserPosts.php
+++ b/tests/phpunit/tests/user/countUserPosts.php
@@ -59,34 +59,34 @@ class Tests_User_CountUserPosts extends WP_UnitTestCase {
 	}
 
 	public function test_count_user_posts_post_type_should_default_to_post() {
-		$this->assertSame( '4', count_user_posts( self::$user_id ) );
+		$this->assertSame( 4, count_user_posts( self::$user_id ) );
 	}
 
 	/**
 	 * @ticket 21364
 	 */
 	public function test_count_user_posts_post_type_post() {
-		$this->assertSame( '4', count_user_posts( self::$user_id, 'post' ) );
+		$this->assertSame( 4, count_user_posts( self::$user_id, 'post' ) );
 	}
 
 	/**
 	 * @ticket 21364
 	 */
 	public function test_count_user_posts_post_type_cpt() {
-		$this->assertSame( '3', count_user_posts( self::$user_id, 'wptests_pt' ) );
+		$this->assertSame( 3, count_user_posts( self::$user_id, 'wptests_pt' ) );
 	}
 
 	/**
 	 * @ticket 32243
 	 */
 	public function test_count_user_posts_with_multiple_post_types() {
-		$this->assertSame( '7', count_user_posts( self::$user_id, array( 'wptests_pt', 'post' ) ) );
+		$this->assertSame( 7, count_user_posts( self::$user_id, array( 'wptests_pt', 'post' ) ) );
 	}
 
 	/**
 	 * @ticket 32243
 	 */
 	public function test_count_user_posts_should_ignore_non_existent_post_types() {
-		$this->assertSame( '4', count_user_posts( self::$user_id, array( 'foo', 'post' ) ) );
+		$this->assertSame( 4, count_user_posts( self::$user_id, array( 'foo', 'post' ) ) );
 	}
 }
