Skip to content

204 No Content

What is HTTP 204 No Content?

204 No Content
When you ask a website to do something, sometimes it does the job but doesn't ne...
HTTP 204 No Content status code illustration

Explain Like I’m 3

You asked someone to do something and they did it perfectly, but they don’t need to tell you anything else! It’s done! Like when you ask someone to close the door - they just close it and don’t say anything back because the job is finished!

Example: You ask someone to throw away trash. They throw it away and give you a thumbs up. They don’t need to bring anything back to you - the job is just done!

Explain Like I’m 5

When you ask a website to do something, sometimes it does the job but doesn’t need to send you anything back. Like if you delete a photo - the website deletes it successfully and says ‘204 No Content’ which means ‘I did what you asked, and there’s nothing to send you because the job is finished!’ If you updated your name on a profile, the website might just say ‘204 No Content’ meaning ‘I updated it!’ without sending your whole profile back, because you already know what you changed.

Example: You click a button to delete a comment you wrote. The website deletes it and responds with ‘204 No Content.’ Your browser doesn’t show a new page - it just removes the comment from your screen, because the website already confirmed it’s deleted.

Jr. Developer

HTTP 204 No Content means the server successfully processed the request but is not returning any content in the response body. This is commonly used for DELETE operations (resource deleted, nothing to return), PUT/PATCH updates where the client already has the updated data, OPTIONS requests (allowed methods returned in headers, no body needed), or actions that trigger side effects without generating output. The 204 response MUST NOT include a message body - it’s always terminated by the first empty line after headers. When you send 204, you can include headers like ETag (for the new resource state after PUT), Location (if applicable), or Cache-Control. Unlike 200 (which can have an empty body but shouldn’t), 204 explicitly communicates ‘no body is coming’ - browsers won’t try to parse a response body, and some frameworks won’t even read from the socket. Use 204 when the client doesn’t need data back; use 200 when you want to return the updated resource; use 201 for resource creation with Location header.

Example: A user deletes their profile picture via DELETE /users/123/avatar. Your API removes the file from storage, clears the database field, and returns 204 No Content. The client receives no body and simply removes the avatar display. If you returned 200 with the updated user object, it would work but wastes bandwidth since the client just needs confirmation.

Code Example

// Express.js using 204 for DELETE
app.delete('/api/posts/:id', async (req, res) => {
const deleted = await db.deletePost(req.params.id);
if (!deleted) {
return res.status(404).json({ error: 'Post not found' });
}
// Success - no content to return
res.status(204).end();
});
// 204 for PUT when client has updated data
app.put('/api/users/:id/preferences', async (req, res) => {
await db.updatePreferences(req.params.id, req.body);
// Client sent the data, they know what it is
// Just confirm success without echoing back
res.status(204).end();
});
// WRONG: Don't send body with 204
res.status(204).json({ success: true }); // BAD!

Crash Course

204 No Content, defined in RFC 9110 Section 15.3.5, indicates the server successfully fulfilled the request and there is no content to send in the response payload body. Per the RFC, the 204 response MUST NOT contain a message-body and is always terminated by the first empty line after header fields. This is semantically different from 200 OK with empty body - 200 suggests ‘here’s your content (which happens to be empty)’ while 204 explicitly states ‘there is intentionally no content.’ Browsers and HTTP clients handle 204 specially: they don’t wait for a response body, won’t try to parse JSON/HTML, and keep the current document view unchanged. Common use cases: DELETE operations (resource removed, nothing to return), PUT/PATCH updates when the client already knows the final state, HEAD requests (rare - usually just return 200 with headers), OPTIONS preflight (methods in Allow header, no body), POST actions triggering side effects (like unsubscribe email links). 204 responses are cacheable by default if cache directives are present, but caching a ‘no content’ response is unusual. Headers you can include with 204: ETag (new entity tag after PUT/PATCH), Content-Location (alternative identifier), Cache-Control/Expires (caching directives), Allow (for OPTIONS), Location (for created resources, though 201 is better). Best practices: Use 204 for DELETE unless you need to return resource metadata or confirmation payload (use 200 instead). For PUT/PATCH, use 204 if the client has all the updated data; use 200 if you want to return the complete updated resource representation (useful if server-side defaults or calculated fields changed). For OPTIONS, 204 is appropriate with Allow header listing supported methods. Avoid 204 for GET - clients expect content for GET requests. Framework differences: Express res.status(204).end() (don’t call .json() or .send()), Spring @ResponseStatus(HttpStatus.NO_CONTENT), Django return HttpResponse(status=204).

Example: A task management API receives DELETE /api/tasks/12345 from a client. The server verifies the user owns the task, removes it from the database, deletes associated subtasks, and removes file attachments from storage. Instead of returning the deleted task object (which would be 200 with task data), it returns 204 No Content. The client receives the 204 status and knows the deletion succeeded. It removes the task from the UI without needing to parse any response body, saving bandwidth and simplifying client logic.

Code Example

