Post meta lets you attach extra data to any post, page or custom post type — a product’s SKU, an event’s date, a recipe’s cooking time. WordPress stores it as key/value rows in the wp_postmeta table. This guide covers the APIs you’ll use most and the pitfalls to avoid.
The core functions
// Add (allows duplicates for the same key unless $unique is true)
add_post_meta( $post_id, 'event_date', '2026-12-01', true );
// Create or replace
update_post_meta( $post_id, 'event_date', '2026-12-05' );
// Read: true = single value, false = array of all values for that key
$date = get_post_meta( $post_id, 'event_date', true );
// Delete
delete_post_meta( $post_id, 'event_date' );
Keys starting with an underscore (e.g. _price) are hidden from the classic “Custom Fields” panel. Use that for values your code manages.
Register your meta
register_post_meta() declares the type, a sanitize callback and whether the field is exposed to the REST API — required if the block editor or a headless front end needs it:
add_action( 'init', function () {
register_post_meta( 'event', 'event_date', [
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => fn() => current_user_can( 'edit_posts' ),
] );
} );
With show_in_rest, the value appears in /wp-json/wp/v2/event/<id> under meta, and block editor plugins can read and write it with useEntityProp.
A simple meta box (classic UI)
add_action( 'add_meta_boxes', function () {
add_meta_box( 'event_details', 'Event details', 'render_event_box', 'event', 'side' );
} );
function render_event_box( $post ) {
wp_nonce_field( 'save_event', 'event_nonce' );
$value = get_post_meta( $post->ID, 'event_date', true );
printf( '<label>Date <input type="date" name="event_date" value="%s"></label>', esc_attr( $value ) );
}
add_action( 'save_post_event', function ( $post_id ) {
if ( ! isset( $_POST['event_nonce'] ) || ! wp_verify_nonce( $_POST['event_nonce'], 'save_event' ) ) return;
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( ! current_user_can( 'edit_post', $post_id ) ) return;
if ( isset( $_POST['event_date'] ) ) {
update_post_meta( $post_id, 'event_date', sanitize_text_field( wp_unslash( $_POST['event_date'] ) ) );
}
} );
Always check the nonce, autosave and capability, sanitize on save, and escape on output (esc_html, esc_attr, esc_url).
Querying posts by meta
$events = new WP_Query( [
'post_type' => 'event',
'meta_key' => 'event_date',
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_query' => [
[ 'key' => 'event_date', 'value' => date( 'Y-m-d' ), 'compare' => '>=', 'type' => 'DATE' ],
],
] );
Store dates as Y-m-d and numbers as plain numbers so comparisons sort correctly.
Performance
meta_valueisn’t indexed. Meta queries on large sites (hundreds of thousands of rows) can be slow. For data you filter on constantly, consider a custom table or a taxonomy.get_post_meta()is cached per request; WordPress loads all meta for a post in one query. A persistent object cache (Redis) avoids repeating it across requests.- Avoid storing large serialized arrays you need to search — you can’t query inside them efficiently.
On a VPS you can add Redis in a few minutes (apt install redis-server plus an object-cache plugin), which helps meta-heavy sites a lot. See WordPress VPS performance tuning.
Plugins vs code
Advanced Custom Fields (ACF), Meta Box and Pods provide field UIs and store values as regular post meta, so the functions above still work with their data. Use them when editors need complex field layouts; write code when you need a few fields and full control.
Related: understanding WordPress post meta covers how the wp_postmeta table works.