How can I safely retry a FeathersJS Mongoose patch with upsert to avoid duplicate writes on transient failures?
0 reputation · 15 Jul 2026, 09:45 UTC
0 reputation · 15 Jul 2026, 09:45 UTC
When using a FeathersJS service backed by feathers‑mongoose, I want to retry a patch operation that uses the upsert:true option in case of transient network errors. The concern is that a retry after a partial failure could cause a second insert instead of an update, leading to duplicate documents. The adapter can return a writeResult that contains information about whether the operation inserted or modified a record, and there is also a TransactionManager that can roll back changes on error.
How can I use the writeResult to reliably determine whether the original patch performed an insert or an update, and whether it is safe to retry the operation without creating duplicates? Should I rely on the presence of an upsertedId field, or combine upsert with a transaction to guarantee atomicity on retry?
26525 reputation · 15 Jul 2026, 12:23 UTC
When a FeathersJS service backed by feathers‑mongoose performs a patch with upsert:true, the driver returns a writeResult that contains an upsertedId if a new document was created. You can safely retry the operation only if you can confirm that the first attempt either succeeded or failed before any acknowledgment was sent to the client. The most reliable way to guarantee this is to wrap the patch in a MongoDB transaction and use writeConcern: { w: 'majority', j: true }. The transaction ensures that either the insert or the update is fully committed before the client sees success, and the upsertedId will be present only for an actual insert.
writeResult returned by findOneAndUpdate (used internally by feathers‑mongoose) contains upsertedId when a new document is inserted.writeConcern: { w: 'majority', j: true } guarantees that the write is acknowledged by a majority of nodes before the driver resolves the promise.async function startSession(hook) {
const session = await hook.app.get('mongooseClient').startSession();
hook.session = session;
}
async function transactionalPatch(hook) {
const { app, data, params } = hook;
const session = hook.session;
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
await session.withTransaction(async () => {
// feathers‑mongoose internally calls findOneAndUpdate
const result = await app.service('my-service').patch(null, data, {
...params,
session,
upsert: true,
new: true,
writeConcern: { w: 'majority', j: true }
});
hook.result = result;
});
break; // success
} catch (err) {
if (isTransient(err)) { attempt++; continue; }
throw err;
}
}
if (attempt === maxRetries) throw new Error('Max retry attempts reached');
}
function isTransient(err) {
return err.name === 'MongoNetworkError' || err.name === 'MongoWriteConcernError';
}
upsertedId:
const { upsertedId } = hook.result;
if (upsertedId) {
// A new document was inserted – safe to log or return the new ID
}
_id or a unique index field. This guarantees that even if a retry happens after the first write, the filter will only match the same document and not create a duplicate.
await hook.session.endSession();
upsertedId AloneIf your deployment does not support transactions (e.g., a single-node MongoDB, or a cluster without a replica set), you can still use upsertedId to decide whether to retry. However, you must be very careful to use a unique filter and to catch only genuine network failures. In such environments, an exponential‑backoff retry loop with a low maximum count (2–3 retries) is recommended.
To fine‑tune the recommendation, could you confirm whether your MongoDB deployment supports transactions (replica set or sharded cluster with replica sets) and what field(s) you use as the patch filter? This will determine if the transaction‑based approach is applicable.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.