Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A Go backend connects to Neon PostgreSQL with a standard PostgreSQL connection string, and the decisions that matter most for an e-commerce store are which connection URL the application uses, how far the Go connection pool is allowed to grow, which database driver the code is written against, and whether an order and its inventory change commit or fail together. This guide works through each of those decisions using Neon’s and the Go project’s official documentation as the reference points.

It is not an account of one specific codebase. Those sources establish how the tools behave; they cannot establish how a particular store will perform under its own traffic. Where a choice depends on your load, schema, or migration tooling, the sections below say so and name what to measure.

Connecting a Go service to Neon

Neon’s connection guide, titled “Connecting Neon to your stack” and marked as updated on 2026-10-05, shows the same pattern for any PostgreSQL client: copy a connection string and pass it to the driver. The steps are:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Open the Neon Console and select the branch, database, and role you want the service to use.
  2. Copy the connection string shown for that combination. It is a standard PostgreSQL URL and includes sslmode=require.
  3. Store the string in deployment configuration, such as your platform’s secret store or an environment variable, and never in committed source files. Neon’s example uses real-looking credentials; do not reuse them.
  4. Read the string at startup, open the handle, and verify the connection before serving traffic.

Neon’s Go example uses Go’s database/sql package with the lib/pq driver. A minimal version looks like this:

package main

import (
    "database/sql"
    "log"
    "os"
    "time"

    _ "github.com/lib/pq"
)

func openDB() (*sql.DB, error) {
    dsn := os.Getenv("DATABASE_URL")
    if dsn == "" {
        log.Fatal("DATABASE_URL is not set")
    }

    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, err
    }

    db.SetMaxOpenConns(maxOpenFromConfig())
    db.SetMaxIdleConns(maxOpenFromConfig() / 2)
    db.SetConnMaxLifetime(30 * time.Minute)

    if err := db.Ping(); err != nil {
        db.Close()
        return nil, err
    }
    return db, nil
}

sql.Open does not connect by itself; it only validates arguments. The Ping call is what proves the URL, credentials, and TLS setting work, so it belongs at startup. maxOpenFromConfig is a placeholder for your own configuration helper; the pool limit is discussed in the pool section below.

Pooled or direct: which Neon connection string to use

Neon provides two kinds of hostname for the same database. The pooled hostname includes -pooler; the direct hostname does not. Neon’s guidance is to use the pooled connection when an application opens many concurrent connections, and the direct connection for migrations and for features that depend on session-level state.

Workload Connection type Reason given in Neon’s guide
Web API serving many concurrent requests Pooled (hostname contains -pooler) Many concurrent client connections
Schema migrations Direct Migration tooling is typically run once per deployment and may depend on session behavior
Code relying on session-level features Direct Session-level features are named as a direct-connection case

This is Neon’s documented recommendation, not a general rule for PostgreSQL. If your migration tool needs a particular URL, check its documentation and configure it explicitly, rather than assuming the application’s URL is correct for it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Two pools: the Go layer and the Neon layer

Connection problems in a Go service on Neon usually come from two places at once, so it helps to separate them.

The Go pool inside sql.DB

The Go project’s “Managing connections” guidance describes sql.DB as the normal handle to use: it is safe for concurrent goroutines and manages a pool of connections. Each operation takes a connection from the pool or opens a new one, then returns it.

  • SetMaxOpenConns caps the number of connections the process can hold open. When the cap is reached, further operations wait for a connection to be released.
  • SetMaxIdleConns controls how many idle connections are kept for reuse.
  • SetConnMaxLifetime limits how long any one connection is reused before it is closed and replaced.
  • db.Stats() reports open, in-use, and idle counts and wait behavior, which is the data you need to choose a cap.

The Go documentation warns that a cap on open connections works like a semaphore. If code holds one connection while waiting for another, or acquires resources in inconsistent orders, the program can deadlock. The fix is to keep each unit of work short and to release connections promptly, not to remove the cap. Choose the cap from measured load, not from a number copied from another project.

Timeouts and cancellation

Every query in a request handler should accept a context.Context. Use QueryContext, ExecContext, and BeginTx, and derive the context from the incoming request with a deadline. A request that is cancelled by the client then stops waiting for a pool connection and stops its query, instead of holding a connection until the database returns.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Neon side

The pooled hostname routes client connections through Neon’s pooler, so the number of connections your Go process opens and the number of server-side connections Postgres handles are not the same thing. Neon’s guide does not publish a formula relating the two. Treat the pooled endpoint as the right default for request traffic and verify behavior against your own load test rather than assuming a specific ceiling.

database/sql or pgx

The Neon example uses database/sql with lib/pq. The pgx project supports two interfaces: its native PostgreSQL API, and an adapter that exposes pgx through database/sql. Its README recommends considering the native API for applications that target only PostgreSQL and have no library requiring database/sql. That is a statement about fit, not a verdict that one is faster or better; the sources do not include benchmarks comparing the two.

