GenericPool
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 poolmaxSize- Size cap for the pool (use -1 for unlimited)expandSize- Size to expand the pool by when it runs out of objectsinsertFunction- callback function run when objects are insertedlocation(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()andRetrieveObjects() - Flexible pool access with methods that support both bulk operations and predicate functions
- Memory management in
Clear()andDestroy() - QOL methods like
ContainsObject(),RemoveObject(), andClone()
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
defaultstable 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 objectTemplatemust haveArchivableset totrue
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?) -> GenericPoolSetExpandSize: (self: GenericPool, expandSize: number?) -> GenericPoolSetLocation: (self: GenericPool, location: Instance?) -> GenericPool(Instance Pool only)SetInsertFunction: (self: GenericPool, insertFunction: ((any) -> ())?, applyFunction: boolean?) -> GenericPoolMakeGlobal: (self: GenericPool, globalName: string) -> GenericPoolMakeLocal: (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) -> booleanReturnObject: (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) -> GenericPoolInsertObjects: (self: GenericPool, objects: {[number]: any}) -> GenericPoolLoadObjects: (self: GenericPool, objectCount: number) -> GenericPoolRemoveObject: (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()andMakeGlobal()
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
expandSizeandSetExpandSize() - Implemented
expandSizein the retrieve methods - Implemented
insertFunctioninLoadObjects()andRetrieveObjects() - Reworked
LoadObjects()andRetrieveObject()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
insertSignalandremoveSignal - Added
WaitForObject()andWaitForObjects()
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.