// Production DELETE with proper 204 handling
const express = require('express');
const app = express();
app.delete('/api/resources/:id', async (req, res) => {
try {
const resource = await db.findResource(req.params.id);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
// Authorization check
if (resource.userId !== req.user.id) {
return res.status(403).json({ error: 'Forbidden' });
}
// Perform deletion
await db.deleteResource(req.params.id);
// Success - return 204 with no body
res.status(204).end();
} catch (error) {
console.error('Delete error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// PUT with 204 when client has full updated state
app.put('/api/settings/:userId', async (req, res) => {
const { theme, notifications, language } = req.body;
// Validate
if (!['light', 'dark'].includes(theme)) {
return res.status(400).json({ error: 'Invalid theme' });
}
// Update database
await db.updateSettings(req.params.userId, {
theme,
notifications,
language
});
// Client sent the data and knows final state
// Include ETag for cache validation
const etag = generateETag({ theme, notifications, language });
res.status(204)
.set('ETag', etag)
.set('Cache-Control', 'no-cache')
.end();
});
// OPTIONS for CORS preflight
app.options('/api/resources', (req, res) => {
res.status(204)
.set('Allow', 'GET, POST, PUT, DELETE, OPTIONS')
.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
.set('Access-Control-Max-Age', '86400') // Cache preflight for 24 hours
.end();
});
// POST action with side effect (no content to return)
app.post('/api/notifications/:id/dismiss', async (req, res) => {
await db.markNotificationDismissed(req.params.id, req.user.id);
// Action completed, nothing to return
res.status(204).end();
});
function generateETag(data) {
const crypto = require('crypto');
return '"' + crypto.createHash('md5').update(JSON.stringify(data)).digest('hex') + '"';
}

Deep Dive

The 204 No Content status code, specified in RFC 9110 Section 15.3.5, indicates that the server has successfully fulfilled the request and there is no additional content to send in the response payload body. The RFC states: ‘The 204 response MUST NOT contain a message-body, and thus is always terminated by the first empty line after the header fields.’ This explicit prohibition on message bodies distinguishes 204 from 200 OK with an empty body. The semantic intent is different: 200 with empty body suggests ‘here is your content (which happens to be zero-length)’ while 204 communicates ‘success, and there is intentionally no content to provide.‘

Technical Details

Protocol-level behavior differs from 200 responses in several ways. Per RFC 9110, a client receiving 204 knows not to expect a message body and should not attempt to read one. This has implications for HTTP/1.1 persistent connections - the connection remains open after 204 without needing to read a body. In HTTP/2 and HTTP/3, the END_STREAM flag is set on the HEADERS frame containing 204, indicating no DATA frames will follow. This optimization reduces round trips.\n\nCaching semantics for 204 are defined in RFC 9111. A 204 response is cacheable by default if it contains cache directives, though caching ‘no content’ is an unusual use case. The primary scenario is caching the fact that a resource was deleted or updated. For instance, DELETE /api/items/123 returning 204 could be cached to avoid redundant DELETE attempts. However, most implementations don’t cache 204 DELETE responses. For PUT/PATCH returning 204, caching becomes meaningful with ETag validators. A client can send If-None-Match with the ETag; if the resource hasn’t changed, the server responds 304 Not Modified (not 204), avoiding retransmission.\n\nContent-Length handling with 204 has nuance. RFC 9110 states that Content-Length can be present in 204 responses but MUST be 0. Some frameworks include Content-Length: 0 explicitly, while others omit it entirely. Both are correct. For chunked transfer encoding (Transfer-Encoding: chunked), the response includes the terminating zero-length chunk but no data chunks.\n\nBrowser behavior with 204 differs from 200. When JavaScript fetch() receives 204, response.text() and response.json() return empty values, and response.body is null. XMLHttpRequest treats 204 specially - xhr.response is an empty string, and xhr.responseText is empty. For navigation requests (clicking a link, form submission), receiving 204 keeps the current page unchanged rather than replacing it with a blank page. This is useful for form submissions where you want to confirm success without navigation.\n\nFramework-specific gotchas abound. Express.js: res.status(204).json({}) sends a body despite 204 - you must use res.status(204).end(). Spring: @ResponseStatus(HttpStatus.NO_CONTENT) on a method returning void works, but returning an object attempts to serialize a body. Django: HttpResponse(status=204) with content argument raises a warning in strict mode. FastAPI: return Response(status_code=204) works, but declaring a response model causes validation errors.\n\nSecurity considerations include information leakage through timing. If DELETE operations on non-existent resources return 404 immediately but existing resources return 204 after deletion work, timing differences reveal existence. Mitigation: perform authorization and existence checks, then do deletion work (even if resource already gone), ensuring consistent timing. For privacy-sensitive operations, consider returning 204 even on failure to avoid leaking whether resources exist.\n\nPerformance implications favor 204…

Code Example

// Enterprise-grade 204 handling with edge cases\nconst express = require('express');\nconst { body, validationResult } = require('express-validator');\nconst app = express();\n\n// Comprehensive DELETE endpoint\napp.delete('/api/resources/:id', async (req, res) => {\n const startTime = Date.now();\n const resourceId = req.params.id;\n \n try {\n // Check authorization first (before existence check)\n // This prevents leaking existence through 404 vs 403\n const hasPermission = await authService.canDeleteResource(\n req.user.id,\n resourceId\n );\n \n if (!hasPermission) {\n return res.status(403).json({\n error: 'Forbidden',\n message: 'You do not have permission to delete this resource'\n });\n }\n \n // Attempt deletion\n const result = await db.transaction(async (trx) => {\n // Delete related records first\n await trx('resource_tags').where('resource_id', resourceId).del();\n await trx('resource_files').where('resource_id', resourceId).del();\n \n // Delete main record\n const deleted = await trx('resources')\n .where('id', resourceId)\n .del();\n \n return deleted > 0;\n });\n \n // Add artificial delay for timing-attack mitigation\n // Ensures consistent response time whether resource existed or not\n const elapsed = Date.now() - startTime;\n const targetTime = 50; // ms\n if (elapsed < targetTime) {\n await new Promise(resolve => setTimeout(resolve, targetTime - elapsed));\n }\n \n // Return 204 whether resource existed or not (idempotency)\n // The desired state is achieved: resource is gone\n res.status(204).end();\n \n // Alternative approach (REST purist): return 404 if not found\n // if (!result) {\n // return res.status(404).json({ error: 'Resource not found' });\n // }\n // res.status(204).end();\n \n } catch (error) {\n console.error('Delete error:', error);\n res.status(500).json({ error: 'Internal server error' });\n }\n});\n\n// PUT with validation and ETag support\napp.put('/api/resources/:id',\n body('name').isString().isLength({ min: 1, max: 100 }),\n body('status').isIn(['draft', 'published', 'archived']),\n async (req, res) => {\n const errors = validationResult(req);\n if (!errors.isEmpty()) {\n return res.status(400).json({ errors: errors.array() });\n }\n \n const { name, status } = req.body;\n const resourceId = req.params.id;\n \n // Check If-Match for optimistic locking\n const clientETag = req.headers['if-match'];\n \n if (clientETag) {\n const current = await db.getResource(resourceId);\n if (!current) {\n return res.status(404).json({ error: 'Resource not found' });\n }\n \n const currentETag = generateETag(current);\n if (clientETag !== currentETag) {\n return res.status(412).json({\n error: 'Precondition Failed',\n messag...

Frequently Asked Questions

What's the difference between 204 No Content and 200 OK with empty body?

Semantically, 204 explicitly means 'there is intentionally no content' while 200 with empty body suggests 'here's your content (which happens to be empty)'. Practically, clients handle 204 specially - they don't wait for a body, won't try to parse it, and browsers keep the current page unchanged. Always use 204 when you have no content to return, not 200 with empty body.

Should DELETE return 204 or 200?

Generally use 204 for DELETE - the resource is deleted and there's nothing to return. Use 200 if you want to return metadata about the deleted resource (useful for 'undo' features) or confirmation details. For idempotent DELETE (deleting already-deleted resource), you can use 204 (emphasizes idempotency) or 404 (emphasizes accuracy). 204 is simpler for clients.

Can I include response body with 204?

No! Per RFC 9110, 204 responses MUST NOT contain a message body. If you call res.status(204).json({...}) in Express, you're violating the spec. Use res.status(204).end() instead. If you need to return data, use 200, not 204.

Should PUT/PATCH return 204 or 200 with updated resource?

For PUT, use 204 if the client has all the updated data (they sent it). Use 200 if server-side defaults, calculated fields, or timestamps were added - client needs to see the final state. For PATCH (partial updates), 200 with full resource is usually better since the client doesn't know all fields. Decide based on whether the client needs to see the final state.

Is 204 cacheable?

Yes, by default if cache directives are present, though caching 'no content' is unusual. The main use case is caching the fact that a resource was updated/deleted to avoid redundant operations. For PUT/PATCH with 204, ETags enable cache validation - clients can send If-None-Match and receive 304 Not Modified if unchanged.

Common Causes

  • Successful DELETE request removing a resource
  • PUT or PATCH request updating resource (client already has updated data)
  • OPTIONS preflight request for CORS (methods returned in Allow header)
  • POST request triggering action with side effect (dismiss notification, unsubscribe)
  • Successful form submission where navigation isn’t needed
  • API endpoint confirming action without returning data
  • Idempotent DELETE on already-deleted resource (depending on API design)

Implementation Guidance

  • Use res.status(204).end() in Express - never .json() or .send() with 204
  • For DELETE, return 204 unless you need to return deleted resource metadata
  • For PUT, use 204 if client has full state; use 200 if server adds fields
  • For PATCH, prefer 200 with full resource since client has partial state
  • Don’t include Content-Length header, or set it to 0 if included
  • Can include ETag header with 204 for cache validation
  • Can include Cache-Control, Expires headers to control caching
  • For idempotent DELETE, decide: 204 for consistency or 404 for accuracy
  • Add timing mitigation for security-sensitive deletes (consistent response time)
  • For browser form submissions, consider 204 to keep current page vs 200 to replace

Comments