diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js
index d3058051956a6ff1df1d73f18733cc101b8bd23d..5e5d1cfb9d515436a5ff76396518292817c2f04d 100755
--- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js
+++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js
@@ -13,6 +13,7 @@ import clearPresentationPods from '/imports/api/presentation-pods/server/modifie
 import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers';
 import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';
 import clearNote from '/imports/api/note/server/modifiers/clearNote';
+import clearNetworkInformation from '/imports/api/network-information/server/modifiers/clearNetworkInformation';
 
 export default function meetingHasEnded(meetingId) {
   return Meetings.remove({ meetingId }, () => {
@@ -28,6 +29,7 @@ export default function meetingHasEnded(meetingId) {
     clearVoiceUsers(meetingId);
     clearUserInfo(meetingId);
     clearNote(meetingId);
+    clearNetworkInformation(meetingId);
 
     return Logger.info(`Cleared Meetings with id ${meetingId}`);
   });
diff --git a/bigbluebutton-html5/imports/api/network-information/index.js b/bigbluebutton-html5/imports/api/network-information/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..de033433a7584c7145c9778ac205ecc165c91b78
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/index.js
@@ -0,0 +1,9 @@
+import { Meteor } from 'meteor/meteor';
+
+const NetworkInformation = new Mongo.Collection('network-information');
+
+if (Meteor.isServer) {
+  NetworkInformation._ensureIndex({ meetingId: 1 });
+}
+
+export default NetworkInformation;
diff --git a/bigbluebutton-html5/imports/api/network-information/server/index.js b/bigbluebutton-html5/imports/api/network-information/server/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..c043da1bfd8f0bb2867c94b40f8dacbf5bf17bf7
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/server/index.js
@@ -0,0 +1,2 @@
+import './methods';
+import './publisher';
diff --git a/bigbluebutton-html5/imports/api/network-information/server/methods.js b/bigbluebutton-html5/imports/api/network-information/server/methods.js
new file mode 100644
index 0000000000000000000000000000000000000000..6eaf424c0d48693e695690f5330d48ed3cb74300
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/server/methods.js
@@ -0,0 +1,6 @@
+import { Meteor } from 'meteor/meteor';
+import userInstabilityDetected from './methods/userInstabilityDetected';
+
+Meteor.methods({
+  userInstabilityDetected,
+});
diff --git a/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js b/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js
new file mode 100644
index 0000000000000000000000000000000000000000..790058e8e08ebdc9b4ea37c11ac6be2c138cdfd8
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js
@@ -0,0 +1,22 @@
+import { check } from 'meteor/check';
+import NetworkInformation from '/imports/api/network-information';
+import Logger from '/imports/startup/server/logger';
+
+export default function userInstabilityDetected(credentials, sender) {
+  const { meetingId, requesterUserId: receiver } = credentials;
+
+  check(meetingId, String);
+  check(receiver, String);
+  check(sender, String);
+
+  const payload = {
+    time: new Date().getTime(),
+    meetingId,
+    receiver,
+    sender,
+  };
+
+  Logger.debug(`Receiver ${receiver} reported a network instability in meeting ${meetingId}`);
+
+  return NetworkInformation.insert(payload);
+}
diff --git a/bigbluebutton-html5/imports/api/network-information/server/modifiers/clearNetworkInformation.js b/bigbluebutton-html5/imports/api/network-information/server/modifiers/clearNetworkInformation.js
new file mode 100644
index 0000000000000000000000000000000000000000..c272d64ab2a27a454d253e03758dd10d50458be0
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/server/modifiers/clearNetworkInformation.js
@@ -0,0 +1,14 @@
+import NetworkInformation from '/imports/api/network-information';
+import Logger from '/imports/startup/server/logger';
+
+export default function clearNetworkInformation(meetingId) {
+  if (meetingId) {
+    return NetworkInformation.remove({ meetingId }, () => {
+      Logger.info(`Cleared Network Information (${meetingId})`);
+    });
+  }
+
+  return NetworkInformation.remove({}, () => {
+    Logger.info('Cleared Network Information (all)');
+  });
+}
diff --git a/bigbluebutton-html5/imports/api/network-information/server/publisher.js b/bigbluebutton-html5/imports/api/network-information/server/publisher.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e83b5cb9e987e80eb03f395f64fbf57a6aa4062
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/server/publisher.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import NetworkInformation from '/imports/api/network-information';
+
+function networkInformation(credentials) {
+  const { meetingId } = credentials;
+
+  check(meetingId, String);
+
+  return NetworkInformation.find({
+    meetingId,
+  });
+}
+
+function publish(...args) {
+  const boundNetworkInformation = networkInformation.bind(this);
+
+  return boundNetworkInformation(...args);
+}
+
+Meteor.publish('network-information', publish);
diff --git a/bigbluebutton-html5/imports/api/users/server/methods.js b/bigbluebutton-html5/imports/api/users/server/methods.js
index 5e70dbd470eed2216fe9ce2cb81dec9fe8b3def2..d53ecc255853c48da5c3c9f86dfc92edd601c659 100644
--- a/bigbluebutton-html5/imports/api/users/server/methods.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods.js
@@ -5,7 +5,9 @@ import assignPresenter from './methods/assignPresenter';
 import changeRole from './methods/changeRole';
 import removeUser from './methods/removeUser';
 import toggleUserLock from './methods/toggleUserLock';
+import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType';
 import userActivitySign from './methods/userActivitySign';
+import userChangedSettings from './methods/userChangedSettings';
 import userLeftMeeting from './methods/userLeftMeeting';
 
 Meteor.methods({
@@ -15,6 +17,8 @@ Meteor.methods({
   removeUser,
   validateAuthToken,
   toggleUserLock,
+  setUserEffectiveConnectionType,
   userActivitySign,
+  userChangedSettings,
   userLeftMeeting,
 });
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js b/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js
new file mode 100644
index 0000000000000000000000000000000000000000..fd6a632d90cfb6425b9d6d661558a94c1f92a79f
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js
@@ -0,0 +1,29 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import RedisPubSub from '/imports/startup/server/redis';
+import Logger from '/imports/startup/server/logger';
+import setEffectiveConnectionType from '../modifiers/setUserEffectiveConnectionType';
+
+export default function setUserEffectiveConnectionType(credentials, effectiveConnectionType) {
+  const REDIS_CONFIG = Meteor.settings.private.redis;
+  const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+  const EVENT_NAME = 'ChangeUserEffectiveConnectionMsg';
+
+  const { meetingId, requesterUserId } = credentials;
+
+  check(meetingId, String);
+  check(requesterUserId, String);
+  check(effectiveConnectionType, String);
+
+  const payload = {
+    meetingId,
+    userId: requesterUserId,
+    effectiveConnectionType,
+  };
+
+  setEffectiveConnectionType(meetingId, requesterUserId, effectiveConnectionType);
+
+  Logger.verbose(`User ${requesterUserId} effective connection updated to ${effectiveConnectionType}`);
+
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
+}
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/userChangedSettings.js b/bigbluebutton-html5/imports/api/users/server/methods/userChangedSettings.js
new file mode 100644
index 0000000000000000000000000000000000000000..4eecd37d2aac5c76fd2d5b79ad89094e1f540ca6
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/users/server/methods/userChangedSettings.js
@@ -0,0 +1,30 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import RedisPubSub from '/imports/startup/server/redis';
+import setChangedSettings from '../modifiers/setChangedSettings';
+
+export default function userChangedSettings(credentials, setting, value) {
+  const REDIS_CONFIG = Meteor.settings.private.redis;
+  const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+  const EVENT_NAME = 'UserChangedSettingsEvtMsg';
+
+  const { meetingId, requesterUserId } = credentials;
+
+  if (!meetingId || !requesterUserId) return;
+
+  check(meetingId, String);
+  check(requesterUserId, String);
+  check(setting, String);
+  check(value, Match.Any);
+
+  const payload = {
+    meetingId,
+    requesterUserId,
+    setting,
+    value,
+  };
+
+  setChangedSettings(requesterUserId, setting, value);
+
+  RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
+}
diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js
index d10c706ace352b07f6cd5268b67faed9b512b495..9102145f6e82e6afe68c5cba6e3779b59e188b07 100755
--- a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js
+++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js
@@ -80,6 +80,7 @@ export default function addUser(meetingId, user) {
           isBreakoutUser: Meeting.meetingProp.isBreakout,
           parentId: Meeting.breakoutProps.parentId,
         },
+        effectiveConnectionType: null,
         inactivityCheck: false,
         responseDelay: 0,
         loggedOut: false,
diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/setChangedSettings.js b/bigbluebutton-html5/imports/api/users/server/modifiers/setChangedSettings.js
new file mode 100644
index 0000000000000000000000000000000000000000..2278151048fd285b6c95125e2a8ae25fa3d359c5
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/users/server/modifiers/setChangedSettings.js
@@ -0,0 +1,31 @@
+import { check } from 'meteor/check';
+import Users from '/imports/api/users';
+import Logger from '/imports/startup/server/logger';
+
+export default function setChangedSettings(userId, setting, value) {
+  check(userId, String);
+  check(setting, String);
+  check(value, Match.Any);
+
+  const selector = {
+    userId,
+  };
+
+  const modifier = {
+    $set: {},
+  };
+
+  modifier.$set[setting] = value;
+
+  const cb = (err, numChanged) => {
+    if (err) {
+      Logger.error(`${err}`);
+    }
+
+    if (numChanged) {
+      Logger.info(`Updated setting ${setting} to ${value} for user ${userId}`);
+    }
+  };
+
+  return Users.update(selector, modifier, cb);
+}
diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/setUserEffectiveConnectionType.js b/bigbluebutton-html5/imports/api/users/server/modifiers/setUserEffectiveConnectionType.js
new file mode 100644
index 0000000000000000000000000000000000000000..94543f5a019bd448a50e357fe928505bcd86fee7
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/users/server/modifiers/setUserEffectiveConnectionType.js
@@ -0,0 +1,33 @@
+import { check } from 'meteor/check';
+import Users from '/imports/api/users';
+import Logger from '/imports/startup/server/logger';
+
+export default function setUserEffectiveConnectionType(meetingId, userId, effectiveConnectionType) {
+  check(meetingId, String);
+  check(userId, String);
+  check(effectiveConnectionType, String);
+
+  const selector = {
+    meetingId,
+    userId,
+    effectiveConnectionType: { $ne: effectiveConnectionType },
+  };
+
+  const modifier = {
+    $set: {
+      effectiveConnectionType,
+    },
+  };
+
+  const cb = (err, numChanged) => {
+    if (err) {
+      Logger.error(`Updating user ${userId}: ${err}`);
+    }
+
+    if (numChanged) {
+      Logger.info(`Updated user ${userId} effective connection to ${effectiveConnectionType} in meeting ${meetingId}`);
+    }
+  };
+
+  return Users.update(selector, modifier, cb);
+}
diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx
index 9d57d5bc770118eb97bec012357c1563557c215b..66e3814742ad7654890a915fb010fa1eaf07d840 100755
--- a/bigbluebutton-html5/imports/ui/components/app/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx
@@ -16,6 +16,7 @@ import AudioContainer from '../audio/container';
 import ChatAlertContainer from '../chat/alert/container';
 import BannerBarContainer from '/imports/ui/components/banner-bar/container';
 import WaitingNotifierContainer from '/imports/ui/components/waiting-users/alert/container';
+import { startBandwidthMonitoring, updateNavigatorConnection } from '/imports/ui/services/network-information/index';
 import LockNotifier from '/imports/ui/components/lock-viewers/notify/container';
 
 import { styles } from './styles';
@@ -24,6 +25,7 @@ const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
 const APP_CONFIG = Meteor.settings.public.app;
 const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
 const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
+const ENABLE_NETWORK_INFORMATION = APP_CONFIG.enableNetworkInformation;
 
 const intlMessages = defineMessages({
   userListLabel: {
@@ -110,11 +112,22 @@ class App extends Component {
     this.handleWindowResize();
     window.addEventListener('resize', this.handleWindowResize, false);
 
+    if (ENABLE_NETWORK_INFORMATION) {
+      if (navigator.connection) {
+        this.handleNetworkConnection();
+        navigator.connection.addEventListener('change', this.handleNetworkConnection);
+      }
+
+      startBandwidthMonitoring();
+    }
+
+
     logger.info({ logCode: 'app_component_componentdidmount' }, 'Client loaded successfully');
   }
 
   componentWillUnmount() {
     window.removeEventListener('resize', this.handleWindowResize, false);
+    navigator.connection.addEventListener('change', this.handleNetworkConnection, false);
   }
 
   handleWindowResize() {
@@ -125,6 +138,10 @@ class App extends Component {
     this.setState({ enableResize: shouldEnableResize });
   }
 
+  handleNetworkConnection() {
+    updateNavigatorConnection(navigator.connection);
+  }
+
   renderPanel() {
     const { enableResize } = this.state;
     const { openPanel } = this.props;
diff --git a/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx
index 655b53243d91f7d5c858e69c938d82b62fcc1374..7210da50693e7cac87ef924d1fa5aa3c83c83f63 100644
--- a/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx
@@ -5,8 +5,10 @@ import { defineMessages, injectIntl } from 'react-intl';
 import _ from 'lodash';
 import Auth from '/imports/ui/services/auth';
 import Meetings from '/imports/api/meetings';
-import NavBarService from '../nav-bar/service';
+import Users from '/imports/api/users';
 import BreakoutRemainingTime from '/imports/ui/components/breakout-room/breakout-remaining-time/container';
+import SlowConnection from '/imports/ui/components/slow-connection/component';
+import NavBarService from '../nav-bar/service';
 
 import NotificationsBar from './component';
 
@@ -19,6 +21,14 @@ const STATUS_FAILED = 'failed';
 // failed to connect and waiting to try to reconnect
 const STATUS_WAITING = 'waiting';
 
+const METEOR_SETTINGS_APP = Meteor.settings.public.app;
+
+// https://github.com/bigbluebutton/bigbluebutton/issues/5286#issuecomment-465342716
+const SLOW_CONNECTIONS_TYPES = METEOR_SETTINGS_APP.effectiveConnection;
+const ENABLE_NETWORK_INFORMATION = METEOR_SETTINGS_APP.enableNetworkInformation;
+
+const HELP_LINK = METEOR_SETTINGS_APP.helpLink;
+
 const intlMessages = defineMessages({
   failedMessage: {
     id: 'app.failedMessage',
@@ -60,6 +70,14 @@ const intlMessages = defineMessages({
     id: 'app.meeting.alertBreakoutEndsUnderOneMinute',
     description: 'Alert that tells that the breakout end under a minute',
   },
+  slowEffectiveConnectionDetected: {
+    id: 'app.network.connection.effective.slow',
+    description: 'Alert for detected slow connections',
+  },
+  slowEffectiveConnectionHelpLink: {
+    id: 'app.network.connection.effective.slow.help',
+    description: 'Help link for slow connections',
+  },
 });
 
 const NotificationsBarContainer = (props) => {
@@ -104,6 +122,22 @@ export default injectIntl(withTracker(({ intl }) => {
   const { status, connected, retryTime } = Meteor.status();
   const data = {};
 
+  const user = Users.findOne({ userId: Auth.userID });
+
+  if (user) {
+    const { effectiveConnectionType } = user;
+    if (ENABLE_NETWORK_INFORMATION && SLOW_CONNECTIONS_TYPES.includes(effectiveConnectionType)) {
+      data.message = (
+        <SlowConnection effectiveConnectionType={effectiveConnectionType}>
+          {intl.formatMessage(intlMessages.slowEffectiveConnectionDetected)}
+          <a href={HELP_LINK} target="_blank" rel="noopener noreferrer">
+            {intl.formatMessage(intlMessages.slowEffectiveConnectionHelpLink)}
+          </a>
+        </SlowConnection>
+      );
+    }
+  }
+
   if (!connected) {
     data.color = 'primary';
     switch (status) {
diff --git a/bigbluebutton-html5/imports/ui/components/slow-connection/component.jsx b/bigbluebutton-html5/imports/ui/components/slow-connection/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..9d8c102a12689e5999912fe07d9a1ba63dbd7834
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/slow-connection/component.jsx
@@ -0,0 +1,32 @@
+import React, { Fragment } from 'react';
+import cx from 'classnames';
+import { styles } from './styles';
+
+const SLOW_CONNECTIONS_TYPES = {
+  critical: {
+    level: styles.bad,
+    bars: styles.oneBar,
+  },
+  danger: {
+    level: styles.bad,
+    bars: styles.twoBars,
+  },
+  warning: {
+    level: styles.warning,
+    bars: styles.threeBars,
+  },
+};
+
+const SlowConnection = ({ children, effectiveConnectionType, iconOnly }) => (
+  <Fragment>
+    <div className={cx(styles.signalBars, styles.sizingBox, SLOW_CONNECTIONS_TYPES[effectiveConnectionType].level, SLOW_CONNECTIONS_TYPES[effectiveConnectionType].bars)}>
+      <div className={cx(styles.firstBar, styles.bar)} />
+      <div className={cx(styles.secondBar, styles.bar)} />
+      <div className={cx(styles.thirdBar, styles.bar)} />
+      <div className={cx(styles.fourthBar, styles.bar)} />
+    </div>
+    {!iconOnly ? (<span>{children}</span>) : null}
+  </Fragment>
+);
+
+export default SlowConnection;
diff --git a/bigbluebutton-html5/imports/ui/components/slow-connection/styles.scss b/bigbluebutton-html5/imports/ui/components/slow-connection/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..7360ee18a1d91dad85b0439f684737486a3e29f1
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/slow-connection/styles.scss
@@ -0,0 +1,34 @@
+.sizingBox {
+  width: 27px;
+  height: 15px;
+  box-sizing: border-box;
+}
+
+.signalBars {
+  display: inline-block;
+}
+
+.signalBars .bar {
+  width: 4px;
+  margin-left: 1px;
+  display: inline-block;
+}
+.signalBars .bar.firstBar  { height: 25%; }
+.signalBars .bar.secondBar { height: 50%; }
+.signalBars .bar.thirdBar  { height: 75%; }
+.signalBars .bar.fourthBar { height: 100%; }
+
+.bad .bar {
+  background-color: #e74c3c;
+}
+.warning .bar {
+  background-color: #f1c40f;
+}
+
+.fourBars .bar.fifthBar,
+.threeBars .bar.fifthBar,
+.threeBars .bar.fourthBar,
+.oneBar .bar:not(.firstBar),
+.twoBars .bar:not(.firstBar):not(.secondBar) {
+  background-color: #c3c3c3;
+}
\ No newline at end of file
diff --git a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx
index 71cca0fdbabba51b9458698b054d007d3da12976..2168cba2ddaa1f5661138a1b3391d5423075f614 100644
--- a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx
@@ -16,6 +16,7 @@ const SUBSCRIPTIONS = [
   'users', 'meetings', 'polls', 'presentations', 'slides', 'captions',
   'voiceUsers', 'whiteboard-multi-user', 'screenshare', 'group-chat',
   'presentation-pods', 'users-settings', 'guestUser', 'users-infos', 'note',
+  'network-information',
 ];
 
 class Subscriptions extends Component {
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
index a5dd80287e34c1357baef3a51a604432cabb3fa5..3362870d2b3e04f1a85908f5c9e9e56128c959e0 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
@@ -490,6 +490,7 @@ class UserDropdown extends PureComponent {
   render() {
     const {
       compact,
+      currentUser,
       user,
       intl,
       isMeetingLocked,
@@ -508,6 +509,8 @@ class UserDropdown extends PureComponent {
 
     const userItemContentsStyle = {};
 
+    const { isModerator } = currentUser;
+
     userItemContentsStyle[styles.dropdown] = true;
     userItemContentsStyle[styles.userListItem] = !isActionsOpen;
     userItemContentsStyle[styles.usertListItemWithMenu] = isActionsOpen;
@@ -551,7 +554,7 @@ class UserDropdown extends PureComponent {
           {<UserIcons
             {...{
               user,
-              compact,
+              isModerator,
             }}
           />}
         </div>
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/component.jsx
index fd945bc2b79cce55c2164ec5bc5fb3ab387de788..ddccfe3773e0b0d33bd46051264857209a50b6fc 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/component.jsx
@@ -1,9 +1,16 @@
-import React from 'react';
+import React, { memo } from 'react';
 import PropTypes from 'prop-types';
-import Icon from '/imports/ui/components/icon/component';
+import SlowConnection from '/imports/ui/components/slow-connection/component';
+import Auth from '/imports/ui/services/auth';
 import { styles } from './styles';
 
+const METEOR_SETTINGS_APP = Meteor.settings.public.app;
+
+const SLOW_CONNECTIONS_TYPES = METEOR_SETTINGS_APP.effectiveConnection;
+const ENABLE_NETWORK_INFORMATION = METEOR_SETTINGS_APP.enableNetworkInformation;
+
 const propTypes = {
+  isModerator: PropTypes.bool.isRequired,
   user: PropTypes.shape({
     name: PropTypes.string.isRequired,
     isPresenter: PropTypes.bool.isRequired,
@@ -11,31 +18,33 @@ const propTypes = {
     isModerator: PropTypes.bool.isRequired,
     image: PropTypes.string,
   }).isRequired,
-  compact: PropTypes.bool.isRequired,
 };
 
 const UserIcons = (props) => {
   const {
-    user,
-    compact,
+    isModerator,
+    user: {
+      effectiveConnectionType,
+      id,
+    },
   } = props;
 
-  if (compact || user.isSharingWebcam) {
-    return null;
-  }
+  const showNetworkInformation = ENABLE_NETWORK_INFORMATION
+    && SLOW_CONNECTIONS_TYPES.includes(effectiveConnectionType)
+    && (id === Auth.userID || isModerator);
 
   return (
     <div className={styles.userIcons}>
       {
-        user.isSharingWebcam ?
+        showNetworkInformation ? (
           <span className={styles.userIconsContainer}>
-            <Icon iconName="video" />
+            <SlowConnection effectiveConnectionType={effectiveConnectionType} iconOnly />
           </span>
-          : null
+        ) : null
       }
     </div>
   );
 };
 
 UserIcons.propTypes = propTypes;
-export default UserIcons;
+export default memo(UserIcons);
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/styles.scss
index 523bac770a748dce6ce7688bd36c8d902545f08b..129edb5739a8f686465a3eea8aefd5c1e640003b 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/styles.scss
@@ -22,7 +22,7 @@
   justify-content: space-between;
   margin: 0 0 0 var(--sm-padding-x)/2;
   text-align: right;
-  font-size: 1rem;
+  font-size: 1.25rem;
   flex-shrink: 1;
   color: var(--user-icons-color);
 
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
index 97ed7bd488d7e31b43e8d817e95c7864495eab16..bdc35fa9a2c77cfb2cd4ce8384f54e5ec7a9a4f7 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
@@ -7,13 +7,21 @@ import { fetchWebRTCMappedStunTurnServers } from '/imports/utils/fetchStunTurnSe
 import ReconnectingWebSocket from 'reconnecting-websocket';
 import logger from '/imports/startup/client/logger';
 import browser from 'browser-detect';
+import {
+  updateCurrentWebcamsConnection,
+  getCurrentWebcams,
+  deleteWebcamConnection,
+  newWebcamConnection,
+  updateWebcamStats,
+} from '/imports/ui/services/network-information/index';
 import { tryGenerateIceCandidates } from '../../../utils/safari-webrtc';
 import Auth from '/imports/ui/services/auth';
 
 import VideoService from './service';
 import VideoList from './video-list/component';
 
-// const VIDEO_CONSTRAINTS = Meteor.settings.public.kurento.cameraConstraints;
+const APP_CONFIG = Meteor.settings.public.app;
+const ENABLE_NETWORK_INFORMATION = APP_CONFIG.enableNetworkInformation;
 const CAMERA_PROFILES = Meteor.settings.public.kurento.cameraProfiles;
 
 const intlClientErrors = defineMessages({
@@ -158,6 +166,30 @@ class VideoProvider extends Component {
 
     this.visibility.onVisible(this.unpauseViewers);
     this.visibility.onHidden(this.pauseViewers);
+
+    if (ENABLE_NETWORK_INFORMATION) {
+      this.currentWebcamsStatsInterval = setInterval(() => {
+        const currentWebcams = getCurrentWebcams();
+        if (!currentWebcams) return;
+
+        const { payload } = currentWebcams;
+
+        payload.forEach((id) => {
+          const peer = this.webRtcPeers[id];
+
+          const hasLocalStream = peer && peer.started === true
+            && peer.peerConnection.getLocalStreams().length > 0;
+          const hasRemoteStream = peer && peer.started === true
+            && peer.peerConnection.getRemoteStreams().length > 0;
+
+          if (hasLocalStream) {
+            this.customGetStats(peer.peerConnection, peer.peerConnection.getLocalStreams()[0].getVideoTracks()[0], (stats => updateWebcamStats(id, stats)), true);
+          } else if (hasRemoteStream) {
+            this.customGetStats(peer.peerConnection, peer.peerConnection.getRemoteStreams()[0].getVideoTracks()[0], (stats => updateWebcamStats(id, stats)), true);
+          }
+        });
+      }, 5000);
+    }
   }
 
   componentWillUpdate({ users, userId }) {
@@ -196,6 +228,8 @@ class VideoProvider extends Component {
       this.stopWebRTCPeer(id);
     });
 
+    clearInterval(this.currentWebcamsStatsInterval);
+
     // Close websocket connection to prevent multiple reconnects from happening
     this.ws.close();
   }
@@ -441,6 +475,10 @@ class VideoProvider extends Component {
         webRtcPeer.dispose();
       }
       delete this.webRtcPeers[id];
+      if (ENABLE_NETWORK_INFORMATION) {
+        deleteWebcamConnection(id);
+        updateCurrentWebcamsConnection(this.webRtcPeers);
+      }
     } else {
       this.logger('warn', 'No WebRTC peer to stop (not an error)', 'video_provider_no_peer_to_destroy', { cameraId: id });
     }
@@ -527,6 +565,10 @@ class VideoProvider extends Component {
       if (this.webRtcPeers[id].peerConnection) {
         this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = this._getOnIceConnectionStateChangeCallback(id);
       }
+      if (ENABLE_NETWORK_INFORMATION) {
+        newWebcamConnection(id);
+        updateCurrentWebcamsConnection(this.webRtcPeers);
+      }
     }
   }
 
@@ -680,7 +722,7 @@ class VideoProvider extends Component {
     }
   }
 
-  customGetStats(peer, mediaStreamTrack, callback) {
+  customGetStats(peer, mediaStreamTrack, callback, monitoring = false) {
     const { stats } = this.state;
     const statsState = stats;
     let promise;
@@ -735,6 +777,7 @@ class VideoProvider extends Component {
         encodeUsagePercent: videoInOrOutbound.encodeUsagePercent,
         rtt: videoInOrOutbound.rtt,
         currentDelay: videoInOrOutbound.currentDelay,
+        pliCount: videoInOrOutbound.pliCount,
       };
 
       const videoStatsArray = statsState;
@@ -742,7 +785,10 @@ class VideoProvider extends Component {
       while (videoStatsArray.length > 5) { // maximum interval to consider
         videoStatsArray.shift();
       }
-      this.setState({ stats: videoStatsArray });
+
+      if (!monitoring) {
+        this.setState({ stats: videoStatsArray });
+      }
 
       const firstVideoStats = videoStatsArray[0];
       const lastVideoStats = videoStatsArray[videoStatsArray.length - 1];
@@ -767,9 +813,9 @@ class VideoProvider extends Component {
 
       let videoBitrate;
       if (videoStats.packetsReceived > 0) { // Remote video
-        videoLostPercentage = ((videoStats.packetsLost / ((videoStats.packetsLost + videoStats.packetsReceived) * 100)) || 0).toFixed(1);
+        videoLostPercentage = ((videoStats.packetsLost / ((videoStats.packetsLost + videoStats.packetsReceived)) * 100) || 0).toFixed(1);
         videoBitrate = Math.floor(videoKbitsReceivedPerSecond || 0);
-        videoLostRecentPercentage = ((videoIntervalPacketsLost / ((videoIntervalPacketsLost + videoIntervalPacketsReceived) * 100)) || 0).toFixed(1);
+        videoLostRecentPercentage = ((videoIntervalPacketsLost / ((videoIntervalPacketsLost + videoIntervalPacketsReceived)) * 100) || 0).toFixed(1);
       } else {
         videoLostPercentage = (((videoStats.packetsLost / (videoStats.packetsLost + videoStats.packetsSent)) * 100) || 0).toFixed(1);
         videoBitrate = Math.floor(videoKbitsSentPerSecond || 0);
@@ -793,6 +839,7 @@ class VideoProvider extends Component {
           encodeUsagePercent: videoStats.encodeUsagePercent,
           rtt: videoStats.rtt,
           currentDelay: videoStats.currentDelay,
+          pliCount: videoStats.pliCount,
         },
       };
 
diff --git a/bigbluebutton-html5/imports/ui/services/network-information/index.js b/bigbluebutton-html5/imports/ui/services/network-information/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..b95177c0ac1dca8402e50713abfb5e6e4f19fa8a
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/services/network-information/index.js
@@ -0,0 +1,231 @@
+import NetworkInformation from '/imports/api/network-information';
+import { makeCall } from '/imports/ui/services/api';
+import Auth from '/imports/ui/services/auth';
+import Users from '/imports/api/users';
+import logger from '/imports/startup/client/logger';
+import _ from 'lodash';
+
+const NetworkInformationLocal = new Mongo.Collection(null);
+
+const NAVIGATOR_CONNECTION = 'NAVIGATOR_CONNECTION';
+const NUMBER_OF_WEBCAMS_CHANGED = 'NUMBER_OF_WEBCAMS_CHANGED';
+const STARTED_WEBCAM_SHARING = 'STARTED_WEBCAM_SHARING';
+const STOPPED_WEBCAM_SHARING = 'STOPPED_WEBCAM_SHARING';
+const WEBCAMS_GET_STATUS = 'WEBCAMS_GET_STATUS';
+
+const DANGER_BEGIN_TIME = 5000;
+const DANGER_END_TIME = 30000;
+
+const WARNING_END_TIME = 60000;
+
+let monitoringIntervalRef;
+
+export const updateCurrentWebcamsConnection = (connections) => {
+  const doc = {
+    timestamp: new Date().getTime(),
+    event: NUMBER_OF_WEBCAMS_CHANGED,
+    payload: Object.keys(connections),
+  };
+
+  NetworkInformationLocal.insert(doc);
+};
+
+export const deleteWebcamConnection = (id) => {
+  const doc = {
+    timestamp: new Date().getTime(),
+    event: STOPPED_WEBCAM_SHARING,
+    payload: { id },
+  };
+
+  NetworkInformationLocal.insert(doc);
+};
+
+export const getCurrentWebcams = () => NetworkInformationLocal
+  .findOne({
+    event: NUMBER_OF_WEBCAMS_CHANGED,
+  }, { sort: { timestamp: -1 } });
+
+export const newWebcamConnection = (id) => {
+  const doc = {
+    timestamp: new Date().getTime(),
+    event: STARTED_WEBCAM_SHARING,
+    payload: { id },
+  };
+
+  NetworkInformationLocal.insert(doc);
+};
+
+export const startBandwidthMonitoring = () => {
+  monitoringIntervalRef = setInterval(() => {
+    const monitoringTime = new Date().getTime();
+
+    const dangerLowerBoundary = monitoringTime - DANGER_BEGIN_TIME;
+
+    const warningLowerBoundary = monitoringTime - DANGER_END_TIME;
+    const warningUpperBoundary = monitoringTime - WARNING_END_TIME;
+
+    // Remove old documents to reduce the size of the local collection.
+    NetworkInformationLocal.remove({
+      event: WEBCAMS_GET_STATUS,
+      timestamp: { $lt: warningUpperBoundary },
+    });
+
+    const usersWatchingWebcams = Users.find({
+      userId: { $ne: Auth.userID },
+      viewParticipantsWebcams: true,
+      connectionStatus: 'online',
+    }).map(user => user.userId);
+
+    const warningZone = NetworkInformationLocal
+      .find({
+        event: WEBCAMS_GET_STATUS,
+        timestamp: { $lte: warningLowerBoundary, $gt: warningUpperBoundary },
+        $or: [
+          {
+            'payload.id': Auth.userID,
+            'payload.stats.deltaPliCount': { $gt: 0 },
+          },
+          {
+            'payload.id': { $ne: Auth.userID },
+            'payload.stats.deltaPacketsLost': { $gt: 0 },
+          },
+        ],
+      }).count();
+
+    const warningZoneReceivers = NetworkInformation
+      .find({
+        receiver: { $in: usersWatchingWebcams },
+        sender: Auth.userID,
+        time: { $lte: warningLowerBoundary, $gt: warningUpperBoundary },
+      }).count();
+
+    const dangerZone = _.uniqBy(NetworkInformationLocal
+      .find({
+        event: WEBCAMS_GET_STATUS,
+        timestamp: { $lt: dangerLowerBoundary, $gte: warningLowerBoundary },
+        $or: [
+          {
+            'payload.id': Auth.userID,
+            'payload.stats.deltaPliCount': { $gt: 0 },
+          },
+          {
+            'payload.id': { $ne: Auth.userID },
+            'payload.stats.deltaPacketsLost': { $gt: 0 },
+          },
+        ],
+      }).fetch(), 'payload.id').length;
+
+    const dangerZoneReceivers = _.uniqBy(NetworkInformation
+      .find({
+        receiver: { $in: usersWatchingWebcams },
+        sender: Auth.userID,
+        time: { $lt: dangerLowerBoundary, $gte: warningLowerBoundary },
+      }).fetch(), 'receiver').length;
+
+    let effectiveType = 'good';
+
+    if (dangerZone) {
+      if (!dangerZoneReceivers) {
+        effectiveType = 'danger';
+      }
+
+      if (dangerZoneReceivers === usersWatchingWebcams.length) {
+        effectiveType = 'danger';
+      }
+    } else if (warningZone) {
+      if (!warningZoneReceivers) {
+        effectiveType = 'warning';
+      }
+
+      if (warningZoneReceivers === usersWatchingWebcams.length) {
+        effectiveType = 'warning';
+      }
+    }
+
+    const lastEffectiveConnectionType = Users.findOne({ userId: Auth.userID });
+
+    if (lastEffectiveConnectionType
+      && lastEffectiveConnectionType.effectiveConnectionType !== effectiveType) {
+      logger.info({ logCode: 'user_connection_instability' }, `User ${Auth.userID} effective connection is now ${effectiveType}`);
+      makeCall('setUserEffectiveConnectionType', effectiveType);
+    }
+  }, 5000);
+};
+
+export const stopBandwidthMonitoring = () => {
+  clearInterval(monitoringIntervalRef);
+};
+
+export const updateNavigatorConnection = ({ effectiveType, downlink, rtt }) => {
+  const doc = {
+    timestamp: new Date().getTime(),
+    event: NAVIGATOR_CONNECTION,
+    payload: {
+      effectiveType,
+      downlink,
+      rtt,
+    },
+  };
+
+  NetworkInformationLocal.insert(doc);
+};
+
+export const updateWebcamStats = (id, stats) => {
+  if (!stats) return;
+
+  const lastStatus = NetworkInformationLocal
+    .findOne(
+      { event: WEBCAMS_GET_STATUS, 'payload.id': id },
+      { sort: { timestamp: -1 } },
+    );
+
+  const { video } = stats;
+
+  const doc = {
+    timestamp: new Date().getTime(),
+    event: WEBCAMS_GET_STATUS,
+    payload: { id, stats: video },
+  };
+
+  if (lastStatus) {
+    const {
+      payload: {
+        stats: {
+          packetsLost,
+          packetsReceived,
+          packetsSent,
+          pliCount,
+        },
+      },
+    } = lastStatus;
+    const normalizedVideo = { ...video };
+
+    normalizedVideo.deltaPacketsLost = video.packetsLost - packetsLost;
+    normalizedVideo.deltaPacketsReceived = video.packetsReceived - packetsReceived;
+    normalizedVideo.deltaPacketsSent = video.packetsSent - packetsSent;
+    normalizedVideo.deltaPliCount = video.pliCount - pliCount;
+
+    doc.payload = {
+      id,
+      stats: normalizedVideo,
+    };
+
+    if (normalizedVideo.deltaPacketsLost > 0) {
+      makeCall('userInstabilityDetected', id);
+    }
+  }
+
+  NetworkInformationLocal.insert(doc);
+};
+
+export default {
+  NetworkInformationLocal,
+  updateCurrentWebcamsConnection,
+  deleteWebcamConnection,
+  getCurrentWebcams,
+  newWebcamConnection,
+  startBandwidthMonitoring,
+  stopBandwidthMonitoring,
+  updateNavigatorConnection,
+  updateWebcamStats,
+};
diff --git a/bigbluebutton-html5/imports/ui/services/settings/index.js b/bigbluebutton-html5/imports/ui/services/settings/index.js
index c6d76c674d8b14386abcee36f681cf26715f6b26..575de766e99d842abe079ce219bb13270621d159 100644
--- a/bigbluebutton-html5/imports/ui/services/settings/index.js
+++ b/bigbluebutton-html5/imports/ui/services/settings/index.js
@@ -1,5 +1,6 @@
 import Storage from '/imports/ui/services/storage/session';
 import _ from 'lodash';
+import { makeCall } from '/imports/ui/services/api';
 
 const SETTINGS = [
   'application',
@@ -33,9 +34,9 @@ class Settings {
     });
 
     // Sets default locale to browser locale
-    defaultValues.application.locale = navigator.languages ? navigator.languages[0] : false ||
-                                       navigator.language ||
-                                       defaultValues.application.locale;
+    defaultValues.application.locale = navigator.languages ? navigator.languages[0] : false
+                                       || navigator.language
+                                       || defaultValues.application.locale;
 
     this.setDefault(defaultValues);
   }
@@ -55,7 +56,15 @@ class Settings {
   }
 
   save() {
-    Object.keys(this).forEach(k => Storage.setItem(`settings${k}`, this[k].value));
+    Object.keys(this).forEach((k) => {
+      if (k === '_dataSaving') {
+        const { value: { viewParticipantsWebcams } } = this[k];
+
+        makeCall('userChangedSettings', 'viewParticipantsWebcams', viewParticipantsWebcams);
+      }
+
+      Storage.setItem(`settings${k}`, this[k].value);
+    });
   }
 }
 
diff --git a/bigbluebutton-html5/imports/ui/services/user/mapUser.js b/bigbluebutton-html5/imports/ui/services/user/mapUser.js
index 14b554e43fc34f49a44b9a0df0328eeb37506a9a..435e907a41bcbf11ef84a372b840673ca75204e6 100755
--- a/bigbluebutton-html5/imports/ui/services/user/mapUser.js
+++ b/bigbluebutton-html5/imports/ui/services/user/mapUser.js
@@ -31,6 +31,7 @@ const mapUser = (user) => {
     isOnline: user.connectionStatus === 'online',
     clientType: user.clientType,
     loginTime: user.loginTime,
+    effectiveConnectionType: user.effectiveConnectionType,
     externalUserId: user.extId,
   };
 
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 341c5696ce55ba541d5da9063eba95566c7e557a..eb6455e0b8637479a4fbaf6bfb133bafdc2c2cfe 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -19,6 +19,7 @@ public:
     basename: "/html5client"
     askForFeedbackOnLogout: false
     allowUserLookup: false
+    enableNetworkInformation: false
     defaultSettings:
       application:
         animations: true
@@ -83,6 +84,10 @@ public:
     connectionTimeout: 60000
     showHelpButton: true
     enableExternalVideo: true
+    effectiveConnection:
+      - critical
+      - danger
+      - warning
   kurento:
     wsUrl: HOST
     chromeDefaultExtensionKey: akgoaoikmbmhcopjgakkcepdgdgkjfbc
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 235464ea8edcee6a5cc070b10da4c87341dffb9b..3d0281126a466ca9cf25ee4ac51ddd953252615c 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -610,6 +610,8 @@
     "app.externalVideo.urlInput": "Add YouTube URL",
     "app.externalVideo.urlError": "This URL isn't a valid YouTube video",
     "app.externalVideo.close": "Close",
+    "app.network.connection.effective.slow": "We're noticing connectivity issues.",
+    "app.network.connection.effective.slow.help": "How to fix it?",
     "app.externalVideo.noteLabel": "Note: Shared YouTube videos will not appear in the recording",
     "app.actionsBar.actionsDropdown.shareExternalVideo": "Share YouTube video",
     "app.actionsBar.actionsDropdown.stopShareExternalVideo": "Stop sharing YouTube video",
diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js
index ec9a43022fee20313fcff596985053d93539af2c..9906335729dec266c00b5c14b934943659e5407b 100755
--- a/bigbluebutton-html5/server/main.js
+++ b/bigbluebutton-html5/server/main.js
@@ -19,6 +19,7 @@ import '/imports/api/users-settings/server';
 import '/imports/api/voice-users/server';
 import '/imports/api/whiteboard-multi-user/server';
 import '/imports/api/video/server';
+import '/imports/api/network-information/server';
 import '/imports/api/users-infos/server';
 import '/imports/api/note/server';
 import '/imports/api/external-videos/server';