diff --git src/wp-admin/includes/ajax-actions.php src/wp-admin/includes/ajax-actions.php
index a2e829bf65..e132ac8101 100644
--- src/wp-admin/includes/ajax-actions.php
+++ src/wp-admin/includes/ajax-actions.php
@@ -312,14 +312,33 @@ function wp_ajax_get_community_events() {
 	$saved_location = get_user_option( 'community-events-location', $user_id );
 	$events_client  = new WP_Community_Events( $user_id, $saved_location );
 	$events         = $events_client->get_events( $search, $timezone );
+	$ip_changed     = false;
 
 	if ( is_wp_error( $events ) ) {
 		wp_send_json_error( array(
 			'error' => $events->get_error_message(),
 		) );
 	} else {
-		if ( isset( $events['location'] ) ) {
-			// Store the location network-wide, so the user doesn't have to set it on each site.
+		if ( empty( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) ) {
+			$ip_changed = true;
+		} elseif ( isset( $saved_location['ip'] ) && ! empty( $events['location']['ip'] ) && $saved_location['ip'] !== $events['location']['ip'] ) {
+			$ip_changed = true;
+		}
+
+		/*
+		 * The location should only be updated when it changes. The API doesn't always return
+		 * a full location; sometimes it's missing the description or country. The location
+		 * that was saved during the initial request is known to be good and complete, though.
+		 * It should be left in tact until the user explicitly changes it (either by manually
+		 * searching for a new location, or by changing their IP address).
+		 *
+		 * If the location were updated with an incomplete response from the API, then it could
+		 * break assumptions that the UI makes (e.g., that there will always be a description
+		 * that corresponds to a latitude/longitude location).
+		 *
+		 * The location is stored network-wide, so that the user doesn't have to set it on each site.
+		 */
+		if ( $ip_changed || $search ) {
 			update_user_option( $user_id, 'community-events-location', $events['location'], true );
 		}
 
diff --git src/wp-admin/includes/class-wp-community-events.php src/wp-admin/includes/class-wp-community-events.php
index e27e731fff..9a41459740 100644
--- src/wp-admin/includes/class-wp-community-events.php
+++ src/wp-admin/includes/class-wp-community-events.php
@@ -94,12 +94,13 @@ class WP_Community_Events {
 			return $cached_events;
 		}
 
-		$request_url    = $this->get_request_url( $location_search, $timezone );
-		$response       = wp_remote_get( $request_url );
+		$api_url        = 'https://api.wordpress.org/events/1.0/';
+		$request_args   = $this->get_request_args( $location_search, $timezone );
+		$response       = wp_remote_get( $api_url, $request_args );
 		$response_code  = wp_remote_retrieve_response_code( $response );
 		$response_body  = json_decode( wp_remote_retrieve_body( $response ), true );
 		$response_error = null;
-		$debugging_info = compact( 'request_url', 'response_code', 'response_body' );
+		$debugging_info = compact( 'api_url', 'request_args', 'response_code', 'response_body' );
 
 		if ( is_wp_error( $response ) ) {
 			$response_error = $response;
@@ -128,6 +129,31 @@ class WP_Community_Events {
 				unset( $response_body['ttl'] );
 			}
 
+			/*
+			 * The IP in the response is usually the same as the one that was sent
+			 * in the request, but in some cases it is different. In those cases,
+			 * it's important to reset it back to the IP from the request.
+			 *
+			 * For example, if the IP sent in the request is private (e.g., 192.168.1.100),
+			 * then the API will ignore that and use the corresponding public IP instead,
+			 * and the public IP will get returned. If the public IP were saved, though,
+			 * then get_cached_events() would always return `false`, because the transient
+			 * would be generated based on the public IP when saving the cache, but generated
+			 * based on the private IP when retrieving the cache.
+			 */
+			if ( ! empty( $response_body['location']['ip'] ) ) {
+				$response_body['location']['ip'] = $request_args['body']['ip'];
+			}
+
+			/*
+			 * The API doesn't return a description for latitude/longitude requests,
+			 * but the description is already saved in the user location, so that
+			 * one can be used instead.
+			 */
+			if ( $this->coordinates_match( $request_args['body'], $response_body['location'] ) && empty( $response_body['location']['description'] ) ) {
+				$response_body['location']['description'] = $this->user_location['description'];
+			}
+
 			$this->cache_events( $response_body, $expiration );
 
 			$response_body = $this->trim_events( $response_body );
@@ -143,24 +169,23 @@ class WP_Community_Events {
 	}
 
 	/**
-	 * Builds a URL for requests to the w.org Events API.
+	 * Builds an array of args to use in an HTTP request to the w.org Events API.
 	 *
 	 * @access protected
 	 * @since 4.8.0
 	 *
 	 * @param  string $search   Optional. City search string. Default empty string.
 	 * @param  string $timezone Optional. Timezone string. Default empty string.
-	 * @return string The request URL.
+	 * @return @return array The request args.
 	 */
-	protected function get_request_url( $search = '', $timezone = '' ) {
-		$api_url = 'https://api.wordpress.org/events/1.0/';
-		$args    = array(
+	protected function get_request_args( $search = '', $timezone = '' ) {
+		$args = array(
 			'number' => 5, // Get more than three in case some get trimmed out.
-			'ip'     => $this->get_client_ip(),
+			'ip'     => self::get_unsafe_client_ip(),
 		);
 
 		/*
-		 * Send the minimal set of necessary arguments, in order to increase the
+		 * Include the minimal set of necessary arguments, in order to increase the
 		 * chances of a cache-hit on the API side.
 		 */
 		if ( empty( $search ) && isset( $this->user_location['latitude'], $this->user_location['longitude'] ) ) {
@@ -178,7 +203,10 @@ class WP_Community_Events {
 			}
 		}
 
-		return add_query_arg( $args, $api_url );
+		// Wrap the args in an array compatible with the second parameter of `wp_remote_get()`.
+		return array(
+			'body' => $args
+		);
 	}
 
 	/**
@@ -207,7 +235,7 @@ class WP_Community_Events {
 	 * @return false|string The anonymized address on success; the given address
 	 *                      or false on failure.
 	 */
-	protected function get_client_ip() {
+	public static function get_unsafe_client_ip() {
 		$client_ip = false;
 
 		// In order of preference, with the best ones for this purpose first.
@@ -250,6 +278,24 @@ class WP_Community_Events {
 	}
 
 	/**
+	 * Test if two pairs of latitude/longitude coordinates match each other.
+	 *
+	 * @since 4.8.0
+	 * @access protected
+	 *
+	 * @param array $a The first pair, with indexes 'latitude' and 'longitude'.
+	 * @param array $b The second pair, with indexes 'latitude' and 'longitude'.
+	 * @return bool True if they match, false if they don't.
+	 */
+	protected function coordinates_match( $a, $b ) {
+		if ( ! isset( $a['latitude'], $a['longitude'], $b['latitude'], $b['longitude'] ) ) {
+			return false;
+		}
+
+		return $a['latitude'] === $b['latitude'] && $a['longitude'] === $b['longitude'];
+	}
+
+	/**
 	 * Generates a transient key based on user location.
 	 *
 	 * This could be reduced to a one-liner in the calling functions, but it's
@@ -266,7 +312,9 @@ class WP_Community_Events {
 	protected function get_events_transient_key( $location ) {
 		$key = false;
 
-		if ( isset( $location['latitude'], $location['longitude'] ) ) {
+		if ( isset( $location['ip'] ) ) {
+			$key = 'community-events-' . md5( $location['ip'] );
+		} else if ( isset( $location['latitude'], $location['longitude'] ) ) {
 			$key = 'community-events-' . md5( $location['latitude'] . $location['longitude'] );
 		}
 
diff --git src/wp-admin/includes/dashboard.php src/wp-admin/includes/dashboard.php
index 922d2760b4..8c5daef7dd 100644
--- src/wp-admin/includes/dashboard.php
+++ src/wp-admin/includes/dashboard.php
@@ -1234,15 +1234,23 @@ function wp_print_community_events_templates() {
 
 	<script id="tmpl-community-events-no-upcoming-events" type="text/template">
 		<li class="event-none">
-			<?php printf(
-				/* translators: 1: the city the user searched for, 2: meetup organization documentation URL */
-				__( 'There aren&#8217;t any events scheduled near %1$s at the moment. Would you like to <a href="%2$s">organize one</a>?' ),
-				'{{ data.location.description }}',
-				__( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' )
-			); ?>
+			<# if ( data.location.description ) { #>
+				<?php printf(
+					/* translators: 1: the city the user searched for, 2: meetup organization documentation URL */
+					__( 'There aren&#8217;t any events scheduled near %1$s at the moment. Would you like to <a href="%2$s">organize one</a>?' ),
+					'{{ data.location.description }}',
+					__( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' )
+				); ?>
+
+			<# } else { #>
+				<?php printf(
+					/* translators: meetup organization documentation URL. */
+					__( 'There aren&#8217;t any events scheduled near you at the moment. Would you like to <a href="%s">organize one</a>?' ),
+					__( 'https://make.wordpress.org/community/handbook/meetup-organizer/welcome/' )
+				); ?>
+			<# } #>
 		</li>
 	</script>
-
 	<?php
 }
 
diff --git src/wp-admin/js/dashboard.js src/wp-admin/js/dashboard.js
index 6a9b5033d4..9a91265af6 100644
--- src/wp-admin/js/dashboard.js
+++ src/wp-admin/js/dashboard.js
@@ -369,7 +369,26 @@ jQuery( function( $ ) {
 			 * Determine which templates should be rendered and which elements
 			 * should be displayed.
 			 */
-			if ( templateParams.location ) {
+			if ( templateParams.location.ip ) {
+				/*
+				 * If the API determined the location by geolocating an IP, it will
+				 * provide events, but not a specific location.
+				 */
+				$locationMessage.text( communityEventsData.l10n.attend_event_near_generic );
+
+				if ( templateParams.events.length ) {
+					template = wp.template( 'community-events-event-list' );
+					$results.html( template( templateParams ) );
+				} else {
+					template = wp.template( 'community-events-no-upcoming-events' );
+					$results.html( template( templateParams ) );
+				}
+
+				elementVisibility['#community-events-location-message'] = true;
+				elementVisibility['.community-events-toggle-location']  = true;
+				elementVisibility['.community-events-results']          = true;
+
+			} else if ( templateParams.location.description ) {
 				template = wp.template( 'community-events-attend-event-near' );
 				$locationMessage.html( template( templateParams ) );
 
@@ -427,7 +446,7 @@ jQuery( function( $ ) {
 			 * location, and displaying the form would unnecessarily clutter the
 			 * widget.
 			 */
-			if ( 'app' === initiatedBy && templateParams.location ) {
+			if ( 'app' === initiatedBy && ( templateParams.location.ip || templateParams.location.latitude ) ) {
 				app.toggleLocationForm( 'hide' );
 			} else {
 				app.toggleLocationForm( 'show' );
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
index 5dad398001..ff7d8bb855 100644
--- src/wp-includes/script-loader.php
+++ src/wp-includes/script-loader.php
@@ -1012,9 +1012,24 @@ function wp_localize_community_events() {
 
 	require_once( ABSPATH . 'wp-admin/includes/class-wp-community-events.php' );
 
-	$user_id       = get_current_user_id();
-	$user_location = get_user_option( 'community-events-location', $user_id );
-	$events_client = new WP_Community_Events( $user_id, $user_location );
+	$user_id            = get_current_user_id();
+	$saved_location     = get_user_option( 'community-events-location', $user_id );
+	$saved_ip_address   = isset( $saved_location['ip'] ) ? $saved_location['ip'] : false;
+	$current_ip_address = WP_Community_Events::get_unsafe_client_ip();
+
+	/*
+	 * If the user's location is based on their IP address, then update their
+	 * location when their IP address changes. This allows them to see events
+	 * in their current city when travelling. Otherwise, they would always be
+	 * shown events in the city where they were when they first loaded the
+	 * Dashboard, which could have been months or years ago.
+	 */
+	if ( $saved_ip_address && $current_ip_address && $current_ip_address !== $saved_ip_address ) {
+		$saved_location['ip'] = $current_ip_address;
+		update_user_option( $user_id, 'community-events-location', $saved_location, true );
+	}
+
+	$events_client = new WP_Community_Events( $user_id, $saved_location );
 
 	wp_localize_script( 'dashboard', 'communityEventsData', array(
 		'nonce' => wp_create_nonce( 'community_events' ),
@@ -1023,6 +1038,7 @@ function wp_localize_community_events() {
 		'l10n' => array(
 			'enter_closest_city' => __( 'Enter your closest city to find nearby events.' ),
 			'error_occurred_please_try_again' => __( 'An error occurred. Please try again.' ),
+			'attend_event_near_generic' => __( 'Attend an upcoming event near you.' ),
 
 			/*
 			 * These specific examples were chosen to highlight the fact that a
