prisma: Slow queries (Next.js + Prisma + Postgres)

Bug description

I have built an app where you can house forums. This application is built using Next.js, Prisma, and Postgres. Everything runs super smooth and fast in the local development environment.

  • When I deployed everything live I ran into a couple of issues. I resolved the database connection issues I was having using connection pools for the Postgres database in Digitalocean.

  • I resolved some performance issues I was having by making sure that all services (client/serverless functions/database) are located in the same geographical region (Germany/Belgium)

However

I am still having some pretty annoying performance issues. It’s on the border of not being a particularly good user experience. I have run some tests using k6 on a couple of different test routes, to demonstrate the issues I’m having.

CleanShot 2021-05-10 at 11 22 09

Get forums

export default async (req, res) => {
  const forums = await prisma.forum.findMany({
    include: {
      users: true,
    },
  });

  res.json(forums);
};

Get forums without users

export default async (req, res) => {
  const forums = await prisma.forum.findMany();

  res.json(forums);
};

Get without query

export default async (req, res) => {
  res.status(200).end()
};

How to reproduce

  1. git clone git@github.com:albingroen/formulate.git
  2. cd app && yarn
  3. Install k6
  4. env API_URL="https://formulate-theta.vercel.app/api" k6 run loadTest.js

Expected behavior

I expect the requests (Get forums and Get forums without users) to be quite a bit faster. The tables include less than 10 rows each, so that shouldn’t be the reason either.

Prisma information

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model PostReaction {
  id    Int    @id @default(autoincrement())
  value String @db.VarChar(255)

  post   Post @relation(fields: [postId], references: [id])
  postId Int

  user   User @relation(fields: [userId], references: [id])
  userId Int
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String   @db.VarChar(255)
  content   String?
  published Boolean  @default(false)
  posts     Post[]   @relation("PostToPost")
  user      User     @relation(fields: [userId], references: [id])
  userId    Int

  forum   Forum @relation(fields: [forumId], references: [id])
  forumId Int
  Post    Post? @relation("PostToPost", fields: [postId], references: [id])
  postId  Int?

  reactions PostReaction[]

  @@map(name: "posts")
}

enum ForumUserRole {
  USER
  ADMIN
}

model ForumUser {
  id        Int           @id @default(autoincrement())
  createdAt DateTime      @default(now())
  updatedAt DateTime      @updatedAt
  user      User          @relation(fields: [userId], references: [id])
  role      ForumUserRole
  userId    Int

  forum   Forum? @relation(fields: [forumId], references: [id])
  forumId Int?

  @@map(name: "forum_users")
}

model Forum {
  id          Int         @id @default(autoincrement())
  createdAt   DateTime    @default(now())
  updatedAt   DateTime    @updatedAt
  title       String      @db.VarChar(255)
  description String?
  posts       Post[]
  users       ForumUser[]
  logotype    String?

  user   User? @relation(fields: [userId], references: [id])
  userId Int?

  Invitation Invitation[]
  @@map(name: "forums")
}

model Account {
  id                 Int       @id @default(autoincrement())
  compoundId         String    @unique @map(name: "compound_id")
  userId             Int       @map(name: "user_id")
  providerType       String    @map(name: "provider_type")
  providerId         String    @map(name: "provider_id")
  providerAccountId  String    @map(name: "provider_account_id")
  refreshToken       String?   @map(name: "refresh_token")
  accessToken        String?   @map(name: "access_token")
  accessTokenExpires DateTime? @map(name: "access_token_expires")
  createdAt          DateTime  @default(now()) @map(name: "created_at")
  updatedAt          DateTime  @default(now()) @map(name: "updated_at")


  @@index([providerAccountId], name: "providerAccountId")
  @@index([providerId], name: "providerId")
  @@index([userId], name: "userId")
  @@map(name: "accounts")
}

model Session {
  id           Int      @id @default(autoincrement())
  userId       Int      @map(name: "user_id")
  expires      DateTime
  sessionToken String   @unique @map(name: "session_token")
  accessToken  String   @unique @map(name: "access_token")
  createdAt    DateTime @default(now()) @map(name: "created_at")
  updatedAt    DateTime @default(now()) @map(name: "updated_at")

  @@map(name: "sessions")
}

model User {
  id            Int       @id @default(autoincrement())
  name          String?
  email         String?   @unique
  emailVerified DateTime? @map(name: "email_verified")
  image         String?
  createdAt     DateTime  @default(now()) @map(name: "created_at")
  updatedAt     DateTime  @default(now()) @map(name: "updated_at")

  postReactions PostReaction[]
  forumUsers ForumUser[]
  forums     Forum[]
  posts      Post[]

  @@map(name: "users")
}

model VerificationRequest {
  id         Int      @id @default(autoincrement())
  identifier String
  token      String   @unique
  expires    DateTime
  createdAt  DateTime @default(now()) @map(name: "created_at")
  updatedAt  DateTime @default(now()) @map(name: "updated_at")

  @@map(name: "verification_requests")
}

enum InvitationStatus {
  PENDING
  DECLINED
  ACCEPTED
}