Question database/sql with lib/pq Native pgx (v5)
Match to Neon’s published Go example Yes, the example uses this combination Neon’s example is not stated to use pgx; Neon’s guide still applies because it is a standard URL
Works with libraries that require database/sql Yes Yes, through the stdlib adapter
PostgreSQL-specific interface and types Limited to the database/sql abstraction Native API for PostgreSQL features
Pool type sql.DB (standard library) pgxpool in the native API; sql.DB when using the adapter
Performance difference Not stated in the sources Not stated in the sources

A reasonable rule: choose database/sql if your team wants the standard interface, your ORM or migration tool requires it, or the code must stay portable across databases. Choose native pgx if the service is PostgreSQL-only and you want its PostgreSQL-specific API. The native pool is created like this:

import (
    "context"
    "os"

    "github.com/jackc/pgx/v5/pgxpool"
)

func openPool(ctx context.Context) (*pgxpool.Pool, error) {
    pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
    if err != nil {
        return nil, err
    }
    if err := pool.Ping(ctx); err != nil {
        pool.Close()
        return nil, err
    }
    return pool, nil
}

Whichever driver you choose, the Neon connection-string rules above apply unchanged.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keeping orders and inventory consistent

The main correctness risk in an e-commerce backend is writing an order without the matching stock change, or changing stock without an order. The Go project’s “Executing transactions” guidance addresses this directly: a transaction groups operations so they all succeed or none do. Its worked example checks inventory, updates it, inserts an order, and commits, with a deferred rollback that discards the work if any step fails.

Put the stock change and the order in one transaction

The check-then-decrement pattern has a race: two requests can both read a stock value of one and both proceed. Doing the check inside the update avoids this, because the database applies the condition and the change in one statement. The example below handles one product line. It uses a conditional UPDATE and checks the number of affected rows.

package store

import (
    "context"
    "database/sql"
    "errors"
)

var ErrInsufficientStock = errors.New("insufficient stock")

func PlaceOrder(ctx context.Context, db *sql.DB, customerID, productID int64, qty int) (int64, error) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return 0, err
    }
    defer tx.Rollback() // has no effect after a successful Commit

    res, err := tx.ExecContext(ctx,
        `UPDATE products SET stock = stock - $1
         WHERE id = $2 AND stock >= $1`,
        qty, productID)
    if err != nil {
        return 0, err
    }
    n, err := res.RowsAffected()
    if err != nil {
        return 0, err
    }
    if n != 1 {
        return 0, ErrInsufficientStock
    }

    var orderID int64
    err = tx.QueryRowContext(ctx,
        `INSERT INTO orders (customer_id, product_id, quantity, status)
         VALUES ($1, $2, $3, 'pending')
         RETURNING id`,
        customerID, productID, qty).Scan(&orderID)
    if err != nil {
        return 0, err
    }

    if err := tx.Commit(); err != nil {
        return 0, err
    }
    return orderID, nil
}

Three rules from the Go transaction guidance apply to this code:

  • Run every statement that belongs to the order through tx. A call made on db inside the function runs outside the transaction.
  • Use BeginTx, Commit, and Rollback. Do not issue BEGIN or COMMIT as raw SQL through the handle, because the pool may hand different statements to different connections.
  • Pass the request context to every statement so a cancelled request releases its transaction.

Several line items

A real order usually has several lines. Update them inside the same transaction, and always process products in a fixed order, such as ascending product ID. If two orders lock the same rows in opposite orders, PostgreSQL can deadlock; a consistent order prevents that. Keep the transaction short: do not wait on users or external services while it is open.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Payment authorization

Payment calls go to an external system, and that system cannot be rolled back by your database. The design choice is whether to call the payment provider inside the transaction or outside it. The official transaction guidance does not settle this for e-commerce. A common pattern is to commit the order in a pending state, then call the payment provider, then update the order status in a second short transaction, with a reconciliation job for orders left pending after a crash. Whichever pattern you choose, record the provider’s reference on the order so that a retry cannot charge twice. Test the crash-after-payment case explicitly, since no official source here measures how often it occurs.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Failure modes to check before going live

  • Requests slow down under load with no database error: the Go pool cap is reached and requests are waiting. Check db.Stats() for wait count and wait duration before changing the cap.
  • Intermittent deadlock errors on checkout: line items are updated in inconsistent orders, or a transaction waits on something outside the database.
  • Migrations fail or hang on the pooled URL: the migration tool is using the pooled hostname. Point it at the direct hostname, as Neon’s guide recommends for migrations.
  • Connection refused or TLS errors: the URL is missing sslmode=require, has the wrong branch or role selected, or has a copied secret with trailing whitespace.
  • Stock goes negative or orders exist without stock changes: a statement ran on db instead of tx, or the stock check was separated from the decrement.

What to verify in your own build

Before relying on any of the patterns above, confirm the following against your own code and environment:

  • Which URL each process uses: the API, background workers, and the migration tool.
  • The pool limit and the measured wait behavior under realistic concurrency.
  • Whether every order write and stock write goes through the same transaction object.
  • How payment outcomes are recorded and how stuck orders are recovered.

The sources cited in this guide are Neon’s “Connecting Neon to your stack” (updated 2026-10-05), the Go project’s documentation pages “Accessing relational databases,” “Managing connections,” and “Executing transactions,” and the jackc/pgx project README. Check each for the version you deploy, since these pages change over time.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.