Quick answer: Pooled objects retain all their state from previous use. You must explicitly reset every mutable field — velocity, position, active effects, animation state, coroutines — when an object is retrieved from the pool. Use Unity’s built-in ObjectPool<T> with OnGet and OnRelease callbacks to enforce consistent resets.

Object pooling is one of the first performance optimizations most Unity developers learn. Instead of calling Instantiate and Destroy every time a bullet fires or an enemy spawns, you recycle existing GameObjects from a pool. It eliminates GC spikes and reduces frame hitches. But pooling introduces a class of bugs that are genuinely difficult to diagnose: objects that come back from the pool carrying state from their previous life.

The Symptoms

Stale pool state manifests in ways that can look like completely unrelated bugs:

These bugs are maddening because they are intermittent. They depend on what the previous user of that particular pool slot did with the object.

The Root Cause

When you deactivate a GameObject and store it in a pool, Unity does not reset anything. The Rigidbody keeps its velocity. The Animator stays on whatever state it was in. Particle systems retain their emission state. Every field on every component remains exactly as it was. The pool is not a factory — it is a shelf, and whatever you put on the shelf is what you get back.

Most custom pool implementations look something like this:

// Naive pool - no reset logic
public GameObject Get()
{
    if (_pool.Count > 0)
    {
        GameObject obj = _pool.Dequeue();
        obj.SetActive(true);
        return obj; // Still has old state!
    }
    return Instantiate(_prefab);
}

public void Release(GameObject obj)
{
    obj.SetActive(false);
    _pool.Enqueue(obj);
}

The problem is clear: Get hands back an object with whatever state it accumulated during its last lifetime, and Release just hides it without cleaning up.

The Fix: IPoolable Interface + Callbacks

The cleanest pattern is to define a reset contract that every poolable object must implement:

public interface IPoolable
{
    void OnGetFromPool();
    void OnReturnToPool();
}

public class Projectile : MonoBehaviour, IPoolable
{
    private Rigidbody _rb;
    private TrailRenderer _trail;
    private ParticleSystem _impactFx;

    public void OnGetFromPool()
    {
        // Reset physics
        _rb.linearVelocity = Vector3.zero;
        _rb.angularVelocity = Vector3.zero;

        // Clear visual artifacts
        _trail.Clear();
        _impactFx.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);

        // Reset game state
        _hasHit = false;
        _lifetime = 0f;
    }

    public void OnReturnToPool()
    {
        // Stop any running coroutines
        StopAllCoroutines();

        // Ensure no lingering physics forces
        _rb.linearVelocity = Vector3.zero;
        _rb.angularVelocity = Vector3.zero;
    }
}

Using Unity’s Built-in ObjectPool<T>

Since Unity 2021.1, the UnityEngine.Pool namespace provides ObjectPool<T>, which has built-in callback hooks for exactly this purpose. Here is how to wire it up:

using UnityEngine.Pool;

public class ProjectilePool : MonoBehaviour
{
    [SerializeField] private Projectile _prefab;
    private ObjectPool<Projectile> _pool;

    void Awake()
    {
        _pool = new ObjectPool<Projectile>(
            createFunc:      () => Instantiate(_prefab),
            actionOnGet:     p  => { p.gameObject.SetActive(true);  p.OnGetFromPool(); },
            actionOnRelease: p  => { p.OnReturnToPool(); p.gameObject.SetActive(false); },
            actionOnDestroy: p  => Destroy(p.gameObject),
            defaultCapacity: 20,
            maxSize:         100
        );
    }

    public Projectile Get() => _pool.Get();
    public void Release(Projectile p) => _pool.Release(p);
}

The key insight is the ordering in actionOnRelease: call your cleanup logic before deactivating the object. Some cleanup operations (like stopping particle systems) behave differently on inactive GameObjects.

The Reset Checklist

Every poolable object should reset at minimum:

Common Pitfall: TrailRenderer Ghosts

Trail renderers deserve special mention because they are the most visibly broken component in pooled objects. When you retrieve a pooled projectile and move it to its new spawn position, the trail draws a line from wherever the object was last used to its new position — producing a visual “ghost trail” that streaks across the screen for a single frame.

The fix is to clear the trail after setting the new position:

Projectile p = _pool.Get();
p.transform.position = spawnPoint;
p.GetComponent<TrailRenderer>().Clear();

Alternatively, you can disable the trail renderer on release, set the position on get, and re-enable it one frame later using a coroutine or callback.

“We spent two days chasing a bug where enemies spawned invincible. Turns out the ‘isDead’ flag was still true from the last pool cycle, and our damage system checked it before our spawn logic reset it. One bool, two days.”

Related Issues

Pooling intersects with several other Unity systems that can cause confusing behavior. See Fix Unity Additive Scene Not Unloading (Memory Leak) for what happens when pooled objects prevent scene unloading, and Automated Crash Reporting for Indie Games for how to capture the stack traces that help you trace stale-state bugs back to their source.

Tip: Add a debug-only check to your pool’s Get method that logs a warning if any Rigidbody velocity is non-zero at retrieval time. It costs nothing in release builds and catches reset oversights immediately during testing.