Skip to content
Postgres extensions

pg_net HTTP

What it is

pg_net lets PostgreSQL enqueue asynchronous HTTP and HTTPS requests. A background worker delivers GET, POST, and DELETE requests without holding the transaction open for the remote server.

What it provides

  • Asynchronous HTTP requests initiated from SQL, functions, or triggers
  • A request queue and response table for inspecting delivery results
  • Natural composition with triggers and pg_cron

Use it for database webhooks, notifying serverless functions after a write, synchronizing an external service, or periodically calling an internal API.

Use it with Rent

extension pg_net {
  name = "pg_net"
}

The RSL declaration makes installation and migration intent part of the reviewed application schema. Register the typed pack at runtime to call its APIs:

use rent_ext_pg_net::{PgNet, PgNetClientExt};

let pack = extensions.register_pack::<PgNet>()?;
let net = pack.client(&pool);
let health_request = net.get("https://example.com/health").await?;
let delivery_request = net
    .post(
        "https://example.com/events",
        serde_json::json!({"id": 42}),
    )
    .await?;

Application URLs and payloads are values, not interpolated SQL fragments.

A returned request ID means the request was queued, not that the destination accepted it. Requests become eligible for delivery after their transaction commits. Rollback discards them. Bind the extension client to your application transaction when the webhook must follow a successful write:

let tx = client.tx().await?;
let request_id = pack
    .client(&tx.client().system())
    .post(webhook_url, serde_json::json!({"post_id": post.id}))
    .await?;

tx.commit().await?;

Inspect net._http_response for the HTTP status and response body. Its retention is limited, and pg_net's queue and response tables are unlogged: this is not a durable, retrying message queue. Use a durable outbox or pgmq when delivery must survive a database crash. Restrict destination URLs in your application so untrusted users cannot make the database contact internal services.

Run the application

Add the extension declaration above to your application's schema. Confirm that your PostgreSQL provider offers the extension and that your migration role can install it, then apply the migration and run your application:

rent migrate dev --name enable_pg_net
cargo run

The PostgreSQL worker must be able to reach the destination URL. If PostgreSQL runs in Docker and the receiver runs on your host, use a host address reachable from that container. Restrict destinations accepted from untrusted callers.

Complete example

Download this project, or copy the files below into an empty directory.

Replace RENT_REPOSITORY_URL in Cargo.toml with the URL supplied with your preview access. Use the matching Rent CLI release.

Use an empty PostgreSQL database with the extension binaries available on its server. Set DATABASE_URL to that database and follow any server setup requirements described above. Set WEBHOOK_URL to a receiver you control before running this HTTP example.

rent extension check --provider self-hosted
rent generate
rent migrate dev --name initial
cargo run

If your host preinstalls other extensions, declare those in RSL as well. For a database with existing application tables, baseline it first.

Cargo.toml
[package]
name = "rent-extension-pg-net"
version = "0.1.0"
edition = "2024"
[workspace]
[[bin]]
name = "rent-extension-pg-net"
path = "main.rs"
[dependencies]
rent = { git = "RENT_REPOSITORY_URL", tag = "v0.8.6", default-features = false, features = ["postgres", "extensions", "migrate"] }
rent-ext-pg-net = { git = "RENT_REPOSITORY_URL", tag = "v0.8.6" }
anyhow = "1.0.104"
tokio = { version = "1.53.1", features = ["full"] }
serde_json = "1.0.151"
rent.toml
schema = "schema.rsl"
generated = "generated"
migrations = "migrations"

[database]
dialect = "postgres"
backends = ["postgres"]
schema.rsl
datasource db {
  provider = "postgresql"
}

extension pg_net {
  name = "pg_net"
}

model Post {
  id    BigInt  @id
  title String
  body  String  @default("")
}
main.rs
use anyhow::Result;
use rent::{driver::DatabasePool, extension::ExtensionRegistry};
#[allow(dead_code, unused_imports)]
#[rustfmt::skip]
#[path = "generated/mod.rs"]
mod generated;
#[tokio::main]
async fn main() -> Result<()> {
    let pool = DatabasePool::connect_url(&std::env::var("DATABASE_URL")?).await?;
    let mut extensions = ExtensionRegistry::default();
    use rent_ext_pg_net::{PgNet, PgNetClientExt};
    let pack = extensions.register_pack::<PgNet>()?;
    let destination = std::env::var("WEBHOOK_URL")?;
    let request = pack
        .client(&pool)
        .post(destination, serde_json::json!({"kind":"post.published"}))
        .await?;
    println!("Queued HTTP request {request}; delivery happens after commit.");
    Ok(())
}