π Hash Vault (Check Cache)
Never pay for the same prompt twice
- any_input
- any_input_2
- any_input_3
- any_input_4
- cached_data
- is_cached
- hash_key
ComfyUI's native caching is great - until it isn't. For local nodes it skips work it's already done and everybody's happy. But with external API nodes it falls apart: dynamic timestamps, non-deterministic seeds, images that differ by a single byte. Re-queue the same prompt and the API cheerfully charges you again. This node is the read half of a disk-cache layer built for paid API calls: it hashes whatever defines your call, looks it up in output/hash_vault/, and tells you whether you've already paid for exactly this result.
How the hash works
The cache key is a SHA-256 over payload_string plus up to four any_input slots. The important part is how it hashes. Tensors are hashed byte-for-byte - the full content including dtype and shape metadata, not some lossy perceptual approximation. Same image with a slightly different value is a different key. Nested structures (dicts, lists, tuples) hash recursively, and dict keys are sorted first, so the order keys appear in doesn't matter. Wire a different prompt, change a dropdown, nudge a strength slider - the key changes and the API runs again. That's the contract: strict identity, no fuzziness.
Cache files live at output/hash_vault/{hash_key}.pt and are loaded with map_location="cpu", so entries are portable across GPU configs. Every read/write is wrapped in a file lock, and writes are atomic - a crashed save can't corrupt an entry.
The inputs that matter
All of them are optional, which is the design: hash on whatever subset defines uniqueness for your call.
payload_string- the prompt, JSON params, or any string that factors into the key. For a prompt-driven API, this one input is usually all you need.any_inputthroughany_input_4- any type: image, latent, conditioning, or a converted widget. Right-click a downstream node's widget β Convert Widget to Input β wire it here. One slot per thing that defines your call.cache_ttl_hours(default 0) - time-to-live in hours. 0 = never expires. Set a TTL for anything where a stale result is worse than a fresh one.
The three outputs
is_cached- INT, 1 on a hit, 0 on a miss. Wire this into the Lazy API Switch; that's what lets ComfyUI skip the API node entirely.hash_key- STRING, the cache key. Feed it to Hash Vault (Save API Result) so a miss writes under the same key.cached_data- the stored result on a hit,Noneon a miss. Also goes to the Lazy Switch, which routes it downstream when it's there.
The sandwich pattern
This node never works alone. The standard wiring: fan your prompt/image out to both Check Cache and your API node. Check Cache's is_cached + cached_data go to the Lazy API Switch; the API node's output goes to Hash Vault (Save Result), using the same hash_key. First run misses, the API executes, Save banks the result. Second run hits, and the Lazy Switch feeds you the cached copy while the API node never executes - no call, no charge.
Gotchas
Watch the converted-widget trap. If your API node's dropdown becomes an input and you drive it with a raw string primitive, there's no validation - a typo silently changes the hash. The hash stays valid, the cache just never hits again, and the API errors at runtime. That's why the pack's example workflows use a Settings node (like Gemini Style Transfer Settings) that owns the real dropdowns and emits validated strings.
Cache not hitting? Something in your inputs is changing between runs - a timestamp, a seed that isn't fixed, a tensor that differs in dtype or shape. Since hashing is strict, any of those reads as "new" and you pay again.
Install
Ships with the ComfyUI-API-Optimizer pack. Via ComfyUI Manager, search "ComfyUI API Optimizer", or:
cd ComfyUI/custom_nodes/
git clone https://github.com/jeremieLouvaert/ComfyUI-API-Optimizer.git
pip install -r ComfyUI-API-Optimizer/requirements.txt
Restart ComfyUI. Dependencies are just filelock on top of the PyTorch you already have - no models to download.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| payload_stringopt | STRING | Prompt, JSON params, or any STRING that should factor into the cache key. Optional β you can hash purely on any_input slots if you prefer. | |
| any_inputopt | * | Any input to hash: image, latent, conditioning, or a converted widget (convert a downstream node's widget to input and wire it here). Content is hashed recursively. | |
| any_input_2opt | * | Second any-type input. Use one slot per widget you want to factor into the cache key β e.g. image on any_input, style dropdown on any_input_2, strength float on any_input_3. | |
| any_input_3opt | * | Third any-type input. | |
| any_input_4opt | * | Fourth any-type input. | |
| cache_ttl_hoursopt | FLOAT | 00β8760 | Cache time-to-live in hours. 0 = never expires |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| cached_data | * | β |
| is_cached | INT | β |
| hash_key | STRING | β |