BuildWithMatija
Get In Touch
  1. Home
  2. Blog
  3. Payload
  4. Payload Async Hooks: Avoid the Transaction Trap - 3 Fixes

Payload Async Hooks: Avoid the Transaction Trap - 3 Fixes

Why passing req in Payload CMS hooks can break transactions - learn 2 safe patterns to prevent silent rollbacks

12th April 2026·Updated on:24th March 2026·MŽMatija Žiberna·
Payload
Early Access

You are viewing this article before its public release.

This goes live on April 12, 2026 at 6:00 AM.

Payload Async Hooks: Avoid the Transaction Trap - 3 Fixes

Need Help Making the Switch?

Moving to Next.js and Payload CMS? I offer advisory support on an hourly basis.

Book Hourly Advisory

If you fire async work inside a Payload CMS hook without awaiting it, and that work still carries the original req object, you can receive a success response for data that never actually committed to the database. The fix is straightforward: either await anything that passes req, or drop req entirely from work you want to run as a detached side effect. This article walks through exactly why that distinction matters and how to apply it consistently in your hooks.

I ran into this while building out a more ambitious afterChange hook for a client project — one that triggered several secondary writes on top of the main operation. Everything worked perfectly in happy-path testing. The failure only surfaced under specific timing conditions, and by then the client-facing response had already confirmed success. That kind of silent inconsistency is hard to diagnose. Once I understood the actual mechanism, I realized it came down to one overlooked detail: what it means to pass req into a Payload operation.

Why req Is More Than Request Metadata

In Payload, req carries transactional context. When you pass it into an internal operation like req.payload.create({ req, ... }), you are not just routing the request through the system — you are attaching that operation to the same database transaction the original request is running inside.

That is exactly what you want when the operation is a core part of the business action. Shared transaction context means the whole unit succeeds or fails together. The problem surfaces when you combine that shared context with unawaited async work.

The Dangerous Pattern

Here is the hook that looks harmless but creates real risk:

// File: src/collections/Orders/hooks/afterChange.ts

import type { CollectionAfterChangeHook } from 'payload'

const afterChange: CollectionAfterChangeHook = async ({ req }) => {
  const result = req.payload.create({
    req,
    collection: 'order-logs',
    data: {
      message: 'Order created',
    },
  })

  // hook continues without waiting
}

The sequence of events this creates is the core of the problem. The async create operation starts. The hook does not wait for it. The request moves forward toward its response. Payload returns a success to the client. Then the async operation fails — and because it was tied to the same transaction via req, the transaction rolls back. The client now holds a success response for data that does not exist in committed form.

This is the warning buried in Payload's documentation:

Since Payload hooks can be async and be written to not await the result, it is possible to have an incorrect success response returned on a request that is rolled back.

It is a dense sentence to read past, but it describes exactly this failure mode.

The Two Safe Patterns

The decision point is one question: should this operation share the fate of the main request?

If yes, it belongs inside the transaction, and you need to both pass req and await the result. If no, it is a detached side effect, and req should not be passed at all.

PatternPass reqAwaitUse when
TransactionalYesYesOperation is part of the core business action — it must succeed or fail with the request
DetachedNoNo (void)Operation is a side effect — analytics, logging, best-effort writes
DangerousYesNoNever — carries transactional consequences without transactional timing

Pattern A: Transactional and Awaited

// File: src/collections/Orders/hooks/afterChange.ts

import type { CollectionAfterChangeHook } from 'payload'

const afterChange: CollectionAfterChangeHook = async ({ req }) => {
  await req.payload.create({
    req,
    collection: 'order-logs',
    data: {
      message: 'Order created',
    },
  })
}

This preserves the full contract. If the create fails, the transaction rolls back. If it succeeds, you know it is committed before the response goes out.

Pattern B: Detached and Non-Blocking

// File: src/collections/Orders/hooks/afterChange.ts

import type { CollectionAfterChangeHook } from 'payload'

