GenericPool | High-Performance Instance Pooling (Update 1.1!)

GenericPool

Instance.new() sucks, so use this instead :D


CreatorMarketplaceButton   DownloadButton


GenericPool is a comprehensive OOP ModuleScript for Instance pooling, aka Instance caching, built to efficiently manage thousands of objects at once. GenericPool objects are highly customizable and come with a range of utility methods designed to handle both common and niche use cases with ease.


If you’re unfamiliar, Instance pools temporarily store unused Instances that you plan to reuse later. While creating new Instances on demand is convenient, neither Instance.new() nor Instance:Destroy() scales well when used frequently, which creates significant overhead. Instance pools solve this issue by recycling existing Instances instead of constantly creating and destroying them. No matter what your project is, whether it’s a tycoon or a bullet hell, there is almost always a use for an Instance pool.

The purpose of GenericPool is to improve performance, and it’s optimized specifically for that. Any modern hardware can easily handle multiple GenericPool objects, each storing hundreds or even thousands of Instances, with negligible performance impact.

The module achieves these runtimes by leveraging numerous micro-optimizations and APIs, such as workspace:BulkMoveTo() for batch CFrame operations. It also handles memory usage effectively, allowing you to clear and destroy GenericPool objects at any time, ensuring unused pools don’t waste any resources.


Instance & Object Pooling:

GenericPool provides two separate ModuleScript variants, each optimized for pooling different object types.

The Instance Pool variant is designed for Instances with CFrames, such as Parts, Models, and Attachments. This variant uses workspace:BulkMoveTo() for batching CFrame operations and stores all Instances in the pool at the position CFrame.new(math.huge, math.huge, math.huge) and parented to a dedicated storage folder. This variant is not recommended for physics-based Instances.

The Object Pool variant works with any data type: non-CFrame Instances, primitive data types, or custom OOP objects. It doesn’t assume that objects in the pool have any specific properties, such as CFrame or Parent, making it much more versatile. If you’re unsure which variant to use, the Object Pool is definitely the most reliable of the two.

Configurable Instance Data:

When you create a GenericPool object, you can specify extra Instance data:

  • initialSize - Amount of objects to preload into the pool
  • maxSize - Size cap for the pool (use -1 for unlimited)
  • expandSize - Size to expand the pool by when it runs out of objects
  • insertFunction - callback function run when objects are inserted
  • location (Instance pools only) – The parent Instance for the pool folder

All of these values can be modified at any time. You can also register GenericPool objects globally within the local environment by calling GenericPool:MakeGlobal(name), enabling you to access them anywhere via GenericPool.globalPools[name].

Utility Methods:

Unlike most pooling modules that stop at GetPart() and ReturnPart(), GenericPool offers an expanded library of utility methods:

  • Safe Instance data setters that handle edge cases (e.g., auto-shrinking when maxSize is lowered)
  • Batch operations with methods like InsertObjects() and RetrieveObjects()
  • Flexible pool access with methods that support both bulk operations and predicate functions
  • Memory management in Clear() and Destroy()
  • QOL methods like ContainsObject(), RemoveObject(), and Clone()

QOL Features:

GenericPool also includes various quality-of-life features that make using the module more convenient:

  • Method chaining support
  • Comprehensive type checking
  • Error handling for specific methods
  • A built-in defaults table for easy configuration
  • Metamethods for __call(), __iter(), and __len()

Creating GenericPool Objects

Creating GenericPool Objects:

Since there are two variants of the module, each takes in a slightly different set of arguments. The Instance Pool variant accepts objectTemplate (a template Instance that the pool will clone), while the Object Pool variant accepts objectClass (the class or module of the object you want to pool) and objectInput (either constructor arguments, or a template object).

For the Instance Pool variant, simply pass in your template Instance, and the pool will clone it as needed. For the Object Pool variant, provide a class with a constructor at the index new for objectClass and a table of arguments for objectInput. If the class happens to have a Clone() method, you can pass in a template object for objectInput instead of the arguments table.

local InstancePool = require(scriptLocation.GenericPoolInstance)
local ObjectPool = require(scriptLocation.GenericPoolObject)

local templateInstance = Instance.new("Part")
local templateObjectClass = require(scriptLocation.exampleObjectClass) 
-- templateObjectClass.new() is a constructor

local exampleInstancePool = InstancePool.new(templateInstance, 100, 50, nil, workspace)

local exampleObjectPoolOne = ObjectPool.new(templateObjectClass, {argument1, argument2}, 0, -1, 50, nil)
local exampleObjectPoolTwo = ObjectPool.new(Instance, {"Part"}, 10, 250, 10, function(part)
    part.Transparency = 1 
end)

