urBackend: https://urbackend.bitbros.in
Github: https://github.com/yash-pouranik/urbackend
On the user dashboard, I display a list of all projects owned by the logged-in user. The UI only requires the project name and description, but the backend query was returning the full Project document.
- Query fetched all fields from the Project model (≈15 fields)
- Unnecessary data transfer and Mongoose document hydration
- Not optimal for a frequently hit dashboard endpoint
const projects = await Project.find({ owner: req.user._id })
.select('name description')
.lean();Additionally, I created an index:
projectSchema.index({ owner: 1 });select('name description'): Reduces payload size and serialization cost.lean(): Skips Mongoose document hydration (plain JS objects)- Index on owner: Avoids collection scans and significantly improves query speed
select()optimizes data transfer, not query execution- Indexes drive real query speed
.lean()is critical for read-heavy, display-only endpoints
- Faster dashboard load time
- Lower memory and CPU usage on the backend
- Cleaner and safer API response
The API-key authentication middleware runs on every incoming request. Originally, each request queried MongoDB to validate the API key and check the project owner’s verification status, making this a hot path and a potential performance bottleneck.
- Repeated MongoDB queries for the same API key
- Increased latency under high request volume
- Unnecessary load on the database for read-heavy authentication checks
I introduced Redis caching for API-key validation.
- Redis String: JSON-serialized object
- Cache key:
project:apikey:{hashedApiKey} - Cached data:
projectIdowner.isVerified- Minimal project metadata required by middleware
- TTL: 2 hours
- Hash incoming API key
- Check Redis for cached project
- On cache hit > skip MongoDB
- On cache miss > query MongoDB, cache result, continue
- Verify owner status before allowing request
- Entire project object is read at once
- No partial updates required
- Simple GET / SET pattern
- Easy invalidation
Cache is explicitly invalidated when:
- API key is rotated
- Project is deleted
- Project ownership or verification status changes
- Middleware is a high-impact caching target
select()optimizes payload size, not query execution- Redis significantly reduces DB load for auth-heavy systems
- Correct invalidation is critical for security
- Reduced authentication latency
- Lower MongoDB query volume
- More scalable API-key validation path
After introducing Redis caching for API-key middleware, req.project started coming from Redis instead of MongoDB. Redis returns plain JavaScript objects, not hydrated Mongoose documents.
Some controllers (e.g., insertData) perform write operations on the Project model, such as updating databaseUsed.
When the project object came from Redis:
project.save is not a function
This happened because cached objects do not have Mongoose instance methods.
I evaluated multiple approaches:
- Skipping
.save()(unsafe, loses consistency) - Rehydrating cached objects into Mongoose documents (error-prone)
- Moving writes to background jobs (correct but premature)
- Refetching the document on every write (works but adds overhead)
I replaced the read-modify-write pattern with atomic MongoDB updates.
await Project.updateOne(
{ _id: project._id },
{ $inc: { databaseUsed: docSize } }
);- Avoids document hydration
- Single database call
- Atomic and concurrency-safe
- Keeps MongoDB as the source of truth
- Cache remains strictly read-only
- Redis caching must be treated as a read optimization.
- All mutations should go through the database using atomic operations.
- No runtime crashes
- Lower database overhead
- Clear separation between read and write paths
- More scalable quota enforcement logic