diff --git a/src/d_think.h b/src/d_think.h
index efc1589bf62e277a67ab309f05d33d66af741865..58912458783a2f3b2187e50ebaf00a667a0e0a78 100644
--- a/src/d_think.h
+++ b/src/d_think.h
@@ -51,6 +51,7 @@ typedef struct thinker_s
 	// killough 11/98: count of how many other objects reference
 	// this one using pointers. Used for garbage collection.
 	INT32 references;
+	boolean cachable;
 
 #ifdef PARANOIA
 	INT32 debug_mobjtype;
diff --git a/src/p_local.h b/src/p_local.h
index 2d97f19da9f5d1bca3979cc64ff428d8996dcce7..454c39d5f17e178f664274fb7295ff347822543e 100644
--- a/src/p_local.h
+++ b/src/p_local.h
@@ -71,6 +71,7 @@ typedef enum
 	NUM_THINKERLISTS
 } thinklistnum_t; /**< Thinker lists. */
 extern thinker_t thlist[];
+extern mobj_t *mobjcache;
 
 void P_InitThinkers(void);
 void P_AddThinker(const thinklistnum_t n, thinker_t *thinker);
diff --git a/src/p_mobj.c b/src/p_mobj.c
index 6658cdfcc69f70621e14dee7570e30641557bd4e..c94bd51f215afe141e525300d6b7671fbca03656 100644
--- a/src/p_mobj.c
+++ b/src/p_mobj.c
@@ -45,6 +45,8 @@ actioncache_t actioncachehead;
 
 static mobj_t *overlaycap = NULL;
 
+mobj_t *mobjcache = NULL;
+
 void P_InitCachedActions(void)
 {
 	actioncachehead.prev = actioncachehead.next = &actioncachehead;
@@ -10787,7 +10789,16 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type, ...)
 	if (type == MT_NULL)
 		return NULL;
 
-	mobj = Z_Calloc(sizeof (*mobj), PU_LEVEL, NULL);
+	if (mobjcache != NULL)
+	{
+		mobj = mobjcache;
+		mobjcache = mobjcache->hnext;
+		memset(mobj, 0, sizeof(*mobj));
+	}
+	else
+	{
+		mobj = Z_Calloc(sizeof (*mobj), PU_LEVEL, NULL);
+	}
 
 	// this is officially a mobj, declared as soon as possible.
 	mobj->thinker.function.acp1 = (actionf_p1)P_MobjThinker;
@@ -11402,7 +11413,9 @@ void P_RemoveMobj(mobj_t *mobj)
 		INT32 prevreferences;
 		if (!mobj->thinker.references)
 		{
-			Z_Free(mobj); // No refrrences? Can be removed immediately! :D
+			// no references, dump it directly in the mobj cache
+			mobj->hnext = mobjcache;
+			mobjcache = mobj;
 			return;
 		}
 
diff --git a/src/p_setup.c b/src/p_setup.c
index 442db943091904fff17f6eeefcb2368fed0ef757..6f5ec17dcd59814ddae0032911704c3c9e4f6b16 100644
--- a/src/p_setup.c
+++ b/src/p_setup.c
@@ -7876,6 +7876,7 @@ boolean P_LoadLevel(boolean fromnetsave, boolean reloadinggamestate)
 	Patch_FreeTag(PU_PATCH_LOWPRIORITY);
 	Patch_FreeTag(PU_PATCH_ROTATED);
 	Z_FreeTags(PU_LEVEL, PU_PURGELEVEL - 1);
+	mobjcache = NULL;
 
 	R_InitializeLevelInterpolators();
 
diff --git a/src/p_tick.c b/src/p_tick.c
index c6832bfc3ee90d3ee0bc5bd1cd1e13e572bd753f..dca806ebee2810b5556f8cf6d288c7ffd1607069 100644
--- a/src/p_tick.c
+++ b/src/p_tick.c
@@ -217,6 +217,7 @@ void P_AddThinker(const thinklistnum_t n, thinker_t *thinker)
 	thlist[n].prev = thinker;
 
 	thinker->references = 0;    // killough 11/98: init reference counter to 0
+	thinker->cachable = n == THINK_MOBJ;
 
 #ifdef PARANOIA
 	thinker->debug_mobjtype = MT_NULL;
@@ -320,7 +321,16 @@ void P_RemoveThinkerDelayed(thinker_t *thinker)
 	(next->prev = currentthinker = thinker->prev)->next = next;
 
 	R_DestroyLevelInterpolators(thinker);
-	Z_Free(thinker);
+	if (thinker->cachable)
+	{
+		// put cachable thinkers in the mobj cache, so we can avoid allocations
+		((mobj_t *)thinker)->hnext = mobjcache;
+		mobjcache = (mobj_t *)thinker;
+	}
+	else
+	{
+		Z_Free(thinker);
+	}
 }
 
 //