API:

  • Instance Pool: new: (objectTemplate: PVInstance?, initialSize: number?, maxSize: number?, expandSize: number?, location: Instance?, insertFunction: ((PVInstance) -> ())?) -> GenericPool
  • Object Pool: new: (objectClass: {new: (...any) -> any}?, objectInput: ({[number]: any} | any)?, initialSize: number?, maxSize: number?, expandSize: number?, insertFunction: ((any) -> ())?) -> GenericPool
  • objectTemplate must have Archivable set to true

Managing GenericPool Objects

Editing the Instance Data of GenericPool Objects:

All of the methods to edit Instance data are relatively simple, each usually taking a single argument: the new value to apply. Similar to the constructor, you can leave this argument as nil and the pool will fall back to the value in the defaults table instead. The only exception is SetInsertFunction(), which has an optional second parameter applyFunction that determines whether the new function is applied to objects already in the pool.

The SetMaxSize() method automatically handles edge cases. For example, if you set maxSize below the current pool size, excess objects are automatically removed and destroyed. SetLocation() is only available for the Instance Pool variant and changes the parent of the pool’s storage folder.

-- exampleObjectPoolOne has 0 objects, exampleObjectPoolTwo has 10 objects

exampleObjectPoolOne:SetMaxSize(10)
-- sets the max size to 10

exampleObjectPoolTwo:SetMaxSize(5)
-- shrinks the pool to 5 objects, destroying excess objects

exampleObjectPoolTwo:SetExpandSize(15)
-- the pool loads 15 new objects when it runs empty

exampleInstancePool:SetLocation(workspace.PoolStorage)
-- changes the parent of the pool folder (Instance Pool variant only)

exampleObjectPoolOne:SetInsertFunction(function(object) 
 object.Size = Vector3.new(1, 1, 1) 
end, true)
-- sets insert function and applies it to all existing objects

exampleObjectPoolTwo:SetInsertFunction(nil)
-- removes the insert function

Managing Global GenericPool Objects:

Managing global pools uses only two methods: MakeGlobal() and MakeLocal(). MakeGlobal() registers a pool globally within the local environment, and MakeLocal() unregisters it. Only MakeGlobal() takes an argument: the string name, which is used to access the signal via GenericPool.globalPools[name].

exampleObjectPoolOne:MakeGlobal("examplePool")
-- adds the pool to the `globalPools` table (access via GenericPool.globalPools["examplePool"])

exampleObjectPoolOne:MakeGlobal("otherName")
-- overrides the current global name

exampleObjectPoolOne:MakeLocal()
-- removes the pool from the `globalPools` table

API:

  • SetMaxSize: (self: GenericPool, maxSize: number?) -> GenericPool
  • SetExpandSize: (self: GenericPool, expandSize: number?) -> GenericPool
  • SetLocation: (self: GenericPool, location: Instance?) -> GenericPool (Instance Pool only)
  • SetInsertFunction: (self: GenericPool, insertFunction: ((any) -> ())?, applyFunction: boolean?) -> GenericPool
  • MakeGlobal: (self: GenericPool, globalName: string) -> GenericPool
  • MakeLocal: (self: GenericPool) -> GenericPool

Checking Pool Contents

Checking the contents of GenericPool Objects:

The “Return” methods allow you to inspect the contents of a pool without actually removing them. ReturnObject() returns the last (next) object in the pool without removing it, while ReturnObjects() returns a table of multiple objects. These methods each have a “Custom” version that accepts a predicate function to filter the results.

This specific group of methods also includes ContainsObject(), which checks whether a specific object is currently stored in the pool and returns a boolean value representing this.

-- exampleObjectPoolOne has 0 objects, exampleObjectPoolTwo has 5 objects

local objectOne = exampleObjectPoolOne:ReturnObject()
-- returns nil since the pool is empty

local objectTwo = exampleObjectPoolTwo:ReturnObject()
-- returns but doesn't remove the last item in the pool

local batch = exampleObjectPoolTwo:ReturnObjects(3)
-- returns but doesn't remove the last three items in the pool (in a table)

local custom = exampleObjectPoolTwo:ReturnCustomObject(function(object) 
    return object.Transparency == 1 
end)
-- returns the first object with transparency of 1, or nil if none exist

local customBatch = exampleObjectPoolTwo:ReturnCustomObjects(function(object)
 return object.Transparency == 1
end)
-- returns a table of all objects with transparency of 1, or nil if none exist

