tl;dr
Most new applications should start with Postgres. Add Redis when a specific workload needs shared low-latency state, expiring keys, counters, rate limits, cache semantics, pub/sub, streams, or queue coordination that Postgres should not carry alone.
- Postgres is usually the system of record. Transactions, constraints, relationships, and flexible queries fit durable business data.
- Redis is not merely ephemeral. It offers persistence and can be a primary database in some architectures.
- Do not add Redis on reputation alone. A second state system adds invalidation, consistency, credentials, monitoring, recovery, and cost.
- Postgres can cover more than tables. It can support locks, notifications, queues, sessions, and computed state at modest scale.
- Use both when their responsibilities are explicit. Postgres owns durable truth. Redis owns a named access or coordination path.
“Postgres or Redis?” is usually the wrong first question.
The right first question is:
What state does this application have, and what contract must that state satisfy?
Postgres and Redis overlap. Both can store application data. Both can persist writes. Both can support coordination patterns. The useful distinction is not “durable database versus temporary cache.” It is which system matches the workload's relationships, access pattern, latency target, expiration model, and failure semantics.
For most agent-generated business applications, the answer starts with Postgres alone.
start with Postgres when correctness is relational
Suppose an agent builds a customer-support application. It stores organizations, users, tickets, assignments, comments, and permissions.
Those records relate to one another. A ticket belongs to an organization. An assignment references a valid user. A comment belongs to an existing ticket. Several changes may need to commit together.
Postgres gives that state a useful default contract:
- transactions for all-or-nothing changes;
- foreign keys and constraints;
- joins and flexible queries;
- indexes for several access paths over the same records;
- durable history that should survive process and cache replacement.
PostgreSQL describes a transaction as multiple steps bundled into one all-or-nothing operation. Its constraints can enforce valid values, uniqueness, primary keys, and relationships in the database rather than trusting every caller to repeat the rule correctly. The current constraints documentation shows those mechanisms.
That matters when an agent adds a second code path later. Database constraints continue to protect the invariant even if the new handler forgets a check.
what Postgres can do before Redis enters the stack
Postgres is not limited to long-lived rows and complex joins.
For a young application, it can also handle:
- server-side sessions;
- advisory locks;
- job rows claimed with transactional locking;
- notifications through
LISTENandNOTIFY; - materialized or precomputed results;
- rate-limit counters at modest traffic;
- JSON documents alongside relational records.
Keeping these responsibilities in one database can be the better architecture. It avoids a cross-system consistency boundary and lets one transaction create both a business record and the work that must follow it.
The key is not whether Redis would be faster in a benchmark. It is whether the application has measured a problem worth introducing another state system.
Redis is a database with different strengths
Redis is an in-memory data store with data structures and atomic operations designed for low-latency access and coordination. Its documented use cases include caching, session storage, rate limiting, job queues, pub/sub, streams, leaderboards, time series, vector search, and agent memory. Redis maintains current examples for these workload patterns.
Redis also supports several persistence choices:
- point-in-time RDB snapshots;
- append-only logging;
- both mechanisms together;
- no persistence for replaceable cache state.
Redis documents the durability and data-loss tradeoffs of each persistence mode. Calling all Redis state ephemeral is therefore wrong.
The better distinction is that Redis makes certain shared, low-latency state transitions natural. Postgres makes relational integrity and durable multi-record truth natural.
the decision table
| requirement | start with Postgres | consider Redis |
|---|---|---|
| Durable users, orders, tickets, permissions | Yes | Only with an intentional Redis-primary design |
| Multi-row transactions and foreign keys | Yes | Usually not the center of the model |
| Ad hoc filtering and reporting | Yes | Not its primary strength |
| Shared cache with TTL | Possible, but not the natural fit | Yes |
| High-rate counters and rate limits | Possible at modest scale | Often a strong fit |
| Pub/sub where offline subscribers may miss messages | Possible with NOTIFY constraints | Redis Pub/Sub is a direct fit |
| Replayable event stream with consumer groups | Possible with tables and cursors | Redis Streams may fit |
| Queue with transactional coupling to business writes | Strong when one Postgres transaction matters | Requires a cross-system handoff unless designed around it |
| Sessions | Often enough | Useful for high-volume shared expiration |
| Leaderboards and ranked sets | More query work | Sorted sets are purpose-built |
This table is a starting point, not a law. Workload evidence decides.
a concrete evolution from Postgres-only to Redis
Return to the support application.
stage 1: Postgres only
The app has a web service and Postgres. Ticket writes, assignments, search filters, and sessions all use the database. This is easy to inspect and recover.
stage 2: the workload changes
The application now serves thousands of agents in many browser tabs. Every request recomputes the same organization permissions. The primary database shows repeated reads dominating load. The result may be stale for 30 seconds without harming correctness.
Now Redis has a named job:
Cache organization-permission projections for 30 seconds, with Postgres remaining authoritative.
The application uses cache-aside:
- Read Redis by organization and permissions-version key.
- On a miss, read Postgres.
- Store the projection with a TTL.
- On a permission write, commit Postgres first and invalidate the relevant cache key.
- If Redis is unavailable, fall back to Postgres.
Redis is justified because the team can name the hot read, staleness tolerance, fallback, invalidation event, and success metric.
the cost of adding Redis
A second state system creates obligations that code generation can hide:
- Which database is authoritative for each key?
- Can the cache be stale, and for how long?
- What invalidates a value?
- What happens when invalidation fails?
- Does the application continue when Redis is unavailable?
- Which keys belong to each environment and application?
- What is the eviction policy?
- Which persistence and backup contract applies?
- How are credentials rotated?
- Can a preview read development or production keys?
If these questions do not have answers, the architecture gained ambiguity rather than capability.
queues and streams need sharper language
“Use Redis as a queue” is not a complete design.
Redis supports several message shapes with different semantics:
- Pub/Sub broadcasts to currently connected subscribers and retains no history.
- Streams retain ordered entries and can use consumer groups, acknowledgments, and replay.
- A job queue implementation assigns tasks to workers, usually with retry and visibility behavior defined by the library or application.
Redis explicitly distinguishes Pub/Sub, streams, and job queues. The choice determines what happens when a consumer crashes, a message is delivered twice, or a worker is offline.
Do not write “exactly once” because one library makes duplicate processing unlikely. If a queue provides at-least-once delivery, the handler still needs idempotency or a deduplication boundary.
common mistakes from agent-generated code
Watch for these patterns:
- Redis added to the template before a workload needs it;
- user records written to both Postgres and Redis without one authority;
- cache keys with no TTL or invalidation path;
- authorization decisions served from an unversioned cache;
- queue payloads that cannot be processed twice safely;
- global key names shared across applications or environments;
- a Redis outage converted into a total application outage even though Postgres has the source value;
- a benchmark claim used instead of production latency and database-load evidence.
The safe default is boring: one source of truth, one explicit reason for every additional state path.
how this changes preview environments
Once an application has both Postgres and Redis, a preview needs more than a branch URL.
It needs to answer:
- Which Postgres branch or schema does this preview own?
- Which Redis database or key namespace does it own?
- Can a worker from this branch consume another environment's messages?
- Does teardown remove only preview-owned resources?
- Can the agent identify those resources in structured output?
That is why production-shaped previews and shared staging matter. Adding a state layer adds an isolation obligation.
where floo fits
floo can start with the small shape: an application service and floo-managed Postgres in one environment model. If the workload later earns Redis, floo-managed Redis joins that application and environment rather than becoming an unrelated credential pasted into one service.
For previews, supported managed Postgres and Redis receive preview-owned identity and credentials. The application topology, route, source change, and managed attachments remain attributable to the same preview. The repository holds reviewable desired state, while secret connection values stay outside git.
The decision still belongs to the application team. floo does not make every app need Redis. It makes the chosen state topology legible to the agent and human operating it.
Start with Postgres. Add Redis when you can finish this sentence precisely: “We need shared low-latency state for ___, its authority is ___, staleness may last ___, and failure falls back to ___.” If that workload is part of a real app your agent is building, map its production environment with floo.