model Invitation {
  id     Int              @id @default(autoincrement())
  status InvitationStatus @default(PENDING)

  forum   Forum @relation(fields: [forumId], references: [id])
  forumId Int
}

Environment & setup

  • OS: MacOS
  • Database: PostgreSQL
  • Node.js version: 12.8.2
  • Prisma version:
prisma               : 2.22.1
@prisma/client       : 2.22.1
Current platform     : darwin
Query Engine         : query-engine 60cc71d884972ab4e897f0277c4b84383dddaf6c (at node_modules/@prisma/engines/query-engine-darwin)
Migration Engine     : migration-engine-cli 60cc71d884972ab4e897f0277c4b84383dddaf6c (at node_modules/@prisma/engines/migration-engine-darwin)
Introspection Engine : introspection-core 60cc71d884972ab4e897f0277c4b84383dddaf6c (at node_modules/@prisma/engines/introspection-engine-darwin)
Format Binary        : prisma-fmt 60cc71d884972ab4e897f0277c4b84383dddaf6c (at node_modules/@prisma/engines/prisma-fmt-darwin)
Default Engines Hash : 60cc71d884972ab4e897f0277c4b84383dddaf6c
Studio               : 0.379.0

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 4
  • Comments: 23 (12 by maintainers)

Most upvoted comments

NextAuth maintainer here. I AM open for a different implementation, and I did tinker a bit, and created a temporary version at 3.22.0-canary.2

import {getServerSession} from "next-auth"
import {options} from "pages/api/auth/[...nextauth]"
export default Page(){
  return null
}
export async function getServerSideProps(context) {
  return {
    props: {
      session: await getServerSession(context, options),
    },
  }
}

And in your [...nextauth].js you have to extract the options into its own const, so you can share it in the page file. This configuration could ultimately be extracted into something like next-auth.config.js or similar I guess, but this is a Work In Progress for now.

Very interested if someone could check out if it helps. the getServerSession does not do fetch

At this point it seems that you have ruled out the serverless environment and connection pooling as likely issues.

I wouldn’t rule out the serverless environment – and NextAuth definitely looks to be the cause. The trouble with NextAuth’s getSession function is that it performs a fetch call to itself, which causes Vercel-hosted serverless functions to slow down very significantly. Sometimes, even a cold start of a new lambda is required, as each lambda container can only handle a single request at once.

This is hard to reproduce in a local environment, but the solution relies upon https://github.com/nextauthjs/next-auth/issues/1535.


I can say that moving from using session-based authentication to JWTs helped a lot

--- With session-based auth

/api/forums:
433.92 ms

--- With JWT based auth

/api/forums:
131.77 ms

NextAuth’s getToken method doesn’t fetch data over the wire, unlike getSession. I’m getting more and more certain that’s the reason behind slowdowns. The official Next.js docs recommend against self-fetching on the server, too:

You should not use fetch() to call an API route in getServerSideProps. Instead, directly import the logic used inside your API route. You may need to slightly refactor your code for this approach.

Fetching from an external API is fine!

Hello @albingroen

I wasn’t able to reproduce this. Here is results of my production deploy: image

The average is around 100ms which is acceptable

This is using a digitalocean database in sfo3 region and a vercel deployment to sfo1 region. This was tested from a machine in sfo. Here is my deployment: https://issue-7009-drigger.vercel.app/api/test

Can you please double check the database region and serverless region once? Also, can you please check the lambda execution time. Here is what I am getting there: image image

More things to double check:

  • Did you use the connection string for pgbouncer in your app? Digitalocean creates a new connection string when you create a connection pool. You need to use that in order to use pgbouncer.
  • Did you add &pgbouncer=true to your connection url in order to enable pgbouncer mode in Prisma?

Thank you for thoroughly investigating this issue @kripod!

It seems like the NextAuth maintainer is open to potentially change the implementation, so hopefully this will get better soon. Glad you found a different approach that works for you!

I will close this issue now as it is clear there was no actual problem with Prisma, but the deployment of the infrastructure was suboptimal. Happy we could help you fix that @albingroen.

We will look into the next-auth Prisma reproduction a bit though of course, and I will also ping the next-auth people to take a look what might be going on here.

Yes, I’m using the Prisma adapter. I can say that moving from using session-based authentication to JWTs helped a lot

--- With session-based auth

/api/forums:
433.92 ms

--- With JWT based auth

/api/forums:
131.77 ms

I’ve removed the test routes now, but if you want to I could set up another project with the same schemas and test routes that you can play with

At this point it seems that you have ruled out the serverless environment and connection pooling as likely issues.

The next step is to look at the actual queries and verify that your database is able to execute them quickly. Anything above 10 ms is probably too slow.

Q1: have you tried running this sql query directly against the database using the database cli or a GUI tool? You can see her how to make Prisma print the SQL queries it generates: https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/logging

Q2: do you have large amounts of data? If you have a million records or more, it’s important to configure indexes correctly to support your queries.

Lastly, I’ll just mention that it is really important for Prisma to be deployed as close to the database as possible. Ideally you should aim for 10 ms network latency or less. This is because Prisma often issues multiple consecutive requests, so you will incur a latency cost that is a multiple of the network latency between Prisma and the database.

Hope this helps.