API:

  • ContainsObject: (self: GenericPool, object: any) -> boolean
  • ReturnObject: (self: GenericPool) -> any?
  • ReturnObjects: (self: GenericPool, objectCount: number) -> {[number]: any}?
  • ReturnCustomObject: (self: GenericPool, predicate: (any) -> boolean) -> any?
  • ReturnCustomObjects: (self: GenericPool, predicate: (any) -> boolean) -> {[number]: any}?

Managing Pool Contents

Retrieving from GenericPool Objects:

The “Retrieve” methods are similar to the return methods, but they also remove objects from the pool for use. RetrieveObject() removes and returns a single object, while RetrieveObjects() handles bulk retrieval. Both also have a “Custom” version like the return methods, and when the pool runs empty, these methods (not the custom versions) automatically create new objects using the value of expandSize.

For the Instance Pool variant, RetrieveObject() and RetrieveCustomObject() accept an optional cframe parameter that automatically positions the retrieved Instance at the specified CFrame.

-- exampleObjectPoolOne has 0 objects, exampleObjectPoolTwo has 10 objects, exampleInstancePool has 100 Instances

local object = exampleObjectPoolTwo:RetrieveObject()
-- removes and returns the last object in the pool

local batch = exampleObjectPoolTwo:RetrieveObjects(4)
-- removes and returns the last four objects in the pool (in a table)

local newObject = exampleObjectPoolTwo:RetrieveObject()
-- creates 15 new objects (expandSize), returns one, and stores the rest

local custom = exampleObjectPoolTwo:RetrieveCustomObject(function(object)
 return object.Transparency == 0
end)
-- removes and returns the first object with transparency of 0, or nil if none exist

-- Instance Pool variant only
local objectPositioned = exampleInstancePool:RetrieveObject(CFrame.new(0, 10, 0))
-- retrieves an Instance and positions it at (0, 10, 0)

local customPositioned = exampleInstancePool:RetrieveCustomObject(function(part)
 return part.Size == Vector3.new(4, 4, 4)
end, CFrame.new(15, 15, 15))
-- retrieves an Instance with size (4, 4, 4) and positions it at (15, 15, 15), or it does nothing if none exist

Inserting Into GenericPool Objects:

The “Insert” methods add objects to the pool. InsertObject() adds a single object, while InsertObjects() handles bulk insertion. Both methods respect the maxSize limit and will not add objects beyond the maximum capacity.

For the Instance Pool variant, inserted objects are automatically moved to the storage CFrame and parented to the pool folder if not already. For the Object Pool variant, objects that are Instances will be parented to nil.

local exampleObject = MyClass.new(arg1, arg2)

exampleObjectPoolOne:InsertObject(exampleObject)
-- adds `exampleObject` to the end of the pool

local exampleObjectBatch = {
  templateObjectClass.new(arg1, arg2),
  templateObjectClass.new(arg1, arg2),
  templateObjectClass.new(arg1, arg2)
}
exampleObjectPoolOne:InsertObjects(exampleObjectBatch )
-- adds all three objects to the end of the pool

local exampleInstance= templateInstance:Clone()
exampleInstance.Parent = workspace

exampleInstancePool:InsertObject(exampleInstance)
-- `exampleInstance` is moved to the storage CFrame and parented to the pool folder

Other Operations for Managing GenericPool Objects:

LoadObjects() preloads a specified number of new objects into the pool and also respects the maxSize limit. RemoveObject() searches for, and subsequently removes and destroys a specific object in the pool regardless of its position.

exampleInstancePool:LoadObjects(50)
-- creates and adds 50 new objects to the pool

exampleObjectPoolOne:RemoveObject(exampleObject)
-- removes and destroys `exampleObject` from the pool

API:

  • RetrieveObject: (self: GenericPool, cframe: CFrame?) -> any (cframe only for Instance Pool)
  • RetrieveObjects: (self: GenericPool, objectCount: number) -> {[number]: any}
  • RetrieveCustomObject: (self: GenericPool, predicate: (any) -> boolean, cframe: CFrame?) -> any? (cframe only for Instance Pool)
  • RetrieveCustomObjects: (self: GenericPool, predicate: (any) -> boolean) -> {[number]: any}?
  • InsertObject: (self: GenericPool, object: any) -> GenericPool
  • InsertObjects: (self: GenericPool, objects: {[number]: any}) -> GenericPool
  • LoadObjects: (self: GenericPool, objectCount: number) -> GenericPool
  • RemoveObject: (self: GenericPool, object: any) -> GenericPool

Other Operations with GenericPool Objects

Cloning GenericPool Objects:

