diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index 9349978d2f07254330b45ed384030401ffa77066..2230a1bac51ac4bda9ea4b0202ec74dcd4205423 100755
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -19,6 +19,8 @@ import AudioService from '/imports/ui/components/audio/service';
 import { FormattedMessage } from 'react-intl';
 import { notify } from '/imports/ui/services/notification';
 
+const BREAKOUT_END_NOTIFY_DELAY = 50;
+
 const HTML = document.getElementsByTagName('html')[0];
 
 let breakoutNotified = false;
@@ -237,6 +239,7 @@ const BaseContainer = withTracker(() => {
   const meeting = Meetings.findOne({ meetingId }, {
     fields: {
       meetingEnded: 1,
+      meetingProp: 1,
     },
   });
 
@@ -248,22 +251,32 @@ const BaseContainer = withTracker(() => {
   const ejected = User && User.ejected;
   let userSubscriptionHandler;
 
-
-  Breakouts.find().observeChanges({
+  Breakouts.find({}, { fields: { _id: 1 } }).observeChanges({
     added() {
       breakoutNotified = false;
     },
     removed() {
-      if (!AudioService.isUsingAudio() && !breakoutNotified) {
-        if (meeting && !meeting.meetingEnded) {
-          notify(
-            <FormattedMessage
-              id="app.toast.breakoutRoomEnded"
-              description="message when the breakout room is ended"
-            />,
-            'info',
-            'rooms',
-          );
+      // Need to check the number of breakouts left because if a user's role changes to viewer
+      // then all but one room is removed. The data here isn't reactive so no need to filter
+      // the fields
+      const numBreakouts = Breakouts.find().count();
+      if (!AudioService.isUsingAudio() && !breakoutNotified && numBreakouts === 0) {
+        if (meeting && !meeting.meetingEnded && !meeting.meetingProp.isBreakout) {
+          // There's a race condition when reloading a tab where the collection gets cleared
+          // out and then refilled. The removal of the old data triggers the notification so
+          // instead wait a bit and check to see that records weren't added right after.
+          setTimeout(() => {
+            if (breakoutNotified) {
+              notify(
+                <FormattedMessage
+                  id="app.toast.breakoutRoomEnded"
+                  description="message when the breakout room is ended"
+                />,
+                'info',
+                'rooms',
+              );
+            }
+          }, BREAKOUT_END_NOTIFY_DELAY);
         }
         breakoutNotified = true;
       }
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
index f0f349136ca5a3c6c89533750a202e5b27b82c97..a63f158a7af19c61f670be5158f322884a2923f8 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
@@ -170,8 +170,6 @@ class BreakoutRoom extends PureComponent {
       numberOfRoomsIsValid: true,
     };
 
-    this.breakoutFormId = _.uniqueId('breakout-form-');
-    this.freeJoinId = _.uniqueId('free-join-check-');
     this.btnLevelId = _.uniqueId('btn-set-level-');
   }
 
@@ -308,7 +306,7 @@ class BreakoutRoom extends PureComponent {
   }
 
   setFreeJoin(e) {
-    this.setState({ freeJoin: e.target.checked });
+    this.setState({ freeJoin: e.target.checked, valid: true });
   }
 
   setRecord(e) {
@@ -455,8 +453,8 @@ class BreakoutRoom extends PureComponent {
     if (isInvitation) return null;
 
     return (
-      <React.Fragment>
-        <div className={styles.breakoutSettings} key={this.breakoutFormId}>
+      <React.Fragment key="breakout-form">
+        <div className={styles.breakoutSettings}>
           <div>
             <p
               className={cx(styles.labelText, !numberOfRoomsIsValid
@@ -573,8 +571,8 @@ class BreakoutRoom extends PureComponent {
       record,
     } = this.state;
     return (
-      <div className={styles.checkBoxesContainer}>
-        <label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel} key={this.freeJoinId}>
+      <div className={styles.checkBoxesContainer} key="breakout-checkboxes">
+        <label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel} key="free-join-breakouts">
           <input
             type="checkbox"
             id="freeJoinCheckbox"
@@ -587,7 +585,7 @@ class BreakoutRoom extends PureComponent {
         </label>
         {
           isBreakoutRecordable ? (
-            <label htmlFor="recordBreakoutCheckbox" className={styles.freeJoinLabel} key={this.freeJoinId}>
+            <label htmlFor="recordBreakoutCheckbox" className={styles.freeJoinLabel} key="record-breakouts">
               <input
                 id="recordBreakoutCheckbox"
                 type="checkbox"
diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx
index 6aad239d925a5f33bffc12b28c84e39dcd67257f..2d0a90bb6fad3e0bd323f77b89c66969b74877a1 100644
--- a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx
@@ -155,7 +155,7 @@ class BreakoutRoom extends PureComponent {
     this.setState({ joinedAudioOnly: false, breakoutId });
   }
 
-  renderUserActions(breakoutId, number) {
+  renderUserActions(breakoutId, joinedUsers, number) {
     const {
       isMicrophoneUser,
       amIModerator,
@@ -189,7 +189,7 @@ class BreakoutRoom extends PureComponent {
       };
     return (
       <div className={styles.breakoutActions}>
-        {isUserInBreakoutRoom(breakoutId)
+        {isUserInBreakoutRoom(joinedUsers)
           ? (
             <span className={styles.alreadyConnected}>
               {intl.formatMessage(intlMessages.alreadyConnected)}
@@ -242,7 +242,6 @@ class BreakoutRoom extends PureComponent {
     const {
       breakoutRooms,
       intl,
-      getNumUsersByBreakoutId,
     } = this.props;
 
     const {
@@ -260,7 +259,7 @@ class BreakoutRoom extends PureComponent {
             {intl.formatMessage(intlMessages.breakoutRoom, breakout.sequence.toString())}
             <span className={styles.usersAssignedNumberLabel}>
               (
-              {getNumUsersByBreakoutId(breakout.breakoutId)}
+              {breakout.joinedUsers.length}
               )
             </span>
           </span>
@@ -269,7 +268,11 @@ class BreakoutRoom extends PureComponent {
               {intl.formatMessage(intlMessages.generatingURL)}
               <span className={styles.connectingAnimation} />
             </span>
-          ) : this.renderUserActions(breakout.breakoutId, breakout.sequence.toString())}
+          ) : this.renderUserActions(
+            breakout.breakoutId,
+            breakout.joinedUsers,
+            breakout.sequence.toString(),
+          )}
         </div>
         <div className={styles.joinedUserNames}>
           {breakout.joinedUsers
diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx
index c32cb3f069bf33314d64fcdbdcaf32d8fec140df..74113934765ed695a838f6f4c26eac72123a84fe 100644
--- a/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx
@@ -18,7 +18,6 @@ export default withTracker((props) => {
     meetingId,
     amIModerator,
     closeBreakoutPanel,
-    getNumUsersByBreakoutId,
     isUserInBreakoutRoom,
   } = Service;
   const breakoutRooms = findBreakouts();
@@ -37,7 +36,6 @@ export default withTracker((props) => {
     meetingId: meetingId(),
     amIModerator: amIModerator(),
     closeBreakoutPanel,
-    getNumUsersByBreakoutId,
     isMeteorConnected,
     isUserInBreakoutRoom,
   };
diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/component.jsx
index af0a733c443a87734caee48cc376572ea16c1223..3071a22776fe62b59103d3807080893a537f0556 100644
--- a/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/component.jsx
@@ -1,8 +1,31 @@
 import React, { Component } from 'react';
+import PropTypes from 'prop-types';
 import { Session } from 'meteor/session';
 import { withModalMounter } from '/imports/ui/components/modal/service';
 import BreakoutJoinConfirmation from '/imports/ui/components/breakout-join-confirmation/container';
 
+const BREAKOUT_MODAL_DELAY = 200;
+
+const propTypes = {
+  mountModal: PropTypes.func.isRequired,
+  currentBreakoutUser: PropTypes.shape({
+    insertedTime: PropTypes.number.isRequired,
+  }),
+  getBreakoutByUser: PropTypes.func.isRequired,
+  breakoutUserIsIn: PropTypes.shape({
+    sequence: PropTypes.number.isRequired,
+  }),
+  breakouts: PropTypes.arrayOf(PropTypes.shape({
+    freeJoin: PropTypes.bool.isRequired,
+  })),
+};
+
+const defaultProps = {
+  currentBreakoutUser: undefined,
+  breakoutUserIsIn: undefined,
+  breakouts: [],
+};
+
 const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal(
   <BreakoutJoinConfirmation
     breakout={breakout}
@@ -13,40 +36,80 @@ const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mou
 const closeBreakoutJoinConfirmation = mountModal => mountModal(null);
 
 class BreakoutRoomInvitation extends Component {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      didSendBreakoutInvite: false,
+    };
+  }
+
+  componentDidMount() {
+    // use dummy old data on mount so it works if no data changes
+    this.checkBreakouts({ breakouts: [] });
+  }
+
   componentDidUpdate(oldProps) {
+    this.checkBreakouts(oldProps);
+  }
+
+  checkBreakouts(oldProps) {
     const {
       breakouts,
       mountModal,
       currentBreakoutUser,
       getBreakoutByUser,
+      breakoutUserIsIn,
     } = this.props;
 
-    const hadBreakouts = oldProps.breakouts.length;
-    const hasBreakouts = breakouts.length;
+    const {
+      didSendBreakoutInvite,
+    } = this.state;
+
+    const hadBreakouts = oldProps.breakouts.length > 0;
+    const hasBreakouts = breakouts.length > 0;
     if (!hasBreakouts && hadBreakouts) {
       closeBreakoutJoinConfirmation(mountModal);
     }
 
-    if (hasBreakouts && currentBreakoutUser) {
-      const currentIsertedTime = currentBreakoutUser.insertedTime;
-      const oldCurrentUser = oldProps.currentBreakoutUser || {};
-      const oldInsertedTime = oldCurrentUser.insertedTime;
-
-      if (currentIsertedTime !== oldInsertedTime) {
-        const breakoutRoom = getBreakoutByUser(currentBreakoutUser);
-        const breakoutId = Session.get('lastBreakoutOpened');
-        if (breakoutRoom.breakoutId !== breakoutId) {
-          this.inviteUserToBreakout(breakoutRoom);
+    if (hasBreakouts && !breakoutUserIsIn) {
+      // Have to check for freeJoin breakouts first because currentBreakoutUser will
+      // populate after a room has been joined
+      const freeJoinBreakout = breakouts.find(breakout => breakout.freeJoin);
+      if (freeJoinBreakout) {
+        if (!didSendBreakoutInvite) {
+          this.inviteUserToBreakout(freeJoinBreakout);
+          this.setState({ didSendBreakoutInvite: true });
+        }
+      } else if (currentBreakoutUser) {
+        const currentInsertedTime = currentBreakoutUser.insertedTime;
+        const oldCurrentUser = oldProps.currentBreakoutUser || {};
+        const oldInsertedTime = oldCurrentUser.insertedTime;
+        if (currentInsertedTime !== oldInsertedTime) {
+          const breakoutRoom = getBreakoutByUser(currentBreakoutUser);
+          const breakoutId = Session.get('lastBreakoutOpened');
+          if (breakoutRoom.breakoutId !== breakoutId) {
+            this.inviteUserToBreakout(breakoutRoom);
+          }
         }
       }
     }
+
+    if (!hasBreakouts && didSendBreakoutInvite) {
+      this.setState({ didSendBreakoutInvite: false });
+    }
   }
 
   inviteUserToBreakout(breakout) {
     const {
       mountModal,
     } = this.props;
-    openBreakoutJoinConfirmation.call(this, breakout, breakout.name, mountModal);
+    // There's a race condition on page load with modals. Only one modal can be shown at a
+    // time and new ones overwrite old ones. We delay the opening of the breakout modal
+    // because it should always be on top if breakouts are running.
+    setTimeout(() => {
+      openBreakoutJoinConfirmation.call(this, breakout, breakout.name, mountModal);
+    }, BREAKOUT_MODAL_DELAY);
   }
 
   render() {
@@ -54,4 +117,7 @@ class BreakoutRoomInvitation extends Component {
   }
 }
 
+BreakoutRoomInvitation.propTypes = propTypes;
+BreakoutRoomInvitation.defaultProps = defaultProps;
+
 export default withModalMounter(BreakoutRoomInvitation);
diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/container.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/container.jsx
index 5790bf96636af4b5303edaab673bf22f81e74ef3..5409394dbd142b47c81a930145d2a7130dff9e48 100644
--- a/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/breakout-room/invitation/container.jsx
@@ -3,14 +3,19 @@ import { withTracker } from 'meteor/react-meteor-data';
 import BreakoutRoomInvitation from './component';
 import BreakoutService from '../service';
 import Auth from '/imports/ui/services/auth';
+import AppService from '/imports/ui/components/app/service';
 
-const BreakoutRoomInvitationContainer = props => (
-  <BreakoutRoomInvitation {...props} />
-);
+const BreakoutRoomInvitationContainer = ({ isMeetingBreakout, ...props }) => {
+  if (isMeetingBreakout) return null;
+  return (
+    <BreakoutRoomInvitation {...props} />
+  );
+};
 
 export default withTracker(() => ({
-  breakouts: BreakoutService.getBreakouts(),
+  isMeetingBreakout: AppService.meetingIsBreakout(),
+  breakouts: BreakoutService.getBreakoutsNoTime(),
   getBreakoutByUser: BreakoutService.getBreakoutByUser,
   currentBreakoutUser: BreakoutService.getBreakoutUserByUserId(Auth.userID),
-  getBreakoutByUserId: BreakoutService.getBreakoutByUserId,
+  breakoutUserIsIn: BreakoutService.getBreakoutUserIsIn(Auth.userID),
 }))(BreakoutRoomInvitationContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/service.js b/bigbluebutton-html5/imports/ui/components/breakout-room/service.js
index d2b04f6da22dfa8c2dcb5a6299015983a047861a..28d549d134b134eb3dd261ec8bf1c0b26746dfa1 100644
--- a/bigbluebutton-html5/imports/ui/components/breakout-room/service.js
+++ b/bigbluebutton-html5/imports/ui/components/breakout-room/service.js
@@ -60,12 +60,10 @@ const amIModerator = () => {
   return User.role === ROLE_MODERATOR;
 };
 
-const getNumUsersByBreakoutId = breakoutId => Users.find({
-  meetingId: breakoutId,
-  connectionStatus: 'online',
-}, { fields: {} }).count();
-
-const getBreakoutByUserId = userId => Breakouts.find({ 'users.userId': userId }).fetch();
+const getBreakoutByUserId = userId => Breakouts.find(
+  { 'users.userId': userId },
+  { fields: { timeRemaining: 0 } },
+).fetch();
 
 const getBreakoutByUser = user => Breakouts.findOne({ users: user });
 
@@ -87,8 +85,21 @@ const getBreakoutUserByUserId = userId => fp.pipe(
 )(userId);
 
 const getBreakouts = () => Breakouts.find({}, { sort: { sequence: 1 } }).fetch();
+const getBreakoutsNoTime = () => Breakouts.find(
+  {},
+  {
+    sort: { sequence: 1 },
+    fields: { timeRemaining: 0 },
+  },
+).fetch();
+
+const getBreakoutUserIsIn = userId => Breakouts.findOne({ 'joinedUsers.userId': new RegExp(`^${userId}`) }, { fields: { sequence: 1 } });
 
-const isUserInBreakoutRoom = breakoutId => Breakouts.find({ breakoutId, 'joinedUsers.userId': new RegExp(`^${Auth.userID}`) }).count();
+const isUserInBreakoutRoom = (joinedUsers) => {
+  const userId = Auth.userID;
+
+  return !!joinedUsers.find(user => user.userId.startsWith(userId));
+};
 
 export default {
   findBreakouts,
@@ -100,10 +111,11 @@ export default {
   meetingId: () => Auth.meetingID,
   closeBreakoutPanel,
   amIModerator,
-  getNumUsersByBreakoutId,
   getBreakoutUserByUserId,
   getBreakoutByUser,
   getBreakouts,
+  getBreakoutsNoTime,
   getBreakoutByUserId,
+  getBreakoutUserIsIn,
   isUserInBreakoutRoom,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx
index 076b78d9e7e48fde3aa60c8f4910c8a2a01f71c7..c5b781f538245824b2e208f636fb4ba900b763bc 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/container.jsx
@@ -1,6 +1,6 @@
 import React from 'react';
 import { withTracker } from 'meteor/react-meteor-data';
-import Breakouts from '/imports/api/breakouts';
+import BreakoutService from '/imports/ui/components/breakout-room/service';
 import Meetings from '/imports/api/meetings';
 import Auth from '/imports/ui/services/auth';
 import UserListItem from './component';
@@ -10,7 +10,7 @@ const UserListItemContainer = props => <UserListItem {...props} />;
 const isMe = intId => intId === Auth.userID;
 
 export default withTracker(({ user }) => {
-  const findUserInBreakout = Breakouts.findOne({ 'joinedUsers.userId': new RegExp(`^${user.userId}`) });
+  const findUserInBreakout = BreakoutService.getBreakoutUserIsIn(user.userId);
   const breakoutSequence = (findUserInBreakout || {}).sequence;
   const Meeting = Meetings.findOne({ meetingId: Auth.meetingID },
     { fields: { lockSettingsProps: 1 } });