diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles
index d9177bebb5..ea66720529 100644
--- a/.eslintignore.errorfiles
+++ b/.eslintignore.errorfiles
@@ -4,7 +4,6 @@ src/Markdown.js
 src/NodeAnimator.js
 src/components/structures/RoomDirectory.js
 src/components/views/rooms/MemberList.js
-src/ratelimitedfunc.js
 src/utils/DMRoomMap.js
 src/utils/MultiInviter.js
 test/components/structures/MessagePanel-test.js
diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx
index c608f0eee9..338bfc06ab 100644
--- a/src/components/structures/RightPanel.tsx
+++ b/src/components/structures/RightPanel.tsx
@@ -23,7 +23,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 
 import dis from '../../dispatcher/dispatcher';
-import RateLimitedFunc from '../../ratelimitedfunc';
 import GroupStore from '../../stores/GroupStore';
 import {
     RIGHT_PANEL_PHASES_NO_ARGS,
@@ -48,6 +47,7 @@ import FilePanel from "./FilePanel";
 import NotificationPanel from "./NotificationPanel";
 import ResizeNotifier from "../../utils/ResizeNotifier";
 import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
+import { DebouncedFunc, throttle } from 'lodash';
 
 interface IProps {
     room?: Room; // if showing panels for a given room, this is set
@@ -73,7 +73,7 @@ interface IState {
 export default class RightPanel extends React.Component<IProps, IState> {
     static contextType = MatrixClientContext;
 
-    private readonly delayedUpdate: RateLimitedFunc;
+    private readonly delayedUpdate: DebouncedFunc<() => void>;
     private dispatcherRef: string;
 
     constructor(props, context) {
@@ -85,9 +85,9 @@ export default class RightPanel extends React.Component<IProps, IState> {
             member: this.getUserForPanel(),
         };
 
-        this.delayedUpdate = new RateLimitedFunc(() => {
+        this.delayedUpdate = throttle(() => {
             this.forceUpdate();
-        }, 500);
+        }, 500, { leading: true, trailing: true });
     }
 
     // Helper function to split out the logic for getPhaseFromProps() and the constructor
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 81000a87a6..d08eaa2ecd 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -37,7 +37,6 @@ import Modal from '../../Modal';
 import * as sdk from '../../index';
 import CallHandler, { PlaceCallType } from '../../CallHandler';
 import dis from '../../dispatcher/dispatcher';
-import rateLimitedFunc from '../../ratelimitedfunc';
 import * as Rooms from '../../Rooms';
 import eventSearch, { searchPagination } from '../../Searching';
 import MainSplit from './MainSplit';
@@ -82,6 +81,7 @@ import { IOpts } from "../../createRoom";
 import { replaceableComponent } from "../../utils/replaceableComponent";
 import UIStore from "../../stores/UIStore";
 import EditorStateTransfer from "../../utils/EditorStateTransfer";
+import { throttle } from "lodash";
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -675,8 +675,8 @@ export default class RoomView extends React.Component<IProps, IState> {
             );
         }
 
-        // cancel any pending calls to the rate_limited_funcs
-        this.updateRoomMembers.cancelPendingCall();
+        // cancel any pending calls to the throttled updated
+        this.updateRoomMembers.cancel();
 
         for (const watcher of this.settingWatchers) {
             SettingsStore.unwatchSetting(watcher);
@@ -1092,7 +1092,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             return;
         }
 
-        this.updateRoomMembers(member);
+        this.updateRoomMembers();
     };
 
     private onMyMembership = (room: Room, membership: string, oldMembership: string) => {
@@ -1114,10 +1114,10 @@ export default class RoomView extends React.Component<IProps, IState> {
     }
 
     // rate limited because a power level change will emit an event for every member in the room.
-    private updateRoomMembers = rateLimitedFunc(() => {
+    private updateRoomMembers = throttle(() => {
         this.updateDMState();
         this.updateE2EStatus(this.state.room);
-    }, 500);
+    }, 500, { leading: true, trailing: true });
 
     private checkDesktopNotifications() {
         const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 1c817140fa..99286dfa07 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -21,7 +21,6 @@ import { Room } from 'matrix-js-sdk/src/models/room';
 
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import AppsDrawer from './AppsDrawer';
-import RateLimitedFunc from '../../../ratelimitedfunc';
 import SettingsStore from "../../../settings/SettingsStore";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 import { UIFeature } from "../../../settings/UIFeature";
@@ -29,6 +28,7 @@ import ResizeNotifier from "../../../utils/ResizeNotifier";
 import CallViewForRoom from '../voip/CallViewForRoom';
 import { objectHasDiff } from "../../../utils/objects";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { throttle } from 'lodash';
 
 interface IProps {
     // js-sdk room object
@@ -99,9 +99,9 @@ export default class AuxPanel extends React.Component<IProps, IState> {
         }
     }
 
-    private rateLimitedUpdate = new RateLimitedFunc(() => {
+    private rateLimitedUpdate = throttle(() => {
         this.setState({ counters: this.computeCounters() });
-    }, 500);
+    }, 500, { leading: true, trailing: true });
 
     private computeCounters() {
         const counters = [];
diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx
index 68f87580df..f4df70c7ee 100644
--- a/src/components/views/rooms/MemberList.tsx
+++ b/src/components/views/rooms/MemberList.tsx
@@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler';
 import SdkConfig from '../../../SdkConfig';
 import dis from '../../../dispatcher/dispatcher';
 import { isValid3pidInvite } from "../../../RoomInvite";
-import rateLimitedFunction from "../../../ratelimitedfunc";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
 import BaseCard from "../right_panel/BaseCard";
@@ -43,6 +42,7 @@ import AccessibleButton from '../elements/AccessibleButton';
 import EntityTile from "./EntityTile";
 import MemberTile from "./MemberTile";
 import BaseAvatar from '../avatars/BaseAvatar';
+import { throttle } from 'lodash';
 
 const INITIAL_LOAD_NUM_MEMBERS = 30;
 const INITIAL_LOAD_NUM_INVITED = 5;
@@ -133,7 +133,7 @@ export default class MemberList extends React.Component<IProps, IState> {
         }
 
         // cancel any pending calls to the rate_limited_funcs
-        this.updateList.cancelPendingCall();
+        this.updateList.cancel();
     }
 
     /**
@@ -237,9 +237,9 @@ export default class MemberList extends React.Component<IProps, IState> {
         if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
     };
 
-    private updateList = rateLimitedFunction(() => {
+    private updateList = throttle(() => {
         this.updateListNow();
-    }, 500);
+    }, 500, { leading: true, trailing: true });
 
     private updateListNow(): void {
         const members = this.roomMembers();
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 886317f2bf..b05b709e36 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import { _t } from '../../../languageHandler';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
-import RateLimitedFunc from '../../../ratelimitedfunc';
 
 import SettingsStore from "../../../settings/SettingsStore";
 import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
@@ -31,6 +30,7 @@ import RoomTopic from "../elements/RoomTopic";
 import RoomName from "../elements/RoomName";
 import { PlaceCallType } from "../../../CallHandler";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { throttle } from 'lodash';
 
 @replaceableComponent("views.rooms.RoomHeader")
 export default class RoomHeader extends React.Component {
@@ -73,10 +73,9 @@ export default class RoomHeader extends React.Component {
         this._rateLimitedUpdate();
     };
 
-    _rateLimitedUpdate = new RateLimitedFunc(function() {
-        /* eslint-disable @babel/no-invalid-this */
+    _rateLimitedUpdate = throttle(() => {
         this.forceUpdate();
-    }, 500);
+    }, 500, { leading: true, trailing: true });
 
     render() {
         let searchStatus = null;
diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx
index f088ec0c8e..ec190c829a 100644
--- a/src/components/views/rooms/SendMessageComposer.tsx
+++ b/src/components/views/rooms/SendMessageComposer.tsx
@@ -39,7 +39,6 @@ import Modal from '../../../Modal';
 import { _t, _td } from '../../../languageHandler';
 import ContentMessages from '../../../ContentMessages';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import RateLimitedFunc from '../../../ratelimitedfunc';
 import { Action } from "../../../dispatcher/actions";
 import { containsEmoji } from "../../../effects/utils";
 import { CHAT_EFFECTS } from '../../../effects';
@@ -53,6 +52,7 @@ import { Room } from 'matrix-js-sdk/src/models/room';
 import ErrorDialog from "../dialogs/ErrorDialog";
 import QuestionDialog from "../dialogs/QuestionDialog";
 import { ActionPayload } from "../../../dispatcher/payloads";
+import { DebouncedFunc, throttle } from 'lodash';
 
 function addReplyToMessageContent(
     content: IContent,
@@ -138,7 +138,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
     static contextType = MatrixClientContext;
     context!: React.ContextType<typeof MatrixClientContext>;
 
-    private readonly prepareToEncrypt?: RateLimitedFunc;
+    private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
     private readonly editorRef = createRef<BasicMessageComposer>();
     private model: EditorModel = null;
     private currentlyComposedEditorState: SerializedPart[] = null;
@@ -149,9 +149,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
         super(props);
         this.context = context; // otherwise React will only set it prior to render due to type def above
         if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
-            this.prepareToEncrypt = new RateLimitedFunc(() => {
+            this.prepareToEncrypt = throttle(() => {
                 this.context.prepareToEncrypt(this.props.room);
-            }, 60000);
+            }, 60000, { leading: true, trailing: false });
         }
 
         window.addEventListener("beforeunload", this.saveStoredEditorState);
diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js
deleted file mode 100644
index 3df3db615e..0000000000
--- a/src/ratelimitedfunc.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/**
- * 'debounces' a function to only execute every n milliseconds.
- * Useful when react-sdk gets many, many events but only wants
- * to update the interface once for all of them.
- *
- * Note that the function must not take arguments, since the args
- * could be different for each invocation of the function.
- *
- * The returned function has a 'cancelPendingCall' property which can be called
- * on unmount or similar to cancel any pending update.
- */
-
-import {throttle} from "lodash";
-
-export default function ratelimitedfunc(fn, time) {
-    const throttledFn = throttle(fn, time, {
-        leading: true,
-        trailing: true,
-    });
-    const _bind = throttledFn.bind;
-    throttledFn.bind = function() {
-        const boundFn = _bind.apply(throttledFn, arguments);
-        boundFn.cancelPendingCall = throttledFn.cancelPendingCall;
-        return boundFn;
-    };
-
-    throttledFn.cancelPendingCall = function() {
-        throttledFn.cancel();
-    };
-    return throttledFn;
-}