Cloning is simple; all you do is call the method Clone(). This creates and returns a deep copy of the GenericPool object, if possible, cloning the objects currently in the pool. If the type of object stored in a pool is not cloneable, then new objects will be created to take their place in the clone.

local clonedPool = exampleInstancePool:Clone()
-- returns a deep copy of `exampleInstancePool`

clonedPool = exampleObjectPoolOne:Clone()
-- if the type of object stored in `exampleObjectPoolOne` is not cloneable, then new objects are created for the clone

Clearing & Destroying GenericPool Objects:

Clear() removes and destroys all objects from the pool but keeps the pool object itself intact. Destroy() removes all objects, destroys the pool’s storage folder (Instance Pool variant only), removes the pool from globalPools if applicable, and cleans up the OOP table. Neither method accepts any arguments.

exampleInstancePool:Clear()
-- removes all objects from the pool

exampleObjectPoolOne:Destroy()
-- fully destroys the pool and cleans up memory


Changelog:

Main Implementation

This is the main implementation of the ModuleScript, any other implementations are completely unnecessary and will have no documentation in the main post. This is probably a bit confusing, but it if want what you just read about in the post download the latest version of THIS implementation.

Version 1.0 (release)

Initial release of the ModuleScript

Version Date: 9/22/25
Version Store Page: GenericPool 1.0
Version Zip File: GenericPool Source

  • Hotfix #1 (9/28/25): Fixed Clone() in the Object Pool variant
  • Hotfix #2 (10/11/25): Fixed bugs in Clone() and MakeGlobal()

Version 1.1 (release)

Expand size, consistency changes, and Signal Implementation

Version Date: 10/11/25
Version Store Page: GenericPool 1.1
Version Zip File: GenericPool Source

Changes:

  • Added expandSize and SetExpandSize()
  • Implemented expandSize in the retrieve methods
  • Implemented insertFunction in LoadObjects() and RetrieveObjects()
  • Reworked LoadObjects() and RetrieveObject() in the Object Pool variant

Signal Implementation

This implementation adds signal capabilities and compatibility with GenericSignal. It includes events for insertions and deletions along with some new methods that use signal functionality.

Version 1.1 (release)

Insertion & deletion events and wait methods

Version Date: 10/11/25
Version Store Page: GenericPool 1.1 Signal Implementation
Version Zip File: GenericPool Signal Implementation Source

Changes:

  • Added insertSignal and removeSignal
  • Added WaitForObject() and WaitForObjects()

Most information on the ModuleScript can be found in this post, but if you have any questions, feel free to leave a reply, and I will usually respond within a day or two. Feedback and suggestions are also appreciated since I plan to keep updating this module.

18 Likes

Information:

Version Date: 10/11/25
Version Store Page: GenericPool 1.1
Version Zip File: GenericPool Source

GenericPool version 1.1 is now available! The main focuses of the update are the new expandSize feature, consistency improvements across both variants, and a Signal Implementation using GenericSignal for event handling.

Changes:

  • Added expandSize and SetExpandSize()
  • Implemented expandSize in the retrieve methods
  • Implemented insertFunction in LoadObjects() and RetrieveObjects()
  • Reworked LoadObjects() and RetrieveObject() in the Object Pool variant

If you find any bugs with this update, especially in the new signal implementation, please leave a reply so I can address them in the next hotfix. There is also a small hotfix for version 1.0 if you aren’t updating to this version yet.

2 Likes

Can you upload this to git? Would be easier to include this into projects with wally.

Would be helpful if you also specified license for the use of this work.

Sorry, I probably won’t be setting up git or wally soon since I haven’t really used them before (I plan to in the future or if it becomes necessary). As for the license, I don’t know which one it is but you can use this in whatever whenever you want to, no credit needed or anything.

Edit: MIT but like you don’t have to include the license on distribution

Edit Edit: I actually might set up Wally. I am doing a quick update of some of the formatting before I try it out, but it looked pretty simple so I can probably get it out in a week or so.

How does this compare to something like the ObjectCache module? They both seem to both be using the BulkMoveTo API.
It does definitely seem interesting with the QOL features, may start using this!
Later today, I’ll do a performance bench to compare, I’d guess they’d be around the same though.
(EDIT: Wrong reply, just treat it as replying to the initial post.)

Performance wise, they are almost exactly the same, I have personally benchmarked them before. Which one you choose is basically just personal preference, so if the QOL features are what you are looking for then this would probably be better than ObjectCache for you.

I definitely want to give you credit once I learn how to use this! This script is amazing, theres no downsides to pooling, only benefits. I know it’s far beyond anything I could make myself for what I’m working on. :upside_down_face:

1 Like