Repository: ray-project/ray
Issue category: Performance Issue

[Bug] Dashboard takes up a lot of Memory
### Search before asking

- [X] I searched the [issues](https://github.com/ray-project/ray/issues) and found no similar issues.


### Ray Component

Dashboard

### Issue Severity

Medium: It contributes to significant difficulty to complete my task but I work arounds and get it resolved.

### What happened + What you expected to happen

We are running Ray cluster on Kubernetes.
Ray dashboard takes up 21Gb memory causing memory overload on Ray head.

![image](https://user-images.githubusercontent.com/54840261/161507028-32e264ea-401a-4313-aa5c-88b9cc443f2a.png)
![image](https://user-images.githubusercontent.com/54840261/161507052-717bb464-2651-4867-8ba5-eb3a075867d1.png)

```
root&#64;ray-ray-head-type-qgg87:/data# pmap -x 3240459
3240459:   /usr/local/bin/python -u /usr/local/lib/python3.7/site-packages/ray/dashboard/dashboard.py --host=0.0.0.0 --port=8265 --port-retries=0 --redis-address=192.168.4.7:6379 --temp-dir=/tmp/ray --log-dir=/tmp/ray/session_2022-03-08_08-31-04_272087_3240181/logs --logging-rotate-bytes=536870912 --logging-rotate-backup-count=5 --redis-password 5241590000000000
Address           Kbytes     RSS   Dirty Mode  Mapping
000055ed1bf63000       4       4       0 r---- python3.7
000055ed1bf64000       4       4       0 r-x-- python3.7
000055ed1bf65000       4       0       0 r---- python3.7
000055ed1bf66000       4       4       4 r---- python3.7
000055ed1bf67000       4       4       4 rw--- python3.7
000055ed1dc9f000 16949244 16949028 16949028 rw---   [ anon ]
00007fbbde5a9000     256     256     256 rw---   [ anon ]
00007fbbde6a9000     256     256     256 rw---   [ anon ]
00007fbbdf237000   43532   43532   43532 rw---   [ anon ]
00007fbbe1cde000     768     768     768 rw---   [ anon ]
00007fbbe1dde000   18944   18944   18944 rw---   [ anon ]
00007fbbe3080000   81408   81408   81408 rw---   [ anon ]
00007fbbe8000000     132      16      16 rw---   [ anon ]
00007fbbe8021000   65404       0       0 -----   [ anon ]
00007fbbec002000   15872   15872   15872 rw---   [ anon ]
00007fbbecf9e000   14848   14848   14848 rw---   [ anon ]
00007fbbede2a000   33024   33024   33024 rw---   [ anon ]
00007fbbefe80000    1536    1536    1536 rw---   [ anon ]
00007fbbf0000000     132      16      16 rw---   [ anon ]
00007fbbf0021000   65404       0       0 -----   [ anon ]
00007fbbf4000000     132      28      28 rw---   [ anon ]
00007fbbf4021000   65404       0       0 -----   [ anon ]
00007fbbf8000000     132      20      20 rw---   [ anon ]
00007fbbf8021000   65404       0       0 -----   [ anon ]
00007fbbfc000000     132      16      16 rw---   [ anon ]
00007fbbfc021000   65404       0       0 -----   [ anon ]
00007fbc00000000     132      20      20 rw---   [ anon ]
00007fbc00021000   65404       0       0 -----   [ anon ]
00007fbc04021000     768     768     768 rw---   [ anon ]
00007fbc040f2000    9984    9984    9984 rw---   [ anon ]
00007fbc04aba000    4864    4864    4864 rw---   [ anon ]
00007fbc04f7a000     512     512     512 rw---   [ anon ]
00007fbc04ffa000       4       0       0 -----   [ anon ]
00007fbc04ffb000    8192      16      16 rw---   [ anon ]
00007fbc057fb000       4       0       0 -----   [ anon ]
00007fbc057fc000    8192      16      16 rw---   [ anon ]
00007fbc05ffc000       4       0       0 -----   [ anon ]
00007fbc05ffd000    8192      16      16 rw---   [ anon ]
00007fbc067fd000       4       0       0 -----   [ anon ]
00007fbc067fe000    8192      16      16 rw---   [ anon ]
00007fbc06ffe000       4       0       0 -----   [ anon ]
00007fbc06fff000    8192      16      16 rw---   [ anon ]
00007fbc077ff000       4       0       0 -----   [ anon ]
00007fbc07800000    8192      16      16 rw---   [ anon ]
00007fbc08000000     132      16      16 rw---   [ anon ]
00007fbc08021000   65404       0       0 -----   [ anon ]
00007fbc0c000000     132      20      20 rw---   [ anon ]
00007fbc0c021000   65404       0       0 -----   [ anon ]
00007fbc10000000     132      16      16 rw---   [ anon ]
00007fbc10021000   65404       0       0 -----   [ anon ]
00007fbc14000000     132      24      24 rw---   [ anon ]
00007fbc14021000   65404       0       0 -----   [ anon ]
00007fbc18000000     132      16      16 rw---   [ anon ]
00007fbc18021000   65404       0       0 -----   [ anon ]
00007fbc1c000000     132      20      20 rw---   [ anon ]
00007fbc1c021000   65404       0       0 -----   [ anon ]
00007fbc20000000     132      20      20 rw---   [ anon ]
00007fbc20021000   65404       0       0 -----   [ anon ]
00007fbc24036000     768     768     768 rw---   [ anon ]
00007fbc240f9000    7168    7168    7168 rw---   [ anon ]
00007fbc247f9000       4       0       0 -----   [ anon ]
00007fbc247fa000    8192      16      16 rw---   [ anon ]
00007fbc24ffa000       4       0       0 -----   [ anon ]
00007fbc24ffb000    8192      16      16 rw---   [ anon ]
00007fbc257fb000       4       0       0 -----   [ anon ]
00007fbc257fc000    8192      16      16 rw---   [ anon ]
00007fbc25ffc000       4       0       0 -----   [ anon ]
00007fbc25ffd000    8192      16      16 rw---   [ anon ]
00007fbc267fd000       4       0       0 -----   [ anon ]
00007fbc267fe000    8192      16      16 rw---   [ anon ]
00007fbc26ffe000       4       0       0 -----   [ anon ]
00007fbc26fff000    8192      16      16 rw---   [ anon ]
00007fbc277ff000       4       0       0 -----   [ anon ]
00007fbc27800000    8192      16      16 rw---   [ anon ]
00007fbc28000000     132      20      20 rw---   [ anon ]
00007fbc28021000   65404       0       0 -----   [ anon ]
00007fbc2c000000     132      24      24 rw---   [ anon ]
00007fbc2c021000   65404       0       0 -----   [ anon ]
00007fbc30000000     132      16      16 rw---   [ anon ]
00007fbc30021000   65404       0       0 -----   [ anon ]
00007fbc34000000     132      20      20 rw---   [ anon ]
00007fbc34021000   65404       0       0 -----   [ anon ]
00007fbc38000000     132      16      16 rw---   [ anon ]
00007fbc38021000   65404       0       0 -----   [ anon ]
00007fbc3c000000     132      16      16 rw---   [ anon ]
00007fbc3c021000   65404       0       0 -----   [ anon ]
00007fbc40000000     132      16      16 rw---   [ anon ]
00007fbc40021000   65404       0       0 -----   [ anon ]
00007fbc44039000    7936    7936    7936 rw---   [ anon ]
00007fbc447f9000       4       0       0 -----   [ anon ]
00007fbc447fa000    8192      16      16 rw---   [ anon ]
00007fbc44ffa000       4       0       0 -----   [ anon ]
00007fbc44ffb000    8192      16      16 rw---   [ anon ]
00007fbc457fb000       4       0       0 -----   [ anon ]
00007fbc457fc000    8192      16      16 rw---   [ anon ]
00007fbc45ffc000       4       0       0 -----   [ anon ]
00007fbc45ffd000    8192      16      16 rw---   [ anon ]
00007fbc467fd000       4       0       0 -----   [ anon ]
00007fbc467fe000    8192      16      16 rw---   [ anon ]
00007fbc46ffe000       4       0       0 -----   [ anon ]
00007fbc46fff000    8192      16      16 rw---   [ anon ]
00007fbc477ff000       4       0       0 -----   [ anon ]
00007fbc47800000    8192      16      16 rw---   [ anon ]
00007fbc48000000     132      28      28 rw---   [ anon ]
00007fbc48021000   65404       0       0 -----   [ anon ]
00007fbc4c000000     132      16      16 rw---   [ anon ]
00007fbc4c021000   65404       0       0 -----   [ anon ]
00007fbc50000000     132      16      16 rw---   [ anon ]
00007fbc50021000   65404       0       0 -----   [ anon ]
00007fbc54000000     132      20      20 rw---   [ anon ]
00007fbc54021000   65404       0       0 -----   [ anon ]
00007fbc58000000     132      16      16 rw---   [ anon ]
00007fbc58021000   65404       0       0 -----   [ anon ]
00007fbc5c000000     132      16      16 rw---   [ anon ]
00007fbc5c021000   65404       0       0 -----   [ anon ]
00007fbc60000000     132      16      16 rw---   [ anon ]
00007fbc60021000   65404       0       0 -----   [ anon ]
00007fbc64039000    7936    7936    7936 rw---   [ anon ]
00007fbc647f9000       4       0       0 -----   [ anon ]
00007fbc647fa000    8192      16      16 rw---   [ anon ]
00007fbc64ffa000       4       0       0 -----   [ anon ]
00007fbc64ffb000    8192      16      16 rw---   [ anon ]
00007fbc657fb000       4       0       0 -----   [ anon ]
00007fbc657fc000    8192      16      16 rw---   [ anon ]
00007fbc65ffc000       4       0       0 -----   [ anon ]
00007fbc65ffd000    8192      16      16 rw---   [ anon ]
00007fbc667fd000       4       0       0 -----   [ anon ]
00007fbc667fe000    8192      16      16 rw---   [ anon ]
00007fbc66ffe000       4       0       0 -----   [ anon ]
00007fbc66fff000    8192      16      16 rw---   [ anon ]
00007fbc677ff000       4       0       0 -----   [ anon ]
00007fbc67800000    8192      16      16 rw---   [ anon ]
00007fbc68000000     132      16      16 rw---   [ anon ]
00007fbc68021000   65404       0       0 -----   [ anon ]
00007fbc6c000000     132      16      16 rw---   [ anon ]
00007fbc6c021000   65404       0       0 -----   [ anon ]
00007fbc70000000     132      16      16 rw---   [ anon ]
00007fbc70021000   65404       0       0 -----   [ anon ]
00007fbc74000000     132      20      20 rw---   [ anon ]
00007fbc74021000   65404       0       0 -----   [ anon ]
00007fbc78000000     132      16      16 rw---   [ anon ]
00007fbc78021000   65404       0       0 -----   [ anon ]
00007fbc7c000000     132      16      16 rw---   [ anon ]
00007fbc7c021000   65404       0       0 -----   [ anon ]
00007fbc80000000     132      20      20 rw---   [ anon ]
00007fbc80021000   65404       0       0 -----   [ anon ]
00007fbc84039000    7936    7936    7936 rw---   [ anon ]
00007fbc847f9000       4       0       0 -----   [ anon ]
00007fbc847fa000    8192      16      16 rw---   [ anon ]
00007fbc84ffa000       4       0       0 -----   [ anon ]
00007fbc84ffb000    8192      16      16 rw---   [ anon ]
00007fbc857fb000       4       0       0 -----   [ anon ]
00007fbc857fc000    8192      16      16 rw---   [ anon ]
00007fbc85ffc000       4       0       0 -----   [ anon ]
00007fbc85ffd000    8192      16      16 rw---   [ anon ]
00007fbc867fd000       4       0       0 -----   [ anon ]
00007fbc867fe000    8192      16      16 rw---   [ anon ]
00007fbc86ffe000       4       0       0 -----   [ anon ]
00007fbc86fff000    8192      16      16 rw---   [ anon ]
00007fbc877ff000       4       0       0 -----   [ anon ]
00007fbc87800000    8192      16      16 rw---   [ anon ]
00007fbc88000000     132      16      16 rw---   [ anon ]
00007fbc88021000   65404       0       0 -----   [ anon ]
00007fbc8c000000     132      16      16 rw---   [ anon ]
00007fbc8c021000   65404       0       0 -----   [ anon ]
00007fbc90000000     132      36      36 rw---   [ anon ]
00007fbc90021000   65404       0       0 -----   [ anon ]
00007fbc94000000     132      20      20 rw---   [ anon ]
00007fbc94021000   65404       0       0 -----   [ anon ]
00007fbc98000000     296     296     296 rw---   [ anon ]
00007fbc9804a000   65240       0       0 -----   [ anon ]
00007fbc9c000000     132      16      16 rw---   [ anon ]
00007fbc9c021000   65404       0       0 -----   [ anon ]
00007fbca0000000     132      16      16 rw---   [ anon ]
00007fbca0021000   65404       0       0 -----   [ anon ]
00007fbca400f000    1024    1024    1024 rw---   [ anon ]
00007fbca410f000       4       0       0 -----   [ anon ]
00007fbca4110000    8192      16      16 rw---   [ anon ]
00007fbca4910000       4       0       0 -----   [ anon ]
00007fbca4911000    8192      16      16 rw---   [ anon ]
00007fbca5111000       4       0       0 -----   [ anon ]
00007fbca5112000    8448     272     272 rw---   [ anon ]
00007fbca5952000       4       0       0 -----   [ anon ]
00007fbca5953000    8192      16      16 rw---   [ anon ]
00007fbca6153000       4       0       0 -----   [ anon ]
00007fbca6154000    8192      16      16 rw---   [ anon ]
00007fbca6954000       4       0       0 -----   [ anon ]
00007fbca6955000    8448     276     276 rw---   [ anon ]
00007fbca7195000       4       0       0 -----   [ anon ]
00007fbca7196000    9984    1808    1808 rw---   [ anon ]
00007fbca7b56000      76      76       0 r-x-- testing.cpython-37m-x86_64-linux-gnu.so
00007fbca7b69000    2044       0       0 ----- testing.cpython-37m-x86_64-linux-gnu.so
00007fbca7d68000      12      12      12 rw--- testing.cpython-37m-x86_64-linux-gnu.so
00007fbca7d6b000     512     512     512 rw---   [ anon ]
00007fbca7deb000      80      80       0 r-x-- json.cpython-37m-x86_64-linux-gnu.so
00007fbca7dff000    2048       0       0 ----- json.cpython-37m-x86_64-linux-gnu.so
00007fbca7fff000       4       4       4 rw--- json.cpython-37m-x86_64-linux-gnu.so
00007fbca8000000     132      12      12 rw---   [ anon ]
00007fbca8021000   65404       0       0 -----   [ anon ]
00007fbcac006000    1280    1280    1280 rw---   [ anon ]
00007fbcac146000     488     424       0 r-x-- parsers.cpython-37m-x86_64-linux-gnu.so
00007fbcac1c0000    2048       0       0 ----- parsers.cpython-37m-x86_64-linux-gnu.so
00007fbcac3c0000      28      28      28 rw--- parsers.cpython-37m-x86_64-linux-gnu.so
00007fbcac3c7000     780     780     780 rw---   [ anon ]
00007fbcac48a000     276     148       0 r-x-- reduction.cpython-37m-x86_64-linux-gnu.so
00007fbcac4cf000    2048       0       0 ----- reduction.cpython-37m-x86_64-linux-gnu.so
00007fbcac6cf000      20      20      20 rw--- reduction.cpython...
```

### Versions / Dependencies

Ray v1.9.1
Kubernetes: v1.21

### Reproduction script

It happens when running ray for a long time.

### Anything else

_No response_

### Are you willing to submit a PR?

- [ ] Yes I am willing to submit a PR!
[Bug] Dashboard memory leak
### Search before asking

- [X] I searched the [issues](https://github.com/ray-project/ray/issues) and found no similar issues.


### Ray Component

Dashboard

### What happened + What you expected to happen

Ray dashboard keeps the whole history of actors and never delete the old ones. So as time going, it'll OOM.

This issue is a follow up for this one https://github.com/ray-project/ray/issues/21432

many drivers test eventually OOM because we are running faster than before and as dashboard memory keeps increasing, it eventually OOM.

### Versions / Dependencies

master

### Reproduction script

many driver nightly tests

### Anything else

_No response_

### Are you willing to submit a PR?

- [ ] Yes I am willing to submit a PR!
[Dashboard] Too many logs causes API requests (e.g. `job submit`, `job stop`) to fail
### What happened + What you expected to happen

Start Ray and run the following script on the head node to produce a lot of logs:

```python
import ray

ray.init(address="auto")

@ray.remote
def f():
    print("Print from task")

while True:
    print("Print from driver")
    ray.get(f.remote())
```



This causes Dashboard API requests to fail.  For example, submitting a job with Ray Job Submission:

```
❯ ray job submit -- sleep 30s
[...]
Traceback (most recent call last):
  File "/Users/archit/anaconda3/envs/test39/bin/ray", line 8, in <module>
    sys.exit(main())
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/ray/scripts/scripts.py", line 2269, in main
    return cli()
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/ray/autoscaler/_private/cli_logger.py", line 808, in wrapper
    return f(*args, **kwargs)
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/ray/dashboard/modules/job/cli.py", line 152, in submit
    job_id = client.submit_job(
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/ray/dashboard/modules/job/sdk.py", line 142, in submit_job
    self._raise_error(r)
  File "/Users/archit/anaconda3/envs/test39/lib/python3.9/site-packages/ray/dashboard/modules/dashboard_sdk.py", line 217, in _raise_error
    raise RuntimeError(
RuntimeError: Request failed with status code 504: upstream request timeout.
```


### Versions / Dependencies

Verified on Ray 1.10 Linux and Ray 1.12 Linux, Python 3.7 and 3.9

### Reproduction script

Given above.  Here is the py-spy trace indicating the problematic part of the code:

This overloads the dashboard, giving a py-spy trace like the following: 

<img width="903" alt="Screen Shot 2022-04-27 at 12 56 08 PM" src="https://user-images.githubusercontent.com/5459654/165620600-ccc08646-b888-49d9-854e-e866912aa6f3.png">

### Issue Severity

Medium: It is a significant difficulty but I can work around it.


Functions to edit:
dashboard/datacenter.py:DataSource
dashboard/datacenter.py:DataOrganizer.get_node_workers
dashboard/datacenter.py:DataOrganizer.get_node_info
dashboard/modules/actor/actor_head.py:ActorHead.__init__
dashboard/modules/actor/actor_head.py:ActorHead._update_actors
dashboard/modules/actor/actor_head.py:ActorHead.run
dashboard/modules/node/node_head.py:NodeHead.get_logs
dashboard/modules/node/node_head.py:NodeHead
dashboard/modules/node/node_head.py:NodeHead._update_log_info
dashboard/modules/node/node_head.py:NodeHead._update_error_info
Functions added: dashboard/modules/actor/actor_head.py:ActorHead._cleanup_actors

Patch:
diff --git a/dashboard/client/src/api.ts b/dashboard/client/src/api.ts
index 42467df8e7e2d..e532d0829b98b 100644
--- a/dashboard/client/src/api.ts
+++ b/dashboard/client/src/api.ts
@@ -1,4 +1,4 @@
-import { formatUrl } from "./service/requestHandlers";
+import { formatUrl, get as getV2 } from "./service/requestHandlers";
 
 type APIResponse<T> = {
   result: boolean;
@@ -294,19 +294,18 @@ export const getErrors = (nodeIp: string, pid: number | null) =>
     pid: pid ?? "",
   });
 
-export type LogsResponse = {
-  logs: LogsByPid;
-};
-
-export type LogsByPid = {
-  [pid: string]: string[];
-};
-
-export const getLogs = (nodeIp: string, pid: number | null) =>
-  get<LogsResponse>("/node_logs", {
-    ip: nodeIp,
-    pid: pid ?? "",
+export const getLogs = async (nodeIp: string, pid: number) => {
+  const result = await getV2<string>("/api/v0/logs/file", {
+    params: {
+      node_ip: nodeIp,
+      pid: pid,
+      lines: 15000,
+    },
   });
+  // Substring to get rid of initial "1" or "0" that represents successful stream.
+  // TODO(aguo): should we get rid of that?
+  return result.data.substring(1).split("\n");
+};
 
 export type LaunchProfilingResponse = string;
 
diff --git a/dashboard/client/src/pages/dashboard/node-info/NodeInfo.tsx b/dashboard/client/src/pages/dashboard/node-info/NodeInfo.tsx
index fd6eead1016fa..b41ca94e5019d 100644
--- a/dashboard/client/src/pages/dashboard/node-info/NodeInfo.tsx
+++ b/dashboard/client/src/pages/dashboard/node-info/NodeInfo.tsx
@@ -147,6 +147,11 @@ const liveNodesSelector = (state: StoreState) =>
   );
 
 type DialogState = {
+  nodeIp: string;
+  pid: number;
+} | null;
+
+type ErrorDialogState = {
   nodeIp: string;
   pid: number | null;
 } | null;
@@ -169,7 +174,7 @@ const nodeInfoHeaders: HeaderInfo<nodeInfoColumnId>[] = [
 
 const NodeInfo: React.FC<{}> = () => {
   const [logDialog, setLogDialog] = useState<DialogState>(null);
-  const [errorDialog, setErrorDialog] = useState<DialogState>(null);
+  const [errorDialog, setErrorDialog] = useState<ErrorDialogState>(null);
   const [isGrouped, setIsGrouped] = useState(true);
   const [order, setOrder] = React.useState<Order>("asc");
   const toggleOrder = () => setOrder(order === "asc" ? "desc" : "asc");
diff --git a/dashboard/client/src/pages/dashboard/node-info/dialogs/logs/Logs.tsx b/dashboard/client/src/pages/dashboard/node-info/dialogs/logs/Logs.tsx
index 4651be36d7308..a6def83781dfa 100644
--- a/dashboard/client/src/pages/dashboard/node-info/dialogs/logs/Logs.tsx
+++ b/dashboard/client/src/pages/dashboard/node-info/dialogs/logs/Logs.tsx
@@ -7,7 +7,7 @@ import {
   withStyles,
 } from "@material-ui/core";
 import React from "react";
-import { getLogs, LogsByPid } from "../../../../../api";
+import { getLogs } from "../../../../../api";
 import DialogWithTitle from "../../../../../common/DialogWithTitle";
 import NumberedLines from "../../../../../common/NumberedLines";
 
@@ -30,11 +30,11 @@ const styles = (theme: Theme) =>
 type Props = {
   clearLogDialog: () => void;
   nodeIp: string;
-  pid: number | null;
+  pid: number;
 };
 
 type State = {
-  result: LogsByPid | null;
+  result: string[] | null;
   error: string | null;
 };
 
@@ -48,7 +48,7 @@ class Logs extends React.Component<Props & WithStyles<typeof styles>, State> {
     try {
       const { nodeIp, pid } = this.props;
       const result = await getLogs(nodeIp, pid);
-      this.setState({ result: result.logs, error: null });
+      this.setState({ result, error: null });
     } catch (error) {
       this.setState({ result: null, error: error.toString() });
     }
@@ -65,20 +65,18 @@ class Logs extends React.Component<Props & WithStyles<typeof styles>, State> {
         ) : result === null ? (
           <Typography color="textSecondary">Loading...</Typography>
         ) : (
-          Object.entries(result).map(([pid, lines]) => (
-            <React.Fragment key={pid}>
-              <Typography className={classes.header}>
-                {nodeIp} (PID: {pid})
-              </Typography>
-              {lines.length > 0 ? (
-                <div className={classes.log}>
-                  <NumberedLines lines={lines} />
-                </div>
-              ) : (
-                <Typography color="textSecondary">No logs found.</Typography>
-              )}
-            </React.Fragment>
-          ))
+          <React.Fragment>
+            <Typography className={classes.header}>
+              {nodeIp} (PID: {this.props.pid})
+            </Typography>
+            {result.length > 0 ? (
+              <div className={classes.log}>
+                <NumberedLines lines={result} />
+              </div>
+            ) : (
+              <Typography color="textSecondary">No logs found.</Typography>
+            )}
+          </React.Fragment>
         )}
       </DialogWithTitle>
     );
diff --git a/dashboard/client/src/pages/dashboard/node-info/features/Logs.tsx b/dashboard/client/src/pages/dashboard/node-info/features/Logs.tsx
index 3985db534b3da..f9163b3cc018a 100644
--- a/dashboard/client/src/pages/dashboard/node-info/features/Logs.tsx
+++ b/dashboard/client/src/pages/dashboard/node-info/features/Logs.tsx
@@ -25,30 +25,25 @@ const ClusterLogs: ClusterFeatureRenderFn = ({ nodes }) => {
   );
 };
 
-const makeNodeLogs =
-  (
-    setLogDialog: (nodeIp: string, pid: number | null) => void,
-  ): NodeFeatureRenderFn =>
-  ({ node }) => {
-    const logCount = node.logCount ?? 0;
-    return logCount === 0 ? (
-      <Typography color="textSecondary" component="span" variant="inherit">
-        No logs
-      </Typography>
-    ) : (
-      <SpanButton onClick={() => setLogDialog(node.ip, null)}>
-        View all logs ({logCount.toLocaleString()}{" "}
-        {node.logCount === 1 ? "line" : "lines"})
-      </SpanButton>
-    );
-  };
+const makeNodeLogs: NodeFeatureRenderFn = ({ node }) => {
+  const logCount = node.logCount ?? 0;
+  return logCount === 0 ? (
+    <Typography color="textSecondary" component="span" variant="inherit">
+      No logs
+    </Typography>
+  ) : (
+    <Typography color="textSecondary" component="span" variant="inherit">
+      {logCount.toLocaleString()} {node.logCount === 1 ? "line" : "lines"}
+    </Typography>
+  );
+};
 
 const nodeLogsAccessor: Accessor<NodeFeatureData> = ({ node }) =>
   node.logCount ? sum(Object.values(node.logCount)) : 0;
 
 const makeWorkerLogs =
   (
-    setLogDialog: (nodeIp: string, pid: number | null) => void,
+    setLogDialog: (nodeIp: string, pid: number) => void,
   ): WorkerFeatureRenderFn =>
   ({ worker, node }) => {
     const logCount = worker.logCount ?? 0;
@@ -68,12 +63,12 @@ const workerLogsAccessor: Accessor<WorkerFeatureData> = ({ worker }) =>
   worker.logCount ?? 0;
 
 const makeLogsFeature = (
-  setLogDialog: (nodeIp: string, pid: number | null) => void,
+  setLogDialog: (nodeIp: string, pid: number) => void,
 ): NodeInfoFeature => ({
   id: "logs",
   ClusterFeatureRenderFn: ClusterLogs,
   WorkerFeatureRenderFn: makeWorkerLogs(setLogDialog),
-  NodeFeatureRenderFn: makeNodeLogs(setLogDialog),
+  NodeFeatureRenderFn: makeNodeLogs,
   workerAccessor: workerLogsAccessor,
   nodeAccessor: nodeLogsAccessor,
 });
diff --git a/dashboard/datacenter.py b/dashboard/datacenter.py
index 985908acd4904..2ba3655e071b7 100644
--- a/dashboard/datacenter.py
+++ b/dashboard/datacenter.py
@@ -48,9 +48,9 @@ class DataSource:
     core_worker_stats = Dict()
     # {job id hex(str): {event id(str): event dict}}
     events = Dict()
-    # {node ip (str): log entries by pid
-    # (dict from pid to list of latest log entries)}
-    ip_and_pid_to_logs = Dict()
+    # {node ip (str): log counts by pid
+    # (dict from pid to count of logs for that pid)}
+    ip_and_pid_to_log_counts = Dict()
     # {node ip (str): error entries by pid
     # (dict from pid to list of latest err entries)}
     ip_and_pid_to_errors = Dict()
@@ -103,7 +103,7 @@ async def organize(cls):
     async def get_node_workers(cls, node_id):
         workers = []
         node_ip = DataSource.node_id_to_ip[node_id]
-        node_logs = DataSource.ip_and_pid_to_logs.get(node_ip, {})
+        node_log_counts = DataSource.ip_and_pid_to_log_counts.get(node_ip, {})
         node_errs = DataSource.ip_and_pid_to_errors.get(node_ip, {})
         node_physical_stats = DataSource.node_physical_stats.get(node_id, {})
         node_stats = DataSource.node_stats.get(node_id, {})
@@ -120,15 +120,15 @@ async def get_node_workers(cls, node_id):
             pid_to_job_id[pid] = core_worker_stats["jobId"]
 
         # Clean up logs from a dead pid.
-        dead_pids = set(node_logs.keys()) - pids_on_node
+        dead_pids = set(node_log_counts.keys()) - pids_on_node
         for dead_pid in dead_pids:
-            if dead_pid in node_logs:
-                node_logs.mutable().pop(dead_pid)
+            if dead_pid in node_log_counts:
+                node_log_counts.mutable().pop(dead_pid)
 
         for worker in node_physical_stats.get("workers", []):
             worker = dict(worker)
             pid = worker["pid"]
-            worker["logCount"] = len(node_logs.get(str(pid), []))
+            worker["logCount"] = node_log_counts.get(str(pid), 0)
             worker["errorCount"] = len(node_errs.get(str(pid), []))
             worker["coreWorkerStats"] = pid_to_worker_stats.get(pid, [])
             worker["language"] = pid_to_language.get(
@@ -148,10 +148,10 @@ async def get_node_info(cls, node_id):
         node = DataSource.nodes.get(node_id, {})
         node_ip = DataSource.node_id_to_ip.get(node_id)
         # Merge node log count information into the payload
-        log_info = DataSource.ip_and_pid_to_logs.get(node_ip, {})
+        log_counts = DataSource.ip_and_pid_to_log_counts.get(node_ip, {})
         node_log_count = 0
-        for entries in log_info.values():
-            node_log_count += len(entries)
+        for entries in log_counts.values():
+            node_log_count += entries
         error_info = DataSource.ip_and_pid_to_errors.get(node_ip, {})
         node_err_count = 0
         for entries in error_info.values():
diff --git a/dashboard/modules/actor/actor_head.py b/dashboard/modules/actor/actor_head.py
index fa4edd84236bd..113bb926d0b75 100644
--- a/dashboard/modules/actor/actor_head.py
+++ b/dashboard/modules/actor/actor_head.py
@@ -1,5 +1,7 @@
 import asyncio
+from collections import deque
 import logging
+import os
 
 import aiohttp.web
 
@@ -25,10 +27,12 @@
 except ImportError:
     from grpc.experimental import aio as aiogrpc
 
-
 logger = logging.getLogger(__name__)
 routes = dashboard_optional_utils.ClassMethodRouteTable
 
+MAX_ACTORS_TO_CACHE = int(os.environ.get("RAY_DASHBOARD_MAX_ACTORS_TO_CACHE", 1000))
+ACTOR_CLEANUP_FREQUENCY = 10  # seconds
+
 
 def actor_table_data_to_dict(message):
     orig_message = dashboard_utils.message_to_dict(
@@ -79,6 +83,8 @@ def __init__(self, dashboard_head):
         self._stubs = {}
         # ActorInfoGcsService
         self._gcs_actor_info_stub = None
+        # A queue of dead actors in order of when they died
+        self.dead_actors_queue = deque()
         DataSource.nodes.signal.append(self._update_stubs)
 
     async def _update_stubs(self, change):
@@ -154,6 +160,8 @@ def process_actor_data_from_pubsub(actor_id, actor_table_data):
             actor_id = actor_table_data["actorId"]
             job_id = actor_table_data["jobId"]
             node_id = actor_table_data["address"]["rayletId"]
+            if actor_table_data["state"] == "DEAD":
+                self.dead_actors_queue.append(actor_id)
             # Update actors.
             DataSource.actors[actor_id] = actor_table_data
             # Update node actors (only when node_id is not Nil).
@@ -181,6 +189,30 @@ def process_actor_data_from_pubsub(actor_id, actor_table_data):
             except Exception:
                 logger.exception("Error processing actor info from GCS.")
 
+    async def _cleanup_actors(self):
+        while True:
+            try:
+                if len(DataSource.actors) > MAX_ACTORS_TO_CACHE:
+                    logger.debug("Cleaning up dead actors from GCS")
+                    while len(DataSource.actors) > MAX_ACTORS_TO_CACHE:
+                        if not self.dead_actors_queue:
+                            logger.warning(
+                                f"More than {MAX_ACTORS_TO_CACHE} "
+                                "live actors are cached"
+                            )
+                            break
+                        actor_id = self.dead_actors_queue.popleft()
+                        if actor_id in DataSource.actors:
+                            actor = DataSource.actors.pop(actor_id)
+                            job_id = actor["jobId"]
+                            del DataSource.job_actors[job_id][actor_id]
+                            node_id = actor["address"].get("rayletId")
+                            if node_id:
+                                del DataSource.node_actors[node_id][actor_id]
+                await asyncio.sleep(ACTOR_CLEANUP_FREQUENCY)
+            except Exception:
+                logger.exception("Error cleaning up actor info from GCS.")
+
     @routes.get("/logical/actor_groups")
     async def get_actor_groups(self, req) -> aiohttp.web.Response:
         actors = await DataOrganizer.get_all_actors()
@@ -236,6 +268,7 @@ async def run(self, server):
             gcs_channel
         )
 
+        asyncio.get_event_loop().create_task(self._cleanup_actors())
         await asyncio.gather(self._update_actors())
 
     @staticmethod
diff --git a/dashboard/modules/node/node_head.py b/dashboard/modules/node/node_head.py
index 8a4aa2ecad011..7804be9d1176e 100644
--- a/dashboard/modules/node/node_head.py
+++ b/dashboard/modules/node/node_head.py
@@ -220,17 +220,6 @@ async def set_fetch_memory_info(self, req) -> aiohttp.web.Response:
             success=True, message=f"Successfully set fetching to {should_fetch}"
         )
 
-    @routes.get("/node_logs")
-    async def get_logs(self, req) -> aiohttp.web.Response:
-        ip = req.query["ip"]
-        pid = str(req.query.get("pid", ""))
-        node_logs = DataSource.ip_and_pid_to_logs.get(ip, {})
-        if pid:
-            node_logs = {str(pid): node_logs.get(pid, [])}
-        return dashboard_optional_utils.rest_response(
-            success=True, message="Fetched logs.", logs=node_logs
-        )
-
     @routes.get("/node_errors")
     async def get_errors(self, req) -> aiohttp.web.Response:
         ip = req.query["ip"]
@@ -269,18 +258,13 @@ def process_log_batch(log_batch):
             ip = log_batch["ip"]
             pid = str(log_batch["pid"])
             if pid != "autoscaler":
-                logs_for_ip = dict(DataSource.ip_and_pid_to_logs.get(ip, {}))
-                logs_for_pid = list(logs_for_ip.get(pid, []))
-                logs_for_pid.extend(log_batch["lines"])
-
-                # Only cache upto MAX_LOGS_TO_CACHE
-                logs_length = len(logs_for_pid)
-                if logs_length > MAX_LOGS_TO_CACHE * LOG_PRUNE_THREASHOLD:
-                    offset = logs_length - MAX_LOGS_TO_CACHE
-                    del logs_for_pid[:offset]
-
-                logs_for_ip[pid] = logs_for_pid
-                DataSource.ip_and_pid_to_logs[ip] = logs_for_ip
+                log_counts_for_ip = dict(
+                    DataSource.ip_and_pid_to_log_counts.get(ip, {})
+                )
+                log_counts_for_pid = log_counts_for_ip.get(pid, 0)
+                log_counts_for_pid += len(log_batch["lines"])
+                log_counts_for_ip[pid] = log_counts_for_pid
+                DataSource.ip_and_pid_to_log_counts[ip] = log_counts_for_ip
             logger.debug(f"Received a log for {ip} and {pid}")
 
         while True:
@@ -309,6 +293,13 @@ def process_error(error_data):
                         "type": error_data.type,
                     }
                 )
+
+                # Only cache up to MAX_LOGS_TO_CACHE
+                pid_errors_length = len(pid_errors)
+                if pid_errors_length > MAX_LOGS_TO_CACHE * LOG_PRUNE_THREASHOLD:
+                    offset = pid_errors_length - MAX_LOGS_TO_CACHE
+                    del pid_errors[:offset]
+
                 errs_for_ip[pid] = pid_errors
                 DataSource.ip_and_pid_to_errors[ip] = errs_for_ip
                 logger.info(f"Received error entry for {ip} {pid}")

Test patch: diff --git a/dashboard/modules/actor/tests/test_actor.py b/dashboard/modules/actor/tests/test_actor.py
index 04a04a6fd1389..8cad11a58722b 100644
--- a/dashboard/modules/actor/tests/test_actor.py
+++ b/dashboard/modules/actor/tests/test_actor.py
@@ -342,5 +342,98 @@ class InfeasibleActor:
                 raise Exception(f"Timed out while testing, {ex_stack}")
 
 
+def test_actor_cleanup(
+    disable_aiohttp_cache, reduce_actor_cache, ray_start_with_dashboard
+):
+    @ray.remote
+    class Foo:
+        def __init__(self, num):
+            self.num = num
+
+        def do_task(self):
+            return self.num
+
+    @ray.remote(num_gpus=1)
+    class InfeasibleActor:
+        pass
+
+    infeasible_actor = InfeasibleActor.remote()  # noqa
+
+    foo_actors = [
+        Foo.remote(1),
+        Foo.remote(2),
+        Foo.remote(3),
+        Foo.remote(4),
+        Foo.remote(5),
+        Foo.remote(6),
+    ]
+    results = [actor.do_task.remote() for actor in foo_actors]  # noqa
+    webui_url = ray_start_with_dashboard["webui_url"]
+    assert wait_until_server_available(webui_url)
+    webui_url = format_web_url(webui_url)
+
+    timeout_seconds = 8
+    start_time = time.time()
+    last_ex = None
+    while True:
+        time.sleep(1)
+        try:
+            resp = requests.get(f"{webui_url}/logical/actors")
+            resp_json = resp.json()
+            resp_data = resp_json["data"]
+            actors = resp_data["actors"]
+            # Although max cache is 3, there should be 7 actors
+            # because they are all still alive.
+            assert len(actors) == 7
+
+            break
+        except Exception as ex:
+            last_ex = ex
+        finally:
+            if time.time() > start_time + timeout_seconds:
+                ex_stack = (
+                    traceback.format_exception(
+                        type(last_ex), last_ex, last_ex.__traceback__
+                    )
+                    if last_ex
+                    else []
+                )
+                ex_stack = "".join(ex_stack)
+                raise Exception(f"Timed out while testing, {ex_stack}")
+
+    # kill
+    ray.kill(infeasible_actor)
+    [ray.kill(foo_actor) for foo_actor in foo_actors]
+    # Wait 5 seconds for cleanup to finish
+    time.sleep(5)
+
+    # Check only three remaining in cache
+    start_time = time.time()
+    while True:
+        time.sleep(1)
+        try:
+            resp = requests.get(f"{webui_url}/logical/actors")
+            resp_json = resp.json()
+            resp_data = resp_json["data"]
+            actors = resp_data["actors"]
+            # Max cache is 3 so only 3 actors should be left.
+            assert len(actors) == 3
+
+            break
+        except Exception as ex:
+            last_ex = ex
+        finally:
+            if time.time() > start_time + timeout_seconds:
+                ex_stack = (
+                    traceback.format_exception(
+                        type(last_ex), last_ex, last_ex.__traceback__
+                    )
+                    if last_ex
+                    else []
+                )
+                ex_stack = "".join(ex_stack)
+                raise Exception(f"Timed out while testing, {ex_stack}")
+
+
 if __name__ == "__main__":
     sys.exit(pytest.main(["-v", __file__]))
diff --git a/dashboard/modules/node/tests/test_node.py b/dashboard/modules/node/tests/test_node.py
index 6e33e5ec9af49..80c9ad2700cb1 100644
--- a/dashboard/modules/node/tests/test_node.py
+++ b/dashboard/modules/node/tests/test_node.py
@@ -10,10 +10,6 @@
 import threading
 from datetime import datetime, timedelta
 from ray.cluster_utils import Cluster
-from ray.dashboard.modules.node.node_consts import (
-    LOG_PRUNE_THREASHOLD,
-    MAX_LOGS_TO_CACHE,
-)
 from ray.dashboard.tests.conftest import *  # noqa
 from ray._private.test_utils import (
     format_web_url,
@@ -22,8 +18,6 @@
     wait_until_succeeded_without_exception,
 )
 
-from unittest import mock
-
 
 logger = logging.getLogger(__name__)
 
@@ -327,204 +321,5 @@ def get_nodes():
         time.sleep(2)
 
 
-@pytest.fixture
-def disable_dashboard_log_info(request):
-    if request.param is False:
-        env_var_value = "0"
-    else:
-        env_var_value = "1"
-
-    with mock.patch.dict(
-        os.environ,
-        {
-            "RAY_DISABLE_DASHBOARD_LOG_INFO": env_var_value,
-        },
-    ):
-        yield request.param
-
-
-@pytest.mark.parametrize(
-    "ray_start_cluster_head", [{"include_dashboard": True}], indirect=True
-)
-@pytest.mark.parametrize("disable_dashboard_log_info", [False, True], indirect=True)
-def test_logs(
-    enable_test_module,
-    disable_aiohttp_cache,
-    disable_dashboard_log_info,
-    ray_start_cluster_head,
-):
-    cluster = ray_start_cluster_head
-    assert wait_until_server_available(cluster.webui_url) is True
-    webui_url = cluster.webui_url
-    webui_url = format_web_url(webui_url)
-    nodes = ray.nodes()
-    assert len(nodes) == 1
-    node_ip = nodes[0]["NodeManagerAddress"]
-
-    @ray.remote
-    class LoggingActor:
-        def go(self, n):
-            i = 0
-            while i < n:
-                print(f"On number {i}")
-                i += 1
-
-        def get_pid(self):
-            return os.getpid()
-
-    la = LoggingActor.remote()
-    la2 = LoggingActor.remote()
-    la_pid = str(ray.get(la.get_pid.remote()))
-    la2_pid = str(ray.get(la2.get_pid.remote()))
-    ray.get(la.go.remote(4))
-    ray.get(la2.go.remote(1))
-
-    def check_logs():
-        node_logs_response = requests.get(
-            f"{webui_url}/node_logs", params={"ip": node_ip}
-        )
-        node_logs_response.raise_for_status()
-        node_logs = node_logs_response.json()
-        assert node_logs["result"]
-        assert type(node_logs["data"]["logs"]) is dict
-
-        if disable_dashboard_log_info:
-            assert node_logs["data"]["logs"] == {}
-            return
-
-        assert all(pid in node_logs["data"]["logs"] for pid in (la_pid, la2_pid))
-        assert len(node_logs["data"]["logs"][la2_pid]) == 1
-
-        actor_one_logs_response = requests.get(
-            f"{webui_url}/node_logs", params={"ip": node_ip, "pid": str(la_pid)}
-        )
-        actor_one_logs_response.raise_for_status()
-        actor_one_logs = actor_one_logs_response.json()
-        assert actor_one_logs["result"]
-        assert type(actor_one_logs["data"]["logs"]) is dict
-        assert len(actor_one_logs["data"]["logs"][la_pid]) == 4
-
-    assert wait_until_succeeded_without_exception(
-        check_logs, (AssertionError,), timeout_ms=1000
-    )
-
-
-@pytest.mark.parametrize(
-    "ray_start_cluster_head", [{"include_dashboard": True}], indirect=True
-)
-def test_logs_clean_up(
-    enable_test_module, disable_aiohttp_cache, ray_start_cluster_head
-):
-    """Check if logs from the dead pids are GC'ed."""
-    cluster = ray_start_cluster_head
-    assert wait_until_server_available(cluster.webui_url) is True
-    webui_url = cluster.webui_url
-    webui_url = format_web_url(webui_url)
-    nodes = ray.nodes()
-    assert len(nodes) == 1
-    node_ip = nodes[0]["NodeManagerAddress"]
-
-    @ray.remote
-    class LoggingActor:
-        def go(self, n):
-            i = 0
-            while i < n:
-                print(f"On number {i}")
-                i += 1
-
-        def get_pid(self):
-            return os.getpid()
-
-    la = LoggingActor.remote()
-    la_pid = str(ray.get(la.get_pid.remote()))
-    ray.get(la.go.remote(1))
-
-    def check_logs():
-        node_logs_response = requests.get(
-            f"{webui_url}/node_logs", params={"ip": node_ip}
-        )
-        node_logs_response.raise_for_status()
-        node_logs = node_logs_response.json()
-        assert node_logs["result"]
-        assert la_pid in node_logs["data"]["logs"]
-
-    assert wait_until_succeeded_without_exception(
-        check_logs, (AssertionError,), timeout_ms=1000
-    )
-    ray.kill(la)
-
-    def check_logs_not_exist():
-        node_logs_response = requests.get(
-            f"{webui_url}/node_logs", params={"ip": node_ip}
-        )
-        node_logs_response.raise_for_status()
-        node_logs = node_logs_response.json()
-        assert node_logs["result"]
-        assert la_pid not in node_logs["data"]["logs"]
-
-    assert wait_until_succeeded_without_exception(
-        check_logs_not_exist, (AssertionError,), timeout_ms=10000
-    )
-
-
-@pytest.mark.parametrize(
-    "ray_start_cluster_head", [{"include_dashboard": True}], indirect=True
-)
-def test_logs_max_count(
-    enable_test_module, disable_aiohttp_cache, ray_start_cluster_head
-):
-    """Test that each Ray worker cannot cache more than 1000 logs at a time."""
-    cluster = ray_start_cluster_head
-    assert wait_until_server_available(cluster.webui_url) is True
-    webui_url = cluster.webui_url
-    webui_url = format_web_url(webui_url)
-    nodes = ray.nodes()
-    assert len(nodes) == 1
-    node_ip = nodes[0]["NodeManagerAddress"]
-
-    @ray.remote
-    class LoggingActor:
-        def go(self, n):
-            i = 0
-            while i < n:
-                print(f"On number {i}")
-                i += 1
-
-        def get_pid(self):
-            return os.getpid()
-
-    la = LoggingActor.remote()
-    la_pid = str(ray.get(la.get_pid.remote()))
-    ray.get(la.go.remote(MAX_LOGS_TO_CACHE * LOG_PRUNE_THREASHOLD))
-
-    def check_logs():
-        node_logs_response = requests.get(
-            f"{webui_url}/node_logs", params={"ip": node_ip}
-        )
-        node_logs_response.raise_for_status()
-        node_logs = node_logs_response.json()
-        assert node_logs["result"]
-        assert type(node_logs["data"]["logs"]) is dict
-        assert la_pid in node_logs["data"]["logs"]
-        log_lengths = len(node_logs["data"]["logs"][la_pid])
-        assert log_lengths >= MAX_LOGS_TO_CACHE
-        assert log_lengths <= MAX_LOGS_TO_CACHE * LOG_PRUNE_THREASHOLD
-
-        actor_one_logs_response = requests.get(
-            f"{webui_url}/node_logs", params={"ip": node_ip, "pid": str(la_pid)}
-        )
-        actor_one_logs_response.raise_for_status()
-        actor_one_logs = actor_one_logs_response.json()
-        assert actor_one_logs["result"]
-        assert type(actor_one_logs["data"]["logs"]) is dict
-        log_lengths = len(actor_one_logs["data"]["logs"][la_pid])
-        assert log_lengths >= MAX_LOGS_TO_CACHE
-        assert log_lengths <= MAX_LOGS_TO_CACHE * LOG_PRUNE_THREASHOLD
-
-    assert wait_until_succeeded_without_exception(
-        check_logs, (AssertionError,), timeout_ms=10000
-    )
-
-
 if __name__ == "__main__":
     sys.exit(pytest.main(["-v", __file__]))
diff --git a/dashboard/tests/conftest.py b/dashboard/tests/conftest.py
index b99884bdf3edb..5a5a0a1e96906 100644
--- a/dashboard/tests/conftest.py
+++ b/dashboard/tests/conftest.py
@@ -40,3 +40,10 @@ def fast_gcs_failure_detection():
     os.environ.pop("GCS_CHECK_ALIVE_INTERVAL_SECONDS", None)
     os.environ.pop("GCS_RETRY_CONNECT_INTERVAL_SECONDS", None)
     os.environ.pop("GCS_CHECK_ALIVE_RPC_TIMEOUT", None)
+
+
+@pytest.fixture
+def reduce_actor_cache():
+    os.environ["RAY_DASHBOARD_MAX_ACTORS_TO_CACHE"] = "3"
+    yield
+    os.environ.pop("RAY_DASHBOARD_MAX_ACTORS_TO_CACHE", None)