diff --git a/lib/modules/platform/github/__snapshots__/index.spec.ts.snap b/lib/modules/platform/github/__snapshots__/index.spec.ts.snap
index b117d19bd7ae69a1f936616b3e5cd80cd7bdb65c..c200deb87d269df4da92229f7d68e6b4a0ecb368 100644
--- a/lib/modules/platform/github/__snapshots__/index.spec.ts.snap
+++ b/lib/modules/platform/github/__snapshots__/index.spec.ts.snap
@@ -1,18 +1,8 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`modules/platform/github/index getBranchPr(branchName) should reopen an autoclosed PR 1`] = `
-Object {
-  "displayNumber": "Pull Request #91",
-  "number": 91,
-  "sourceBranch": "somebranch",
-  "sourceRepo": "some/repo",
-  "state": "open",
-  "title": "Some title",
-}
-`;
-
-exports[`modules/platform/github/index getBranchPr(branchName) should return the PR object 1`] = `
+exports[`modules/platform/github/index getBranchPr(branchName) should cache and return the PR object 1`] = `
 Object {
+  "body": "dummy body",
   "displayNumber": "Pull Request #91",
   "number": 91,
   "sourceBranch": "somebranch",
@@ -22,8 +12,9 @@ Object {
 }
 `;
 
-exports[`modules/platform/github/index getBranchPr(branchName) should return the PR object in fork mode 1`] = `
+exports[`modules/platform/github/index getBranchPr(branchName) should cache and return the PR object in fork mode 1`] = `
 Object {
+  "body": "dummy body",
   "displayNumber": "Pull Request #90",
   "number": 90,
   "sourceBranch": "somebranch",
@@ -33,33 +24,33 @@ Object {
 }
 `;
 
-exports[`modules/platform/github/index getPr(prNo) should return PR from closed graphql result 1`] = `
+exports[`modules/platform/github/index getBranchPr(branchName) should reopen and cache autoclosed PR 1`] = `
 Object {
   "body": "dummy body",
-  "displayNumber": "Pull Request #2499",
-  "number": 2499,
-  "sourceBranch": "renovate/delay-4.x",
-  "state": "merged",
-  "title": "build(deps): update dependency delay to v4.0.1",
+  "displayNumber": "Pull Request #91",
+  "number": 91,
+  "sourceBranch": "somebranch",
+  "sourceRepo": "some/repo",
+  "state": "open",
+  "title": "old title",
 }
 `;
 
-exports[`modules/platform/github/index getPr(prNo) should return PR from graphql result 1`] = `
+exports[`modules/platform/github/index getPr(prNo) should return PR 1`] = `
 Object {
-  "body": "Some body",
+  "body": "dummy body",
   "displayNumber": "Pull Request #2500",
-  "hasAssignees": true,
-  "hasReviewers": true,
   "number": 2500,
   "sourceBranch": "renovate/jest-monorepo",
+  "sourceRepo": "some/repo",
   "state": "open",
-  "targetBranch": "master",
   "title": "chore(deps): update dependency jest to v23.6.0",
 }
 `;
 
 exports[`modules/platform/github/index getPr(prNo) should return a PR object - 0 1`] = `
 Object {
+  "body": "dummy body",
   "createdAt": "01-01-2022",
   "displayNumber": "Pull Request #1234",
   "hasAssignees": true,
@@ -77,6 +68,7 @@ Object {
 
 exports[`modules/platform/github/index getPr(prNo) should return a PR object - 1 1`] = `
 Object {
+  "body": "dummy body",
   "displayNumber": "Pull Request #1234",
   "hasAssignees": true,
   "hasReviewers": true,
@@ -89,6 +81,7 @@ Object {
 
 exports[`modules/platform/github/index getPr(prNo) should return a PR object - 2 1`] = `
 Object {
+  "body": "dummy body",
   "displayNumber": "Pull Request #1234",
   "number": 1234,
   "sourceBranch": "some/branch",
diff --git a/lib/modules/platform/github/common.ts b/lib/modules/platform/github/common.ts
index 7e13b3b0d9dfd3b1f31833bb6339832ac3f4ffe3..b7d2c52ed63a987b27c1c10fabd87cf2ae00fc31 100644
--- a/lib/modules/platform/github/common.ts
+++ b/lib/modules/platform/github/common.ts
@@ -1,44 +1,16 @@
 import is from '@sindresorhus/is';
 import { PrState } from '../../../types';
 import type { Pr } from '../types';
-import type { GhGraphQlPr, GhRestPr } from './types';
+import type { GhRestPr } from './types';
 
 /**
- * @see https://developer.github.com/v4/object/pullrequest/
+ * @see https://docs.github.com/en/rest/reference/pulls#list-pull-requests
  */
-export function coerceGraphqlPr(pr: GhGraphQlPr): Pr {
-  const result: Pr = {
-    number: pr.number,
-    displayNumber: `Pull Request #${pr.number}`,
-    title: pr.title,
-    state: pr.state ? pr.state.toLowerCase() : PrState.Open,
-    sourceBranch: pr.headRefName,
-    body: pr.body ? pr.body : 'dummy body',
-  };
-
-  if (pr.baseRefName) {
-    result.targetBranch = pr.baseRefName;
+export function coerceRestPr(pr: GhRestPr | null | undefined): Pr | null {
+  if (!pr) {
+    return null;
   }
 
-  if (pr.assignees) {
-    result.hasAssignees = !!(pr.assignees.totalCount > 0);
-  }
-
-  if (pr.reviewRequests) {
-    result.hasReviewers = !!(pr.reviewRequests.totalCount > 0);
-  }
-
-  if (pr.labels?.nodes) {
-    result.labels = pr.labels.nodes.map((label) => label.name);
-  }
-
-  return result;
-}
-
-/**
- * @see https://docs.github.com/en/rest/reference/pulls#list-pull-requests
- */
-export function coerceRestPr(pr: GhRestPr): Pr {
   const result: Pr = {
     displayNumber: `Pull Request #${pr.number}`,
     number: pr.number,
@@ -48,6 +20,7 @@ export function coerceRestPr(pr: GhRestPr): Pr {
       pr.state === PrState.Closed && is.string(pr.merged_at)
         ? PrState.Merged
         : pr.state,
+    body: pr.body ?? 'dummy body',
   };
 
   if (pr.head?.sha) {
diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts
index 8d945469f3b654e5e487e75d0a26751f1327f7a8..3c910a60a061bda99c53a8579066f782bb436130 100644
--- a/lib/modules/platform/github/index.spec.ts
+++ b/lib/modules/platform/github/index.spec.ts
@@ -1,16 +1,17 @@
 import { DateTime } from 'luxon';
 import * as httpMock from '../../../../test/http-mock';
-import { loadFixture, logger, mocked } from '../../../../test/util';
+import { logger, mocked } from '../../../../test/util';
 import {
   REPOSITORY_NOT_FOUND,
   REPOSITORY_RENAMED,
 } from '../../../constants/error-messages';
 import { BranchStatus, PrState, VulnerabilityAlert } from '../../../types';
+import * as repository from '../../../util/cache/repository';
 import * as _git from '../../../util/git';
 import * as _hostRules from '../../../util/host-rules';
 import { setBaseUrl } from '../../../util/http/github';
 import { toBase64 } from '../../../util/string';
-import type { CreatePRConfig } from '../types';
+import type { CreatePRConfig, UpdatePrConfig } from '../types';
 import * as github from '.';
 
 const githubApiHost = 'https://api.github.com';
@@ -38,12 +39,10 @@ describe('modules/platform/github/index', () => {
     hostRules.find.mockReturnValue({
       token: '123test',
     });
-  });
 
-  const graphqlOpenPullRequests = loadFixture('graphql/pullrequests-open.json');
-  const graphqlClosedPullRequests = loadFixture(
-    'graphql/pullrequests-closed.json'
-  );
+    const repoCache = repository.getCache();
+    delete repoCache.platform;
+  });
 
   describe('initPlatform()', () => {
     it('should throw if no token', async () => {
@@ -551,11 +550,149 @@ describe('modules/platform/github/index', () => {
     });
   });
 
+  describe('getPrList()', () => {
+    const t = DateTime.fromISO('2000-01-01T00:00:00.000+00:00');
+    const t1 = t.plus({ minutes: 1 }).toISO();
+    const t2 = t.plus({ minutes: 2 }).toISO();
+    const t3 = t.plus({ minutes: 3 }).toISO();
+    const t4 = t.plus({ minutes: 4 }).toISO();
+
+    const pr1 = {
+      number: 1,
+      head: { ref: 'branch-1', repo: { full_name: 'some/repo' } },
+      state: PrState.Open,
+      title: 'PR #1',
+      updated_at: t1,
+    };
+
+    const pr2 = {
+      number: 2,
+      head: { ref: 'branch-2', repo: { full_name: 'some/repo' } },
+      state: PrState.Open,
+      title: 'PR #2',
+      updated_at: t2,
+    };
+
+    const pr3 = {
+      number: 3,
+      head: { ref: 'branch-3', repo: { full_name: 'some/repo' } },
+      state: PrState.Open,
+      title: 'PR #3',
+      updated_at: t3,
+    };
+
+    const pagePath = (x: number) =>
+      `/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=${x}`;
+    const pageLink = (x: number) =>
+      `<${githubApiHost}${pagePath(x)}>; rel="next"`;
+
+    it('fetches single page', async () => {
+      const scope = httpMock.scope(githubApiHost);
+      initRepoMock(scope, 'some/repo');
+      scope.get(pagePath(1)).reply(200, [pr1]);
+      await github.initRepo({ repository: 'some/repo' } as never);
+
+      const res = await github.getPrList();
+
+      expect(res).toMatchObject([{ number: 1, title: 'PR #1' }]);
+    });
+
+    it('fetches multiple pages', async () => {
+      const scope = httpMock.scope(githubApiHost);
+      initRepoMock(scope, 'some/repo');
+      scope
+        .get(pagePath(1))
+        .reply(200, [pr3], {
+          link: `${pageLink(2)}, ${pageLink(3).replace('next', 'last')}`,
+        })
+        .get(pagePath(2))
+        .reply(200, [pr2], { link: pageLink(3) })
+        .get(pagePath(3))
+        .reply(200, [pr1]);
+      await github.initRepo({ repository: 'some/repo' } as never);
+
+      const res = await github.getPrList();
+
+      expect(res).toMatchObject([{ number: 1 }, { number: 2 }, { number: 3 }]);
+    });
+
+    it('uses ETag', async () => {
+      const scope = httpMock.scope(githubApiHost);
+      initRepoMock(scope, 'some/repo');
+      initRepoMock(scope, 'some/repo');
+      scope
+        .get(pagePath(1))
+        .reply(200, [pr3, pr2, pr1], { etag: 'foobar' })
+        .get(pagePath(1))
+        .reply(304);
+
+      await github.initRepo({ repository: 'some/repo' } as never);
+      const res1 = await github.getPrList();
+
+      await github.initRepo({ repository: 'some/repo' } as never);
+      const res2 = await github.getPrList();
+
+      expect(res1).toMatchObject([{ number: 1 }, { number: 2 }, { number: 3 }]);
+      expect(res1).toEqual(res2);
+    });
+
+    it('synchronizes cache', async () => {
+      const scope = httpMock.scope(githubApiHost);
+      initRepoMock(scope, 'some/repo');
+      initRepoMock(scope, 'some/repo');
+
+      scope
+        .get(pagePath(1))
+        .reply(200, [pr3], {
+          link: `${pageLink(2)}, ${pageLink(3).replace('next', 'last')}`,
+          etag: 'foo',
+        })
+        .get(pagePath(2))
+        .reply(200, [pr2])
+        .get(pagePath(3))
+        .reply(200, [pr1]);
+
+      await github.initRepo({ repository: 'some/repo' } as never);
+      const res1 = await github.getPrList();
+
+      scope
+        .get(pagePath(1))
+        .reply(200, [{ ...pr3, updated_at: t4, title: 'PR #3 (updated)' }], {
+          link: `${pageLink(2)}`,
+          etag: 'bar',
+        })
+        .get(pagePath(2))
+        .reply(200, [{ ...pr2, updated_at: t4, title: 'PR #2 (updated)' }], {
+          link: `${pageLink(3)}`,
+        })
+        .get(pagePath(3))
+        .reply(200, [{ ...pr1, updated_at: t4, title: 'PR #1 (updated)' }]);
+
+      await github.initRepo({ repository: 'some/repo' } as never);
+      const res2 = await github.getPrList();
+
+      expect(res1).toMatchObject([
+        { number: 1, title: 'PR #1' },
+        { number: 2, title: 'PR #2' },
+        { number: 3, title: 'PR #3' },
+      ]);
+      expect(res2).toMatchObject([
+        { number: 1, title: 'PR #1 (updated)' },
+        { number: 2, title: 'PR #2 (updated)' },
+        { number: 3, title: 'PR #3 (updated)' },
+      ]);
+    });
+  });
+
   describe('getBranchPr(branchName)', () => {
     it('should return null if no PR exists', async () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
-      scope.get('/repos/some/repo/pulls?per_page=100&state=all').reply(200, []);
+      scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, []);
 
       await github.initRepo({
         repository: 'some/repo',
@@ -564,59 +701,46 @@ describe('modules/platform/github/index', () => {
       expect(pr).toBeNull();
     });
 
-    it('should return the PR object', async () => {
+    it('should cache and return the PR object', async () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
-        .post('/graphql')
-        .twice() // getOpenPrs() and getClosedPrs()
-        .reply(200, {
-          data: { repository: { pullRequests: { pageInfo: {} } } },
-        })
-        .get('/repos/some/repo/pulls?per_page=100&state=all')
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
         .reply(200, [
           {
             number: 90,
             head: { ref: 'somebranch', repo: { full_name: 'other/repo' } },
             state: PrState.Open,
+            title: 'PR from another repo',
           },
           {
             number: 91,
+            base: { sha: '1234' },
             head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
             state: PrState.Open,
+            title: 'Some title',
           },
-        ])
-        .get('/repos/some/repo/pulls/91')
-        .reply(200, {
-          number: 91,
-          additions: 1,
-          deletions: 1,
-          commits: 1,
-          base: {
-            sha: '1234',
-          },
-          head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
-          state: PrState.Open,
-          title: 'Some title',
-        });
-
+        ]);
       await github.initRepo({
         repository: 'some/repo',
       } as any);
+
       const pr = await github.getBranchPr('somebranch');
+      const pr2 = await github.getBranchPr('somebranch');
+
       expect(pr).toMatchSnapshot();
+      expect(pr2).toEqual(pr);
     });
 
-    it('should reopen an autoclosed PR', async () => {
+    it('should reopen and cache autoclosed PR', async () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
-        .post('/graphql')
-        .twice() // getOpenPrs() and getClosedPrs()
-        .reply(200, {
-          data: { repository: { pullRequests: { pageInfo: {} } } },
-        })
-        .get('/repos/some/repo/pulls?per_page=100&state=all')
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
         .reply(200, [
           {
             number: 90,
@@ -634,45 +758,45 @@ describe('modules/platform/github/index', () => {
         .post('/repos/some/repo/git/refs')
         .reply(201)
         .patch('/repos/some/repo/pulls/91')
-        .reply(201)
-        .get('/repos/some/repo/pulls/91')
         .reply(200, {
           number: 91,
-          additions: 1,
-          deletions: 1,
-          commits: 1,
-          base: {
-            sha: '1234',
-          },
+          base: { sha: '1234' },
           head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
           state: PrState.Open,
-          title: 'Some title',
+          title: 'old title',
         });
-
       await github.initRepo({
         repository: 'some/repo',
       } as any);
+
       const pr = await github.getBranchPr('somebranch');
-      expect(pr).toMatchSnapshot();
+      const pr2 = await github.getBranchPr('somebranch');
+
+      expect(pr).toMatchSnapshot({ number: 91 });
+      expect(pr2).toEqual(pr);
     });
 
     it('aborts reopen if PR is too old', async () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
-      scope.get('/repos/some/repo/pulls?per_page=100&state=all').reply(200, [
-        {
-          number: 90,
-          head: { ref: 'somebranch', repo: { full_name: 'other/repo' } },
-          state: PrState.Open,
-        },
-        {
-          number: 91,
-          head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
-          title: 'old title - autoclosed',
-          state: PrState.Closed,
-          closed_at: DateTime.now().minus({ days: 7 }).toISO(),
-        },
-      ]);
+      scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, [
+          {
+            number: 90,
+            head: { ref: 'somebranch', repo: { full_name: 'other/repo' } },
+            state: PrState.Open,
+          },
+          {
+            number: 91,
+            head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
+            title: 'old title - autoclosed',
+            state: PrState.Closed,
+            closed_at: DateTime.now().minus({ days: 7 }).toISO(),
+          },
+        ]);
 
       await github.initRepo({
         repository: 'some/repo',
@@ -685,7 +809,9 @@ describe('modules/platform/github/index', () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
-        .get('/repos/some/repo/pulls?per_page=100&state=all')
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
         .reply(200, [
           {
             number: 91,
@@ -711,7 +837,9 @@ describe('modules/platform/github/index', () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
-        .get('/repos/some/repo/pulls?per_page=100&state=all')
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
         .reply(200, [
           {
             number: 91,
@@ -731,49 +859,41 @@ describe('modules/platform/github/index', () => {
       expect(pr).toBeNull();
     });
 
-    it('should return the PR object in fork mode', async () => {
+    it('should cache and return the PR object in fork mode', async () => {
       const scope = httpMock.scope(githubApiHost);
       forkInitRepoMock(scope, 'some/repo', true);
       scope
-        .post('/graphql')
-        .twice() // getOpenPrs() and getClosedPrs()
-        .reply(200, {
-          data: { repository: { pullRequests: { pageInfo: {} } } },
-        })
-        .get('/repos/some/repo/pulls?per_page=100&state=all')
+        .patch('/repos/forked/repo/git/refs/heads/master')
+        .reply(200)
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
         .reply(200, [
           {
             number: 90,
+            base: { sha: '1234' },
             head: { ref: 'somebranch', repo: { full_name: 'other/repo' } },
             state: PrState.Open,
+            title: 'Some title',
           },
           {
             number: 91,
+            base: { sha: '1234' },
             head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
             state: PrState.Open,
+            title: 'Wrong PR',
           },
-        ])
-        .get('/repos/some/repo/pulls/90')
-        .reply(200, {
-          number: 90,
-          additions: 1,
-          deletions: 1,
-          commits: 1,
-          base: {
-            sha: '1234',
-          },
-          head: { ref: 'somebranch', repo: { full_name: 'other/repo' } },
-          state: PrState.Open,
-          title: 'Some title',
-        })
-        .patch('/repos/forked/repo/git/refs/heads/master')
-        .reply(200);
+        ]);
       await github.initRepo({
         repository: 'some/repo',
         forkMode: true,
       } as any);
+
       const pr = await github.getBranchPr('somebranch');
-      expect(pr).toMatchSnapshot();
+      const pr2 = await github.getBranchPr('somebranch');
+
+      expect(pr).toMatchSnapshot({ number: 90 });
+      expect(pr2).toEqual(pr);
     });
   });
 
@@ -1701,14 +1821,22 @@ describe('modules/platform/github/index', () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
-        .post('/graphql')
-        .reply(200, graphqlClosedPullRequests)
+        .get('/repos/some/repo/issues/2499/comments?per_page=100')
+        .reply(200, [
+          {
+            id: 419928791,
+            body: '[![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/renovatebot/renovate?pullRequest=2500) <br/>All committers have signed the CLA.',
+          },
+          {
+            id: 420006957,
+            body: ':tada: This PR is included in version 13.63.5 :tada:\n\nThe release is available on:\n- [npm package (@latest dist-tag)](https://www.npmjs.com/package/renovate)\n- [GitHub release](https://github.com/renovatebot/renovate/releases/tag/13.63.5)\n\nYour **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:',
+          },
+        ])
         .post('/repos/some/repo/issues/2499/comments')
         .reply(200);
       await github.initRepo({
         repository: 'some/repo',
       } as any);
-      await github.getClosedPrs();
 
       await expect(
         github.ensureComment({
@@ -1823,7 +1951,9 @@ describe('modules/platform/github/index', () => {
     it('returns true if no title and all state', async () => {
       const scope = httpMock
         .scope(githubApiHost)
-        .get('/repos/some/repo/pulls?per_page=100&state=all')
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
         .reply(200, [
           {
             number: 2,
@@ -1860,17 +1990,21 @@ describe('modules/platform/github/index', () => {
     });
 
     it('returns true if not open', async () => {
-      httpMock
-        .scope(githubApiHost)
-        .get('/repos/undefined/pulls?per_page=100&state=all')
+      const scope = httpMock.scope(githubApiHost);
+      initRepoMock(scope, 'some/repo');
+      scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
         .reply(200, [
           {
             number: 1,
-            head: { ref: 'branch-a' },
+            head: { ref: 'branch-a', repo: { full_name: 'some/repo' } },
             title: 'branch a pr',
             state: PrState.Closed,
           },
         ]);
+      await github.initRepo({ repository: 'some/repo' } as never);
 
       const res = await github.findPr({
         branchName: 'branch-a',
@@ -1880,18 +2014,24 @@ describe('modules/platform/github/index', () => {
     });
 
     it('caches pr list', async () => {
-      httpMock
-        .scope(githubApiHost)
-        .get('/repos/undefined/pulls?per_page=100&state=all')
+      const scope = httpMock.scope(githubApiHost);
+      initRepoMock(scope, 'some/repo');
+      scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
         .reply(200, [
           {
             number: 1,
-            head: { ref: 'branch-a' },
+            head: { ref: 'branch-a', repo: { full_name: 'some/repo' } },
             title: 'branch a pr',
             state: PrState.Open,
           },
         ]);
+      await github.initRepo({ repository: 'some/repo' } as never);
+
       let res = await github.findPr({ branchName: 'branch-a' });
+
       expect(res).toBeDefined();
       res = await github.findPr({
         branchName: 'branch-a',
@@ -2124,10 +2264,33 @@ describe('modules/platform/github/index', () => {
       expect(pr).toBeNull();
     });
 
-    it('should return PR from graphql result', async () => {
+    it('should return PR', async () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
-      scope.post('/graphql').reply(200, graphqlOpenPullRequests);
+      scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, [
+          {
+            number: 2499,
+            head: {
+              ref: 'renovate/delay-4.x',
+              repo: { full_name: 'some/repo' },
+            },
+            title: 'build(deps): update dependency delay to v4.0.1',
+            state: PrState.Closed,
+          },
+          {
+            number: 2500,
+            head: {
+              ref: 'renovate/jest-monorepo',
+              repo: { full_name: 'some/repo' },
+            },
+            state: PrState.Open,
+            title: 'chore(deps): update dependency jest to v23.6.0',
+          },
+        ]);
       await github.initRepo({
         repository: 'some/repo',
       } as any);
@@ -2136,31 +2299,67 @@ describe('modules/platform/github/index', () => {
       expect(pr).toMatchSnapshot();
     });
 
-    it('should return PR from closed graphql result', async () => {
+    it('should return closed PR', async () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
-        .post('/graphql')
-        .reply(200, graphqlOpenPullRequests)
-        .post('/graphql')
-        .reply(200, graphqlClosedPullRequests);
-      await github.initRepo({
-        repository: 'some/repo',
-      } as any);
-      const pr = await github.getPr(2499);
-      expect(pr).toBeDefined();
-      expect(pr).toMatchSnapshot();
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, [
+          {
+            number: 2500,
+            head: {
+              ref: 'renovate/jest-monorepo',
+              repo: { full_name: 'some/repo' },
+            },
+            title: 'chore(deps): update dependency jest to v23.6.0',
+            state: PrState.Closed,
+          },
+        ]);
+      await github.initRepo({ repository: 'some/repo' } as any);
+
+      const pr = await github.getPr(2500);
+
+      expect(pr).toMatchObject({ number: 2500, state: PrState.Closed });
+    });
+
+    it('should return merged PR', async () => {
+      const scope = httpMock.scope(githubApiHost);
+      initRepoMock(scope, 'some/repo');
+      scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, [
+          {
+            number: 2500,
+            head: {
+              ref: 'renovate/jest-monorepo',
+              repo: { full_name: 'some/repo' },
+            },
+            title: 'chore(deps): update dependency jest to v23.6.0',
+            state: PrState.Closed,
+            merged_at: DateTime.now().toISO(),
+          },
+        ]);
+      await github.initRepo({ repository: 'some/repo' } as any);
+
+      const pr = await github.getPr(2500);
+
+      expect(pr).toMatchObject({ number: 2500, state: PrState.Merged });
     });
 
     it('should return null if no PR is returned from GitHub', async () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, [])
         .get('/repos/some/repo/pulls/1234')
-        .reply(200)
-        .post('/graphql')
-        .twice()
-        .reply(404);
+        .reply(200);
       await github.initRepo({ repository: 'some/repo', token: 'token' } as any);
       const pr = await github.getPr(1234);
       expect(pr).toBeNull();
@@ -2170,6 +2369,10 @@ describe('modules/platform/github/index', () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, [])
         .get('/repos/some/repo/pulls/1234')
         .reply(200, {
           number: 1234,
@@ -2181,10 +2384,7 @@ describe('modules/platform/github/index', () => {
           labels: [{ name: 'foo' }, { name: 'bar' }],
           assignee: { login: 'foobar' },
           created_at: '01-01-2022',
-        })
-        .post('/graphql')
-        .twice()
-        .reply(404);
+        });
       await github.initRepo({
         repository: 'some/repo',
         token: 'token',
@@ -2197,6 +2397,10 @@ describe('modules/platform/github/index', () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, [])
         .get('/repos/some/repo/pulls/1234')
         .reply(200, {
           number: 1234,
@@ -2208,10 +2412,7 @@ describe('modules/platform/github/index', () => {
           title: 'Some title',
           assignees: [{ login: 'foo' }],
           requested_reviewers: [{ login: 'bar' }],
-        })
-        .post('/graphql')
-        .twice()
-        .reply(404);
+        });
       await github.initRepo({
         repository: 'some/repo',
         token: 'token',
@@ -2224,6 +2425,10 @@ describe('modules/platform/github/index', () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
       scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, [])
         .get('/repos/some/repo/pulls/1234')
         .reply(200, {
           number: 1234,
@@ -2232,10 +2437,7 @@ describe('modules/platform/github/index', () => {
           head: { ref: 'some/branch' },
           commits: 1,
           title: 'Some title',
-        })
-        .post('/graphql')
-        .twice()
-        .reply(404);
+        });
       await github.initRepo({
         repository: 'some/repo',
         token: 'token',
@@ -2247,32 +2449,32 @@ describe('modules/platform/github/index', () => {
 
   describe('updatePr(prNo, title, body)', () => {
     it('should update the PR', async () => {
+      const pr: UpdatePrConfig = {
+        number: 1234,
+        prTitle: 'The New Title',
+        prBody: 'Hello world again',
+      };
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
-      scope.patch('/repos/some/repo/pulls/1234').reply(200);
       await github.initRepo({ repository: 'some/repo', token: 'token' } as any);
-      await expect(
-        github.updatePr({
-          number: 1234,
-          prTitle: 'The New Title',
-          prBody: 'Hello world again',
-        })
-      ).toResolve();
+      scope.patch('/repos/some/repo/pulls/1234').reply(200, pr);
+
+      await expect(github.updatePr(pr)).toResolve();
     });
 
     it('should update and close the PR', async () => {
+      const pr: UpdatePrConfig = {
+        number: 1234,
+        prTitle: 'The New Title',
+        prBody: 'Hello world again',
+        state: PrState.Closed,
+      };
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
-      scope.patch('/repos/some/repo/pulls/1234').reply(200);
       await github.initRepo({ repository: 'some/repo', token: 'token' } as any);
-      await expect(
-        github.updatePr({
-          number: 1234,
-          prTitle: 'The New Title',
-          prBody: 'Hello world again',
-          state: PrState.Closed,
-        })
-      ).toResolve();
+      scope.patch('/repos/some/repo/pulls/1234').reply(200, pr);
+
+      await expect(github.updatePr(pr)).toResolve();
     });
   });
 
@@ -2280,20 +2482,33 @@ describe('modules/platform/github/index', () => {
     it('should merge the PR', async () => {
       const scope = httpMock.scope(githubApiHost);
       initRepoMock(scope, 'some/repo');
-      scope.put('/repos/some/repo/pulls/1234/merge').reply(200);
+      scope
+        .get(
+          '/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1'
+        )
+        .reply(200, [
+          {
+            number: 1234,
+            base: { sha: '1234' },
+            head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
+            state: PrState.Open,
+            title: 'Some PR',
+          },
+        ])
+        .put('/repos/some/repo/pulls/1234/merge')
+        .reply(200);
       await github.initRepo({ repository: 'some/repo', token: 'token' } as any);
-      const pr = {
-        number: 1234,
-        head: {
-          ref: 'someref',
-        },
-      };
-      expect(
-        await github.mergePr({
-          branchName: '',
-          id: pr.number,
-        })
-      ).toBeTrue();
+
+      const prBefore = await github.getPr(1234); // fetched remotely
+      const mergeResult = await github.mergePr({
+        id: 1234,
+        branchName: 'somebranch',
+      });
+      const prAfter = await github.getPr(1234); // obtained from cache
+
+      expect(mergeResult).toBeTrue();
+      expect(prBefore.state).toBe(PrState.Open);
+      expect(prAfter.state).toBe(PrState.Merged);
     });
 
     it('should handle merge error', async () => {
diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts
index ddc6384dc80f816a432a2d54a4413d8fbb597980..7abcb41866be347fccbdcc17e0a1a6e38299f991 100644
--- a/lib/modules/platform/github/index.ts
+++ b/lib/modules/platform/github/index.ts
@@ -54,28 +54,25 @@ import type {
   UpdatePrConfig,
 } from '../types';
 import { smartTruncate } from '../utils/pr-body';
-import { coerceGraphqlPr, coerceRestPr } from './common';
+import { coerceRestPr } from './common';
 import {
-  closedPrsQuery,
   enableAutoMergeMutation,
   getIssuesQuery,
-  openPrsQuery,
   repoInfoQuery,
   vulnerabilityAlertsQuery,
 } from './graphql';
 import { massageMarkdownLinks } from './massage-markdown-links';
+import { getPrCache } from './pr';
 import type {
   BranchProtection,
   CombinedBranchStatus,
   Comment,
   GhAutomergeResponse,
   GhBranchStatus,
-  GhGraphQlPr,
   GhRepo,
   GhRestPr,
   LocalRepoConfig,
   PlatformConfig,
-  PrList,
 } from './types';
 import { getUserDetails, getUserEmail } from './user';
 
@@ -359,10 +356,6 @@ export async function initRepo({
   // This shouldn't be necessary, but occasional strange errors happened until it was added
   config.issueList = null;
   config.prList = null;
-  config.openPrList = null;
-  config.closedPrList = null;
-  config.branchPrs = [];
-  config.prComments = {};
 
   config.forkMode = !!forkMode;
   if (forkMode) {
@@ -557,81 +550,28 @@ export async function getRepoForceRebase(): Promise<boolean> {
   return config.repoForceRebase;
 }
 
-export async function getClosedPrs(): Promise<PrList> {
-  if (!config.closedPrList) {
-    config.closedPrList = {};
-    try {
-      // prettier-ignore
-      const nodes = await githubApi.queryRepoField<GhGraphQlPr>(
-        closedPrsQuery,
-        'pullRequests',
-        {
-          variables: {
-            owner: config.repositoryOwner,
-            name: config.repositoryName,
-          },
-        }
-      );
-      const prNumbers: number[] = [];
-      // istanbul ignore if
-      if (!nodes?.length) {
-        logger.debug('getClosedPrs(): no graphql data');
-        return {};
-      }
-      for (const gqlPr of nodes) {
-        const pr = coerceGraphqlPr(gqlPr);
-        config.closedPrList[pr.number] = pr;
-        prNumbers.push(pr.number);
-        if (gqlPr.comments?.nodes) {
-          config.prComments[pr.number] = gqlPr.comments.nodes.map(
-            ({ databaseId: id, body }) => ({ id, body })
-          );
-        }
+function cachePr(pr?: Pr): void {
+  config.prList ??= [];
+  if (pr) {
+    for (let idx = 0; idx < config.prList.length; idx += 1) {
+      const cachedPr = config.prList[idx];
+      if (cachedPr.number === pr.number) {
+        config.prList[idx] = pr;
+        return;
       }
-      prNumbers.sort();
-      logger.debug({ prNumbers }, 'Retrieved closed PR list with graphql');
-    } catch (err) /* istanbul ignore next */ {
-      logger.warn({ err }, 'getClosedPrs(): error');
     }
+    config.prList.push(pr);
   }
-  return config.closedPrList;
 }
 
-async function getOpenPrs(): Promise<PrList> {
-  // The graphql query is supported in the current oldest GHE version 2.19
-  if (!config.openPrList) {
-    config.openPrList = {};
-    try {
-      // prettier-ignore
-      const nodes = await githubApi.queryRepoField<GhGraphQlPr>(
-        openPrsQuery,
-        'pullRequests',
-        {
-          variables: {
-            owner: config.repositoryOwner,
-            name: config.repositoryName,
-          },
-          acceptHeader: 'application/vnd.github.merge-info-preview+json',
-        }
-      );
-      const prNumbers: number[] = [];
-      // istanbul ignore if
-      if (!nodes?.length) {
-        logger.debug('getOpenPrs(): no graphql data');
-        return {};
-      }
-      for (const gqlPr of nodes) {
-        const pr = coerceGraphqlPr(gqlPr);
-        config.openPrList[pr.number] = pr;
-        prNumbers.push(pr.number);
-      }
-      prNumbers.sort();
-      logger.trace({ prNumbers }, 'Retrieved open PR list with graphql');
-    } catch (err) /* istanbul ignore next */ {
-      logger.warn({ err }, 'getOpenPrs(): error');
-    }
-  }
-  return config.openPrList;
+// Fetch fresh Pull Request and cache it when possible
+async function fetchPr(prNo: number): Promise<Pr | null> {
+  const { body: ghRestPr } = await githubApi.getJson<GhRestPr>(
+    `repos/${config.parentRepo || config.repository}/pulls/${prNo}`
+  );
+  const result = coerceRestPr(ghRestPr);
+  cachePr(result);
+  return result;
 }
 
 // Gets details for a PR
@@ -639,28 +579,13 @@ export async function getPr(prNo: number): Promise<Pr | null> {
   if (!prNo) {
     return null;
   }
-  const openPrs = await getOpenPrs();
-  const openPr = openPrs[prNo];
-  if (openPr) {
-    logger.debug('Returning from graphql open PR list');
-    return openPr;
-  }
-  const closedPrs = await getClosedPrs();
-  const closedPr = closedPrs[prNo];
-  if (closedPr) {
-    logger.debug('Returning from graphql closed PR list');
-    return closedPr;
+  const prList = await getPrList();
+  let pr = prList.find(({ number }) => number === prNo);
+  if (pr) {
+    logger.debug('Returning PR from cache');
   }
-  logger.debug(
-    { prNo },
-    'PR not found in open or closed PRs list - trying to fetch it directly'
-  );
-  const ghRestPr = (
-    await githubApi.getJson<GhRestPr>(
-      `repos/${config.parentRepo || config.repository}/pulls/${prNo}`
-    )
-  ).body;
-  return ghRestPr ? coerceRestPr(ghRestPr) : null;
+  pr ??= await fetchPr(prNo);
+  return pr;
 }
 
 function matchesState(state: string, desiredState: string): boolean {
@@ -674,35 +599,16 @@ function matchesState(state: string, desiredState: string): boolean {
 }
 
 export async function getPrList(): Promise<Pr[]> {
-  logger.trace('getPrList()');
   if (!config.prList) {
-    logger.debug('Retrieving PR list');
-    let prList: GhRestPr[];
-    try {
-      prList = (
-        await githubApi.getJson<GhRestPr[]>(
-          `repos/${
-            config.parentRepo || config.repository
-          }/pulls?per_page=100&state=all`,
-          { paginate: true }
-        )
-      ).body;
-    } catch (err) /* istanbul ignore next */ {
-      logger.debug({ err }, 'getPrList err');
-      throw new ExternalHostError(err, PlatformId.Github);
-    }
-    config.prList = prList
-      .filter(
-        (pr) =>
-          config.forkMode ||
-          config.ignorePrAuthor ||
-          (pr?.user?.login && config?.renovateUsername
-            ? pr.user.login === config.renovateUsername
-            : true)
-      )
-      .map(coerceRestPr);
-    logger.debug(`Retrieved ${config.prList.length} Pull Requests`);
+    const repo = config.parentRepo ?? config.repository;
+    const username =
+      !config.forkMode && !config.ignorePrAuthor && config.renovateUsername
+        ? config.renovateUsername
+        : null;
+    const prCache = await getPrCache(githubApi, repo, username);
+    config.prList = Object.values(prCache);
   }
+
   return config.prList;
 }
 
@@ -730,19 +636,16 @@ const REOPEN_THRESHOLD_MILLIS = 1000 * 60 * 60 * 24 * 7;
 
 // Returns the Pull Request for a branch. Null if not exists.
 export async function getBranchPr(branchName: string): Promise<Pr | null> {
-  // istanbul ignore if
-  if (config.branchPrs[branchName]) {
-    return config.branchPrs[branchName];
-  }
   logger.debug(`getBranchPr(${branchName})`);
+
   const openPr = await findPr({
     branchName,
     state: PrState.Open,
   });
   if (openPr) {
-    config.branchPrs[branchName] = await getPr(openPr.number);
-    return config.branchPrs[branchName];
+    return openPr;
   }
+
   const autoclosedPr = await findPr({
     branchName,
     state: PrState.Closed,
@@ -771,24 +674,26 @@ export async function getBranchPr(branchName: string): Promise<Pr | null> {
     }
     try {
       const title = autoclosedPr.title.replace(regEx(/ - autoclosed$/), '');
-      await githubApi.patchJson(`repos/${config.repository}/pulls/${number}`, {
-        body: {
-          state: 'open',
-          title,
-        },
-      });
+      const { body: ghPr } = await githubApi.patchJson<GhRestPr>(
+        `repos/${config.repository}/pulls/${number}`,
+        {
+          body: {
+            state: 'open',
+            title,
+          },
+        }
+      );
       logger.info(
         { branchName, title, number },
         'Successfully reopened autoclosed PR'
       );
+      const result = coerceRestPr(ghPr);
+      cachePr(result);
+      return result;
     } catch (err) {
       logger.debug('Could not reopen autoclosed PR');
       return null;
     }
-    delete config.openPrList; // So that it gets refreshed
-    delete config.closedPrList?.[number]; // So that it's no longer found in the closed list
-    config.branchPrs[branchName] = await getPr(number);
-    return config.branchPrs[branchName];
   }
   return null;
 }
@@ -1285,11 +1190,6 @@ async function deleteComment(commentId: number): Promise<void> {
 }
 
 async function getComments(issueNo: number): Promise<Comment[]> {
-  const cachedComments = config.prComments[issueNo];
-  if (cachedComments) {
-    logger.debug('Returning closed PR list comments');
-    return cachedComments;
-  }
   // GET /repos/:owner/:repo/issues/:number/comments
   logger.debug(`Getting comments for #${issueNo}`);
   const url = `repos/${
@@ -1472,25 +1372,22 @@ export async function createPr({
     options.body.maintainer_can_modify = true;
   }
   logger.debug({ title, head, base, draft: draftPR }, 'Creating PR');
-  const ghRestPr = (
+  const ghPr = (
     await githubApi.postJson<GhRestPr>(
       `repos/${config.parentRepo || config.repository}/pulls`,
       options
     )
   ).body;
   logger.debug(
-    { branch: sourceBranch, pr: ghRestPr.number, draft: draftPR },
+    { branch: sourceBranch, pr: ghPr.number, draft: draftPR },
     'PR created'
   );
-  const { node_id } = ghRestPr;
-  const pr = coerceRestPr(ghRestPr);
-  // istanbul ignore if
-  if (config.prList) {
-    config.prList.push(pr);
-  }
-  await addLabels(pr.number, labels);
-  await tryPrAutomerge(pr.number, node_id, platformOptions);
-  return pr;
+  const { number, node_id } = ghPr;
+  await addLabels(number, labels);
+  await tryPrAutomerge(number, node_id, platformOptions);
+  const result = coerceRestPr(ghPr);
+  cachePr(result);
+  return result;
 }
 
 export async function updatePr({
@@ -1516,10 +1413,12 @@ export async function updatePr({
     options.token = config.forkToken;
   }
   try {
-    await githubApi.patchJson(
+    const { body: ghPr } = await githubApi.patchJson<GhRestPr>(
       `repos/${config.parentRepo || config.repository}/pulls/${prNo}`,
       options
     );
+    const result = coerceRestPr(ghPr);
+    cachePr(result);
     logger.debug({ pr: prNo }, 'PR updated');
   } catch (err) /* istanbul ignore next */ {
     if (err instanceof ExternalHostError) {
@@ -1618,6 +1517,10 @@ export async function mergePr({
     { automergeResult: automergeResult.body, pr: prNo },
     'PR merged'
   );
+  const cachedPr = config.prList?.find(({ number }) => number === prNo);
+  if (cachedPr) {
+    cachePr({ ...cachedPr, state: PrState.Merged });
+  }
   return true;
 }
 
diff --git a/lib/modules/platform/github/pr.ts b/lib/modules/platform/github/pr.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c931e876727590f69074cf936a6cffbbcef71930
--- /dev/null
+++ b/lib/modules/platform/github/pr.ts
@@ -0,0 +1,140 @@
+import { PlatformId } from '../../../constants';
+import { logger } from '../../../logger';
+import { ExternalHostError } from '../../../types/errors/external-host-error';
+import { getCache } from '../../../util/cache/repository';
+import type { GithubHttp, GithubHttpOptions } from '../../../util/http/github';
+import { parseLinkHeader } from '../../../util/url';
+import type { Pr } from '../types';
+import { ApiCache } from './api-cache';
+import { coerceRestPr } from './common';
+import type { ApiPageCache, GhRestPr } from './types';
+
+function getPrApiCache(): ApiCache<GhRestPr> {
+  const repoCache = getCache();
+  repoCache.platform ??= {};
+  repoCache.platform.github ??= {};
+  repoCache.platform.github.prCache ??= { items: {} };
+  const prCache = new ApiCache(
+    repoCache.platform.github.prCache as ApiPageCache<GhRestPr>
+  );
+  return prCache;
+}
+
+/**
+ *  Fetch and return Pull Requests from GitHub repository:
+ *
+ *   1. Synchronize long-term cache.
+ *
+ *   2. Store items in raw format, i.e. exactly what
+ *      has been returned by GitHub REST API.
+ *
+ *   3. Convert items to the Renovate format and return.
+ *
+ * In order synchronize ApiCache properly, we handle 3 cases:
+ *
+ *   a. We never fetched PR list for this repo before.
+ *      This is detected by `etag` presense in the cache.
+ *
+ *      In this case, we're falling back to quick fetch via
+ *      `paginate=true` option (see `util/http/github.ts`).
+ *
+ *   b. None of PRs has changed since last run.
+ *
+ *      We detect this by setting `If-None-Match` HTTP header
+ *      with the `etag` value from the previous run.
+ *
+ *   c. Some of PRs had changed since last run.
+ *
+ *      In this case, we sequentially fetch page by page
+ *      until `ApiCache.coerce` function indicates that
+ *      no more fresh items can be found in the next page.
+ *
+ *      We expect to fetch just one page per run in average,
+ *      since it's rare to have more than 100 updated PRs.
+ */
+export async function getPrCache(
+  http: GithubHttp,
+  repo: string,
+  username: string | null
+): Promise<Record<number, Pr>> {
+  const prCache: Record<number, Pr> = {};
+  const prApiCache = getPrApiCache();
+
+  try {
+    let requestsTotal = 0;
+    let apiQuotaAffected = false;
+    let needNextPageFetch = true;
+    let needNextPageSync = true;
+
+    let pageIdx = 1;
+    while (needNextPageFetch && needNextPageSync) {
+      const urlPath = `repos/${repo}/pulls?per_page=100&state=all&sort=updated&direction=desc&page=${pageIdx}`;
+
+      const opts: GithubHttpOptions = { paginate: false };
+      if (pageIdx === 1) {
+        const oldEtag = prApiCache.etag;
+        if (oldEtag) {
+          opts.headers = { 'If-None-Match': oldEtag };
+        } else {
+          // Speed up initial fetch
+          opts.paginate = true;
+        }
+      }
+
+      const res = await http.getJson<GhRestPr[]>(urlPath, opts);
+      apiQuotaAffected = true;
+      requestsTotal += 1;
+
+      if (pageIdx === 1 && res.statusCode === 304) {
+        apiQuotaAffected = false;
+        break;
+      }
+
+      const {
+        headers: { link: linkHeader, etag: newEtag },
+      } = res;
+
+      let { body: page } = res;
+
+      if (username) {
+        page = page.filter(
+          (ghPr) => ghPr?.user?.login && ghPr.user.login === username
+        );
+      }
+
+      needNextPageSync = prApiCache.reconcile(page);
+      needNextPageFetch = !!parseLinkHeader(linkHeader)?.next;
+
+      if (pageIdx === 1) {
+        if (newEtag) {
+          prApiCache.etag = newEtag;
+        }
+
+        needNextPageFetch &&= !opts.paginate;
+      }
+
+      pageIdx += 1;
+    }
+
+    logger.debug(
+      {
+        pullsTotal: prApiCache.getItems().length,
+        requestsTotal,
+        apiQuotaAffected,
+      },
+      `getPrList success`
+    );
+  } catch (err) /* istanbul ignore next */ {
+    logger.debug({ err }, 'getPrList err');
+    throw new ExternalHostError(err, PlatformId.Github);
+  }
+
+  for (const ghPr of prApiCache.getItems()) {
+    const pr = coerceRestPr(ghPr);
+    if (pr) {
+      prCache[ghPr.number] = pr;
+    }
+  }
+
+  return prCache;
+}
diff --git a/lib/modules/platform/github/types.ts b/lib/modules/platform/github/types.ts
index 49e116295c4ca2dbf853248d43358a3f2eaf228e..d7a3058f06ad56ebcfa684284db446b461df5e43 100644
--- a/lib/modules/platform/github/types.ts
+++ b/lib/modules/platform/github/types.ts
@@ -29,10 +29,12 @@ export interface GhRestPr {
   mergeable_state: string;
   number: number;
   title: string;
+  body: string;
   state: string;
   merged_at: string;
   created_at: string;
   closed_at: string;
+  updated_at: string;
   user?: { login?: string };
   node_id: string;
   assignee?: { login?: string };
@@ -83,10 +85,7 @@ export interface LocalRepoConfig {
   parentRepo: string;
   forkMode?: boolean;
   forkToken?: string;
-  closedPrList: PrList | null;
-  openPrList: PrList | null;
-  prList: Pr[] | null;
-  prComments: Record<number, Comment[]>;
+  prList?: Pr[];
   issueList: any[] | null;
   mergeMethod: 'rebase' | 'squash' | 'merge';
   defaultBranch: string;
@@ -95,13 +94,11 @@ export interface LocalRepoConfig {
   renovateUsername: string;
   productLinks: any;
   ignorePrAuthor: boolean;
-  branchPrs: Pr[];
   autoMergeAllowed: boolean;
   hasIssuesEnabled: boolean;
 }
 
 export type BranchProtection = any;
-export type PrList = Record<number, Pr>;
 
 export interface GhRepo {
   isFork: boolean;
diff --git a/lib/util/cache/repository/types.ts b/lib/util/cache/repository/types.ts
index 9ad84ab219d28b1e7fc1b98a363f6b428fb0dea1..a9b5e0ada98bcac9c82f294145d8442436a8a84f 100644
--- a/lib/util/cache/repository/types.ts
+++ b/lib/util/cache/repository/types.ts
@@ -32,11 +32,6 @@ export interface BranchCache {
   upgrades: BranchUpgradeCache[];
 }
 
-export interface GithubGraphqlPageCache {
-  pageLastResizedAt: string;
-  pageSize: number;
-}
-
 export interface Cache {
   configFileName?: string;
   semanticCommits?: 'enabled' | 'disabled';
@@ -47,9 +42,7 @@ export interface Cache {
   scan?: Record<string, BaseBranchCache>;
   lastPlatformAutomergeFailure?: string;
   platform?: {
-    github?: {
-      graphqlPageCache?: Record<string, GithubGraphqlPageCache>;
-    };
+    github?: Record<string, unknown>;
   };
   gitConflicts?: GitConflictsCache;
   prComments?: Record<number, Record<string, string>>;
diff --git a/lib/util/http/github.spec.ts b/lib/util/http/github.spec.ts
index 7afd84ce9a6b125d9a31550f22d1f320f1e91b07..b537d3acaa6a52b1575931a982db352e84cec908 100644
--- a/lib/util/http/github.spec.ts
+++ b/lib/util/http/github.spec.ts
@@ -528,7 +528,7 @@ describe('util/http/github', () => {
       expect(items).toHaveLength(3);
 
       expect(
-        repoCache?.platform?.github?.graphqlPageCache?.testItem?.pageSize
+        repoCache?.platform?.github?.graphqlPageCache?.['testItem']?.pageSize
       ).toBe(25);
     });
 
@@ -556,7 +556,7 @@ describe('util/http/github', () => {
       const items = await githubApi.queryRepoField(graphqlQuery, 'testItem');
       expect(items).toHaveLength(3);
       expect(
-        repoCache?.platform?.github?.graphqlPageCache?.testItem?.pageSize
+        repoCache?.platform?.github?.graphqlPageCache?.['testItem']?.pageSize
       ).toBe(84);
     });
 
@@ -601,7 +601,7 @@ describe('util/http/github', () => {
       expect(items).toHaveLength(3);
 
       expect(
-        repoCache?.platform?.github?.graphqlPageCache?.testItem
+        repoCache?.platform?.github?.graphqlPageCache?.['testItem']
       ).toBeUndefined();
     });
 
diff --git a/lib/util/http/github.ts b/lib/util/http/github.ts
index ebad865829caf1092d5a30e87eff8c4a02b2bab1..aa39594e043293b2710598664cb5120f04d00b8a 100644
--- a/lib/util/http/github.ts
+++ b/lib/util/http/github.ts
@@ -181,12 +181,20 @@ function constructAcceptString(input?: any): string {
 
 const MAX_GRAPHQL_PAGE_SIZE = 100;
 
+interface GraphqlPageCacheItem {
+  pageLastResizedAt: string;
+  pageSize: number;
+}
+
+type GraphqlPageCache = Record<string, GraphqlPageCacheItem>;
+
 function getGraphqlPageSize(
   fieldName: string,
   defaultPageSize = MAX_GRAPHQL_PAGE_SIZE
 ): number {
   const cache = getCache();
-  const graphqlPageCache = cache?.platform?.github?.graphqlPageCache;
+  const graphqlPageCache = cache?.platform?.github
+    ?.graphqlPageCache as GraphqlPageCache;
   const cachedRecord = graphqlPageCache?.[fieldName];
 
   if (graphqlPageCache && cachedRecord) {
@@ -243,7 +251,9 @@ function setGraphqlPageSize(fieldName: string, newPageSize: number): void {
     cache.platform ??= {};
     cache.platform.github ??= {};
     cache.platform.github.graphqlPageCache ??= {};
-    cache.platform.github.graphqlPageCache[fieldName] = {
+    const graphqlPageCache = cache.platform.github
+      .graphqlPageCache as GraphqlPageCache;
+    graphqlPageCache[fieldName] = {
       pageLastResizedAt,
       pageSize: newPageSize,
     };