diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 90bc2cfcc1..7f21436eb9 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -461,6 +461,8 @@ export declare function getFullApAccount(username: string, host?: string | undef
 
 export declare function getImageSizeFromUrl(url: string): Promise<ImageSize>
 
+export declare function getInternalActor(actor: InternalActor): Promise<User>
+
 export declare function getNoteSummary(fileIds: Array<string>, text: string | undefined | null, cw: string | undefined | null, hasPoll: boolean): string
 
 export declare function getTimestamp(id: string): number
@@ -542,6 +544,9 @@ export interface Instance {
   faviconUrl: string | null
 }
 
+export type InternalActor =  'instance'|
+'relay';
+
 /**
  * Checks if a server is allowlisted.
  * Returns `Ok(true)` if private mode is disabled.
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index e45060a513..139739ac00 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -386,6 +386,7 @@ module.exports.genId = nativeBinding.genId
 module.exports.genIdAt = nativeBinding.genIdAt
 module.exports.getFullApAccount = nativeBinding.getFullApAccount
 module.exports.getImageSizeFromUrl = nativeBinding.getImageSizeFromUrl
+module.exports.getInternalActor = nativeBinding.getInternalActor
 module.exports.getNoteSummary = nativeBinding.getNoteSummary
 module.exports.getTimestamp = nativeBinding.getTimestamp
 module.exports.greet = nativeBinding.greet
@@ -393,6 +394,7 @@ module.exports.hashPassword = nativeBinding.hashPassword
 module.exports.HOUR = nativeBinding.HOUR
 module.exports.Inbound = nativeBinding.Inbound
 module.exports.initializeRustLogger = nativeBinding.initializeRustLogger
+module.exports.InternalActor = nativeBinding.InternalActor
 module.exports.isAllowedServer = nativeBinding.isAllowedServer
 module.exports.isBlockedServer = nativeBinding.isBlockedServer
 module.exports.isOldPasswordAlgorithm = nativeBinding.isOldPasswordAlgorithm
diff --git a/packages/backend-rs/src/federation/acct.rs b/packages/backend-rs/src/federation/acct.rs
index 816e75baf2..60c9e6f25c 100644
--- a/packages/backend-rs/src/federation/acct.rs
+++ b/packages/backend-rs/src/federation/acct.rs
@@ -8,7 +8,7 @@ pub struct Acct {
 }
 
 #[derive(thiserror::Error, Debug)]
-#[doc = "Error type to indicate a string-to-[`Acct`] conversion failure"]
+#[doc = "Error type to indicate a [`String`]-to-[`Acct`] conversion failure"]
 #[error("failed to convert string '{0}' into acct")]
 pub struct InvalidAcctString(String);
 
diff --git a/packages/backend-rs/src/federation/internal_actor/cache.rs b/packages/backend-rs/src/federation/internal_actor/cache.rs
new file mode 100644
index 0000000000..0aeacee660
--- /dev/null
+++ b/packages/backend-rs/src/federation/internal_actor/cache.rs
@@ -0,0 +1,85 @@
+//! In-memory internal actor cache handler
+
+// TODO: refactoring
+
+use super::*;
+use crate::{database::db_conn, model::entity::user};
+use sea_orm::prelude::*;
+use std::sync::{Arc, Mutex};
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[error(transparent)]
+    #[doc = "database error"]
+    Db(#[from] DbErr),
+    #[error("{} does not exist", Acct::from(.0.to_owned()))]
+    #[doc = "internal actor does not exist"]
+    InternalActorNotFound(InternalActor),
+}
+
+static INSTANCE_ACTOR: Mutex<Option<Arc<user::Model>>> = Mutex::new(None);
+static RELAY_ACTOR: Mutex<Option<Arc<user::Model>>> = Mutex::new(None);
+
+fn set_instance_actor(value: Arc<user::Model>) {
+    let _ = INSTANCE_ACTOR.lock().map(|mut cache| *cache = Some(value));
+}
+fn set_relay_actor(value: Arc<user::Model>) {
+    let _ = RELAY_ACTOR.lock().map(|mut cache| *cache = Some(value));
+}
+
+async fn cache_instance_actor() -> Result<Arc<user::Model>, Error> {
+    let actor = user::Entity::find()
+        .filter(user::Column::Username.eq(INSTANCE_ACTOR_USERNAME))
+        .filter(user::Column::Host.is_null())
+        .one(db_conn().await?)
+        .await?;
+
+    if let Some(actor) = actor {
+        let arc = Arc::new(actor);
+        set_instance_actor(arc.clone());
+        Ok(arc)
+    } else {
+        Err(Error::InternalActorNotFound(InternalActor::Instance))
+    }
+}
+async fn cache_relay_actor() -> Result<Arc<user::Model>, Error> {
+    let actor = user::Entity::find()
+        .filter(user::Column::Username.eq(RELAY_ACTOR_USERNAME))
+        .filter(user::Column::Host.is_null())
+        .one(db_conn().await?)
+        .await?;
+
+    if let Some(actor) = actor {
+        let arc = Arc::new(actor);
+        set_relay_actor(arc.clone());
+        Ok(arc)
+    } else {
+        Err(Error::InternalActorNotFound(InternalActor::Relay))
+    }
+}
+
+// for napi export
+// https://github.com/napi-rs/napi-rs/issues/2060
+type User = user::Model;
+
+#[macros::export(js_name = "getInternalActor")]
+pub async fn get(actor: InternalActor) -> Result<Arc<User>, Error> {
+    match actor {
+        InternalActor::Instance => {
+            if let Some(cache) = INSTANCE_ACTOR.lock().ok().and_then(|cache| cache.clone()) {
+                tracing::debug!("Using cached instance.actor");
+                return Ok(cache);
+            }
+            tracing::debug!("Caching instance.actor");
+            cache_instance_actor().await
+        }
+        InternalActor::Relay => {
+            if let Some(cache) = RELAY_ACTOR.lock().ok().and_then(|cache| cache.clone()) {
+                tracing::debug!("Using cached relay.actor");
+                return Ok(cache);
+            }
+            tracing::debug!("Caching relay.actor");
+            cache_relay_actor().await
+        }
+    }
+}
diff --git a/packages/backend-rs/src/federation/internal_actor/mod.rs b/packages/backend-rs/src/federation/internal_actor/mod.rs
new file mode 100644
index 0000000000..6996062b16
--- /dev/null
+++ b/packages/backend-rs/src/federation/internal_actor/mod.rs
@@ -0,0 +1,34 @@
+mod cache;
+
+pub use cache::get;
+
+use super::acct::Acct;
+
+#[derive(Debug)]
+#[macros::derive_clone_and_export(string_enum = "lowercase")]
+pub enum InternalActor {
+    Instance,
+    Relay,
+}
+
+const INSTANCE_ACTOR_USERNAME: &str = "instance.actor";
+const RELAY_ACTOR_USERNAME: &str = "relay.actor";
+
+// TODO: When `std::mem::variant_count` is stabilized, use
+// it to count system actors instead of hard coding the magic number
+pub const INTERNAL_ACTORS: u64 = 2;
+
+impl From<InternalActor> for Acct {
+    fn from(actor: InternalActor) -> Self {
+        match actor {
+            InternalActor::Instance => Acct {
+                username: INSTANCE_ACTOR_USERNAME.to_owned(),
+                host: None,
+            },
+            InternalActor::Relay => Acct {
+                username: RELAY_ACTOR_USERNAME.to_owned(),
+                host: None,
+            },
+        }
+    }
+}
diff --git a/packages/backend-rs/src/federation/mod.rs b/packages/backend-rs/src/federation/mod.rs
index 722a91abdf..1db28b0858 100644
--- a/packages/backend-rs/src/federation/mod.rs
+++ b/packages/backend-rs/src/federation/mod.rs
@@ -1,4 +1,5 @@
 //! Services used to federate with other servers
 
 pub mod acct;
+pub mod internal_actor;
 pub mod nodeinfo;
diff --git a/packages/backend-rs/src/misc/user/count.rs b/packages/backend-rs/src/misc/user/count.rs
index 8f17a71bc5..70353ef018 100644
--- a/packages/backend-rs/src/misc/user/count.rs
+++ b/packages/backend-rs/src/misc/user/count.rs
@@ -1,18 +1,12 @@
-use crate::model::entity::user;
+use crate::{federation::internal_actor::INTERNAL_ACTORS, model::entity::user};
 use sea_orm::prelude::*;
 
-// TODO: When `std::mem::variant_count` is stabilized, use
-// it to count system actors instead of hard coding the magic number
-
-// @instance.actor and @relay.actor are not real users
-const NUMBER_OF_SYSTEM_ACTORS: u64 = 2;
-
 pub async fn local_total(db: &DbConn) -> Result<u64, DbErr> {
     user::Entity::find()
         .filter(user::Column::Host.is_null())
         .count(db)
         .await
-        .map(|count| count - NUMBER_OF_SYSTEM_ACTORS)
+        .map(|count| count - INTERNAL_ACTORS)
 }
 
 #[macros::ts_export(js_name = "countLocalUsers")]
diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts
index abb9c2b2a4..fab913f869 100644
--- a/packages/backend/src/remote/activitypub/resolver.ts
+++ b/packages/backend/src/remote/activitypub/resolver.ts
@@ -1,8 +1,8 @@
 import { config } from "@/config.js";
 import type { ILocalUser } from "@/models/entities/user.js";
-import { getInstanceActor } from "@/services/instance-actor.js";
 import {
 	extractHost,
+	getInternalActor,
 	isAllowedServer,
 	isBlockedServer,
 	isSelfHost,
@@ -112,7 +112,7 @@ export default class Resolver {
 		}
 
 		if (!this.user) {
-			this.user = await getInstanceActor();
+			this.user = await getInternalActor("instance");
 		}
 
 		apLogger.info(
@@ -158,7 +158,7 @@ export default class Resolver {
 		if (!parsed.local) throw new Error("resolveLocal: not local");
 
 		switch (parsed.type) {
-			case "notes":
+			case "notes": {
 				const note = await Notes.findOneByOrFail({ id: parsed.id });
 				if (parsed.rest === "activity") {
 					// this refers to the create activity and not the note itself
@@ -166,20 +166,24 @@ export default class Resolver {
 				} else {
 					return renderNote(note);
 				}
-			case "users":
+			}
+			case "users": {
 				const user = await Users.findOneByOrFail({ id: parsed.id });
 				return await renderPerson(user as ILocalUser);
-			case "questions":
+			}
+			case "questions": {
 				// Polls are indexed by the note they are attached to.
 				const [pollNote, poll] = await Promise.all([
 					Notes.findOneByOrFail({ id: parsed.id }),
 					Polls.findOneByOrFail({ noteId: parsed.id }),
 				]);
 				return await renderQuestion({ id: pollNote.userId }, pollNote, poll);
-			case "likes":
+			}
+			case "likes": {
 				const reaction = await NoteReactions.findOneByOrFail({ id: parsed.id });
 				return renderActivity(renderLike(reaction, { uri: null }));
-			case "follows":
+			}
+			case "follows": {
 				// if rest is a <followee id>
 				if (parsed.rest != null && /^\w+$/.test(parsed.rest)) {
 					const [follower, followee] = await Promise.all(
@@ -207,6 +211,7 @@ export default class Resolver {
 					throw new Error("resolveLocal: invalid follow URI");
 				}
 				return renderActivity(renderFollow(follower, followee, url));
+			}
 			default:
 				throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
 		}
diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index 0d04ded58b..7b05ebffd0 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -9,7 +9,7 @@ import renderKey from "@/remote/activitypub/renderer/key.js";
 import { renderPerson } from "@/remote/activitypub/renderer/person.js";
 import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
 import { inbox as processInbox } from "@/queue/index.js";
-import { fetchMeta, isSelfHost } from "backend-rs";
+import { fetchMeta, getInternalActor, isSelfHost } from "backend-rs";
 import {
 	Notes,
 	Users,
@@ -24,7 +24,6 @@ import {
 	checkFetch,
 	getSignatureUser,
 } from "@/remote/activitypub/check-fetch.js";
-import { getInstanceActor } from "@/services/instance-actor.js";
 import renderFollow from "@/remote/activitypub/renderer/follow.js";
 import Featured from "./activitypub/featured.js";
 import Following from "./activitypub/following.js";
@@ -296,7 +295,7 @@ router.get("/users/:user/collections/featured", Featured);
 
 // publickey
 router.get("/users/:user/publickey", async (ctx) => {
-	const instanceActor = await getInstanceActor();
+	const instanceActor = await getInternalActor("instance");
 	if (ctx.params.user === instanceActor.id) {
 		ctx.body = renderActivity(
 			renderKey(instanceActor, await getUserKeypair(instanceActor.id)),
@@ -360,7 +359,7 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
 router.get("/users/:user", async (ctx, next) => {
 	if (!isActivityPubReq(ctx)) return await next();
 
-	const instanceActor = await getInstanceActor();
+	const instanceActor = await getInternalActor("instance");
 	if (ctx.params.user === instanceActor.id) {
 		await userInfo(ctx, instanceActor);
 		return;
@@ -387,7 +386,7 @@ router.get("/@:user", async (ctx, next) => {
 	if (!isActivityPubReq(ctx)) return await next();
 
 	if (ctx.params.user === "instance.actor") {
-		const instanceActor = await getInstanceActor();
+		const instanceActor = await getInternalActor("instance");
 		await userInfo(ctx, instanceActor);
 		return;
 	}
@@ -407,8 +406,8 @@ router.get("/@:user", async (ctx, next) => {
 	await userInfo(ctx, user);
 });
 
-router.get("/actor", async (ctx, next) => {
-	const instanceActor = await getInstanceActor();
+router.get("/actor", async (ctx, _next) => {
+	const instanceActor = await getInternalActor("instance");
 	await userInfo(ctx, instanceActor);
 });
 //#endregion
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index a94687cf40..1104a591c8 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -1,6 +1,6 @@
 import define from "@/server/api/define.js";
 import { AbuseUserReports, Users } from "@/models/index.js";
-import { getInstanceActor } from "@/services/instance-actor.js";
+import { getInternalActor } from "backend-rs";
 import { deliver } from "@/queue/index.js";
 import { renderActivity } from "@/remote/activitypub/renderer/index.js";
 import { renderFlag } from "@/remote/activitypub/renderer/flag.js";
@@ -29,7 +29,7 @@ export default define(meta, paramDef, async (ps, me) => {
 	}
 
 	if (ps.forward && report.targetUserHost != null) {
-		const actor = await getInstanceActor();
+		const actor = await getInternalActor("instance");
 		const targetUser = await Users.findOneByOrFail({ id: report.targetUserId });
 
 		deliver(
diff --git a/packages/backend/src/services/instance-actor.ts b/packages/backend/src/services/instance-actor.ts
deleted file mode 100644
index 8bdcc1d36d..0000000000
--- a/packages/backend/src/services/instance-actor.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { ILocalUser } from "@/models/entities/user.js";
-import { Users } from "@/models/index.js";
-import { Cache } from "@/misc/cache.js";
-import { IsNull } from "typeorm";
-
-const ACTOR_USERNAME = "instance.actor" as const;
-
-const cache = new Cache<ILocalUser>("instanceActor", 60 * 30);
-
-export async function getInstanceActor(): Promise<ILocalUser> {
-	const cached = await cache.get(null, true);
-	if (cached) return cached;
-
-	const user = (await Users.findOneBy({
-		host: IsNull(),
-		username: ACTOR_USERNAME,
-	})) as ILocalUser;
-
-	await cache.set(null, user);
-	return user;
-}
diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts
index cbc90e25ac..90e217c2e9 100644
--- a/packages/backend/src/services/relay.ts
+++ b/packages/backend/src/services/relay.ts
@@ -1,4 +1,3 @@
-import { IsNull } from "typeorm";
 import { renderFollowRelay } from "@/remote/activitypub/renderer/follow-relay.js";
 import {
 	renderActivity,
@@ -6,25 +5,14 @@ import {
 } from "@/remote/activitypub/renderer/index.js";
 import renderUndo from "@/remote/activitypub/renderer/undo.js";
 import { deliver } from "@/queue/index.js";
-import type { ILocalUser, User } from "@/models/entities/user.js";
-import { Users, Relays } from "@/models/index.js";
-import { genId } from "backend-rs";
+import type { User } from "@/models/entities/user.js";
+import { Relays } from "@/models/index.js";
+import { getInternalActor, genId } from "backend-rs";
 import { Cache } from "@/misc/cache.js";
 import type { Relay } from "@/models/entities/relay.js";
 
-const ACTOR_USERNAME = "relay.actor" as const;
-
 const relaysCache = new Cache<Relay[]>("relay", 60 * 60);
 
-export async function getRelayActor(): Promise<ILocalUser> {
-	const user = await Users.findOneBy({
-		host: IsNull(),
-		username: ACTOR_USERNAME,
-	});
-
-	return user as ILocalUser;
-}
-
 export async function addRelay(inbox: string) {
 	const relay = await Relays.insert({
 		id: genId(),
@@ -32,7 +20,7 @@ export async function addRelay(inbox: string) {
 		status: "requesting",
 	}).then((x) => Relays.findOneByOrFail(x.identifiers[0]));
 
-	const relayActor = await getRelayActor();
+	const relayActor = await getInternalActor("relay");
 	const follow = renderFollowRelay(relay, relayActor);
 	const activity = renderActivity(follow);
 	deliver(relayActor, activity, relay.inbox);
@@ -49,7 +37,7 @@ export async function removeRelay(inbox: string) {
 		throw new Error("relay not found");
 	}
 
-	const relayActor = await getRelayActor();
+	const relayActor = await getInternalActor("relay");
 	const follow = renderFollowRelay(relay, relayActor);
 	const undo = renderUndo(follow, relayActor);
 	const activity = renderActivity(undo);