const afterChange: CollectionAfterChangeHook = async ({ req }) => {
  void req.payload.create({
    collection: 'analytics-events',
    data: {
      type: 'order_created',
    },
  })
}

No req. This operation runs on its own transaction, with its own fate. If it fails, that failure is independent — it does not reach back into the original request and undermine the success response.

How This Creeps into Real Projects

Hook code tends to evolve incrementally. You start simple, then add a secondary write, then a notification, then an audit record. At some point it becomes tempting to let some of that work happen in the background to avoid blocking the response.

That is exactly where the dangerous half-state gets introduced. The API reports the main action as successful. One of the secondary operations was still tied to the original transaction. That operation fails. The transaction rolls back. The client believes in a success that never truly committed.

Because the failure only appears when timing and failure conditions align just wrong, happy-path testing never catches it. Everything looks fine until it does not.

FAQ

Does this apply to all Payload hook types, or just afterChange?

It applies to any async hook where you can kick off operations without awaiting them — afterChange, afterOperation, afterRead, and others. The transaction context travels with req regardless of which hook you are in.

What if I want a secondary write to be best-effort but still use the same request context for other reasons?

You cannot safely have both. If you pass req, you opt into the transaction. If you want best-effort behavior, drop req and accept that the operation runs independently with its own transaction.

Is there a way to start a separate transaction explicitly instead of omitting req?

Payload does not currently expose a public API for manually starting a fresh transaction inside a hook. Omitting req is the correct mechanism for detaching an operation from the original request's transactional context.

Will TypeScript catch this mistake?

No. The type signatures for Payload's local API methods accept req as optional. There is no compiler-level warning for the unawaited-with-req combination. It is purely a runtime behavior issue.

How do I handle errors from detached operations if I still care about them?

Wrap the detached call in a standalone try/catch or attach a .catch() handler. Since it is no longer tied to the main request, you are responsible for its error handling independently.

// File: src/collections/Orders/hooks/afterChange.ts

req.payload
  .create({
    collection: 'analytics-events',
    data: { type: 'order_created' },
  })
  .catch((err) => {
    console.error('Analytics write failed:', err)
  })

Conclusion

The Payload async hook transaction trap comes down to one detail: passing req into an operation opts that operation into the request's transaction boundary. Combine that with unawaited async work, and you create a window where a success response goes out before the transaction has actually committed everything it depends on.

The safe habit is straightforward. Await anything that shares the transaction. Detach anything you truly want to run independently by omitting req. That single distinction keeps your hook design consistent and your success responses trustworthy.

Let me know in the comments if you have questions, and subscribe for more practical Payload and Next.js development guides.

Thanks, Matija

📚 Comprehensive Payload CMS Guides

Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.

No spam. Unsubscribe anytime.

📄View markdown version
0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Table of Contents

  • Why `req` Is More Than Request Metadata
  • The Dangerous Pattern
  • The Two Safe Patterns
  • Pattern A: Transactional and Awaited
  • Pattern B: Detached and Non-Blocking
  • How This Creeps into Real Projects
  • FAQ
  • Conclusion
On this page:
  • Why `req` Is More Than Request Metadata
  • The Dangerous Pattern
  • The Two Safe Patterns
  • How This Creeps into Real Projects
  • FAQ
Build With Matija Logo

Build with Matija

Matija Žiberna

I turn scattered business knowledge into one usable system. End-to-end system architecture, AI integration, and development.

Quick Links

Projects
  • How I Work
  • Blog
  • RSS Feed
  • Services

    • B2B Website Development
    • Bespoke AI Applications
    • Advisory

    Payload

    • B2B Website Development
    • Payload CMS Developer
    • Audit
    • Migration
    • Pricing
    • Payload vs Sanity
    • Payload vs WordPress
    • Payload vs Strapi
    • Payload vs Contentful

    Industries

    • Manufacturing
    • Construction

    Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Book a discovery callContact me →
    © 2026BuildWithMatija•Principal-led system architecture•All rights reserved